Estaba mirando los contenedores STL y tratando de calcular cuáles son realmente (es decir, la estructura de datos utilizada), y el deque me detuvo: al principio pensé que era una lista de doble enlace, que permitiría la inserción y eliminación de ambos extremos en tiempo constante, pero me preocupa la promesa hecha por el operador [] de hacerse en tiempo constante. En una lista vinculada, el acceso arbitrario debe ser O (n), ¿verdad?
Y si es una matriz dinámica, ¿cómo puede agregar elementos en tiempo constante? Cabe mencionar que puede ocurrir una reasignación, y que O (1) es un costo amortizado, como para un vector .
Entonces, me pregunto cuál es esta estructura que permite el acceso arbitrario en tiempo constante y, al mismo tiempo, nunca necesita ser trasladado a un lugar nuevo más grande.
deque
significa cola doblemente terminada , aunque obviamente el requisito estricto de acceso O (1) a elementos intermedios es particular de C ++Respuestas:
Una deque se define de forma recursiva: mantiene internamente una cola de doble extremo de fragmentos de tamaño fijo. Cada fragmento es un vector, y la cola ("mapa" en el gráfico a continuación) de los fragmentos en sí también es un vector.
Hay un gran análisis de las características de rendimiento y cómo se compara con el anterior
vector
en CodeProject .La implementación de la biblioteca estándar de GCC usa internamente a
T**
para representar el mapa. Cada bloque de datos es unT*
que se asigna con un tamaño fijo__deque_buf_size
(que depende desizeof(T)
).fuente
Imagínelo como un vector de vectores. Solo que no son
std::vector
s estándar .El vector externo contiene punteros a los vectores internos. Cuando su capacidad se cambia mediante la reasignación, en lugar de asignar todo el espacio vacío al final como lo
std::vector
hace, divide el espacio vacío en partes iguales al principio y al final del vector. Esto permitepush_front
ypush_back
en este vector para tanto se producen en tiempo amortizado O (1).El comportamiento del vector interno debe cambiar dependiendo de si está en la parte frontal o posterior de la
deque
. En la parte posterior, puede comportarse como un estándarstd::vector
donde crece al final ypush_back
ocurre en el tiempo O (1). En el frente, debe hacer lo contrario, crecer al principio con cada unopush_front
. En la práctica, esto se logra fácilmente agregando un puntero al elemento frontal y la dirección de crecimiento junto con el tamaño. Con esta simple modificaciónpush_front
también puede ser O (1) tiempo.El acceso a cualquier elemento requiere compensación y división en el índice del vector externo apropiado que ocurre en O (1), e indexación en el vector interno que también es O (1). Esto supone que los vectores internos son todos de tamaño fijo, excepto los que están al principio o al final de
deque
.fuente
Un contenedor que puede crecer en cualquier dirección.
Deque generalmente se implementa como un
vector
devectors
(una lista de vectores no puede proporcionar acceso aleatorio de tiempo constante). Si bien el tamaño de los vectores secundarios depende de la implementación, un algoritmo común es usar un tamaño constante en bytes.fuente
array
en nada nivector
en nada que pueda prometerO(1)
push_front amortizado . El interior de las dos estructuras, al menos, debe poder tener unO(1)
push_front, que ni unarray
ni unvector
pueden garantizar.vector
no hace eso, pero es una modificación lo suficientemente simple como para que sea así.(Esta es una respuesta que he dado en otro hilo . Esencialmente estoy argumentando que incluso las implementaciones bastante ingenuas, usando una sola
vector
, se ajustan a los requisitos de "empuje constante no amortizado_ {frente, atrás}". Puede que se sorprenda , y creo que esto es imposible, pero he encontrado otras citas relevantes en el estándar que definen el contexto de una manera sorprendente. Tenga paciencia conmigo; si he cometido un error en esta respuesta, sería muy útil identificar qué cosas He dicho correctamente y dónde se ha roto mi lógica.)En esta respuesta, no estoy tratando de identificar una buena implementación, solo estoy tratando de ayudarnos a interpretar los requisitos de complejidad en el estándar C ++. Estoy citando de N3242 , que es, según Wikipedia , el último documento de estandarización de C ++ 11 disponible gratuitamente. (Parece estar organizado de manera diferente al estándar final, y por lo tanto no citaré los números de página exactos. Por supuesto, estas reglas podrían haber cambiado en el estándar final, pero no creo que eso haya sucedido).
UNA
deque<T>
podría implementarse correctamente utilizando avector<T*>
. Todos los elementos se copian en el montón y los punteros se almacenan en un vector. (Más sobre el vector más adelante).Por qué
T*
lugar deT
? Porque el estándar requiere que(mi énfasis) los
T*
ayudas para satisfacer eso. También nos ayuda a satisfacer esto:Ahora para el bit (controvertido). ¿Por qué usar un
vector
para almacenar elT*
? Nos da acceso aleatorio, que es un buen comienzo. Olvidemos la complejidad del vector por un momento y desarrollemos esto cuidadosamente:El estándar habla sobre "el número de operaciones en los objetos contenidos". Para
deque::push_front
esto es claramente 1 porque exactamente unoT
objeto se construye y cero de las existentesT
objetos se leen o escaneado de ninguna manera. Este número, 1, es claramente una constante y es independiente del número de objetos actualmente en la deque. Esto nos permite decir que:'Para nuestro
deque::push_front
, el número de operaciones en los objetos contenidos (el Ts) es fijo y es independiente del número de objetos que ya están en la deque'.Por supuesto, el número de operaciones en el
T*
no se comportará tan bien. Cuandovector<T*>
crezca demasiado, se reasignará y se copiarán muchos correos electrónicosT*
. Entonces, sí, el número de operaciones en elT*
variará enormemente, pero el número de operacionesT
no se verá afectado.¿Por qué nos importa esta distinción entre operaciones de
T
conteo y operaciones de conteoT*
? Es porque el estándar dice:Para el
deque
, los objetos contenidos son elT
, no elT*
, lo que significa que podemos ignorar cualquier operación que copia (o reasigna) aT*
.No he dicho mucho sobre cómo se comportaría un vector en una deque. Quizás lo interpretaríamos como un búfer circular (con el vector siempre ocupando su máximo
capacity()
, y luego reasignar todo en un búfer más grande cuando el vector esté lleno. Los detalles no importan.En los últimos párrafos, hemos analizado
deque::push_front
y la relación entre el número de objetos en la deque ya y el número de operaciones realizadas por push_front enT
objetos contenidos . Y descubrimos que eran independientes entre sí. Como el estándar exige que la complejidad esté en términos de operaciones en cursoT
, entonces podemos decir que esto tiene una complejidad constante.Sí, la Complejidad de Operaciones en T * se amortiza (debido a
vector
), pero solo nos interesa la Complejidad de Operaciones en T y esta es constante (no amortizada).La complejidad de vector :: push_back o vector :: push_front es irrelevante en esta implementación; esas consideraciones involucran operaciones
T*
y, por lo tanto, son irrelevantes. Si el estándar se refería a la noción teórica "convencional" de complejidad, entonces no se habrían restringido explícitamente al "número de operaciones en los objetos contenidos". ¿Estoy sobreinterpretando esa oración?fuente
list
independientemente del tamaño actual de la lista; Si la lista es demasiado grande, la asignación será lenta o fallará. Por lo tanto, hasta donde puedo ver, el comité tomó la decisión de especificar solo las operaciones que pueden contarse y medirse objetivamente. (PD: Tengo otra teoría sobre esto para otra respuesta.)O(n)
el número de operaciones es asintóticamente proporcional al número de elementos. IE, las metaoperaciones cuentan. De lo contrario, no tendría sentido limitar la búsquedaO(1)
. Ergo, las listas enlazadas no califican.list
podría implementarse como unvector
puntero (las inserciones en el medio darán como resultado una invocación de constructor de copia única , independientemente del tamaño de la lista, y laO(N)
combinación aleatoria de los punteros puede ignorarse porque no son operaciones en T).deque
esta manera y (2) "engañan" de esta manera (incluso si lo permite el estándar) cuando calcular la complejidad algorítmica no es útil para escribir programas eficientes .Desde la perspectiva general, puede pensar
deque
como undouble-ended queue
Los datos en
deque
son almacenados por fragmentos de vector de tamaño fijo, que sonseñalado por un
map
(que también es un fragmento de vector, pero su tamaño puede cambiar)El código de la parte principal del
deque iterator
es el siguiente:El código de la parte principal del
deque
es el siguiente:A continuación le daré el código central de
deque
, principalmente sobre tres partes:iterador
¿Cómo construir un
deque
1. iterador (
__deque_iterator
)El principal problema del iterador es, cuando ++, - iterator, puede saltar a otro fragmento (si apunta al borde del fragmento). Por ejemplo, hay tres fragmentos de datos:
chunk 1
,chunk 2
,chunk 3
.Los
pointer1
punteros al comienzo dechunk 2
, cuando el operador--pointer
apunta al final dechunk 1
, con el fin depointer2
.A continuación daré la función principal de
__deque_iterator
:En primer lugar, salta a cualquier fragmento:
Tenga en cuenta que, la
chunk_size()
función que calcula el tamaño del fragmento, puede pensar que devuelve 8 para simplificar aquí.operator*
obtener los datos en el fragmentooperator++, --
// formas de incremento de prefijo
Saltar iterador n pasos / acceso aleatorio2. Cómo construir un
deque
función común de
deque
Supongamos que
i_deque
tiene 20 elementos int0~19
cuyo tamaño de fragmento es 8 y ahora push_back 3 elementos (0, 1, 2) parai_deque
:Es la estructura interna como a continuación:
Luego push_back nuevamente, invocará asignar nuevo fragmento:
Si lo hacemos
push_front
, asignará un nuevo fragmento antes del anteriorstart
Tenga en cuenta que cuando el
push_back
elemento está en deque, si todos los mapas y fragmentos están llenos, se asignará un nuevo mapa y se ajustarán los fragmentos. Pero el código anterior puede ser suficiente para que usted lo entiendadeque
.fuente
Estaba leyendo "Estructuras de datos y algoritmos en C ++" por Adam Drozdek, y encontré esto útil. HTH
Puede notar que en el medio está la matriz de punteros a los datos (fragmentos a la derecha), y también puede notar que la matriz en el medio está cambiando dinámicamente.
Una imagen vale más que mil palabras.
fuente
deque
parte y está bastante bien.Si bien el estándar no exige ninguna implementación en particular (solo acceso aleatorio de tiempo constante), una deque generalmente se implementa como una colección de "páginas" de memoria contiguas. Las páginas nuevas se asignan según sea necesario, pero aún tiene acceso aleatorio. diferente a
std::vector
, no se le promete que los datos se almacenen de manera contigua, pero al igual que los vectores, las inserciones en el medio requieren mucha reubicación.fuente
insert
requiere mucha reubicación, ¿cómo muestra aquí el experimento 4 una asombrosa diferencia entrevector::insert()
ydeque::insert()
?