Eu estava olhando para contêineres STL e tentando descobrir o que eles realmente são (ou seja, a estrutura de dados usada), e o deque me parou: primeiro pensei que era uma lista com dois links, que permitiria a inserção e exclusão de ambas as extremidades em tempo constante, mas estou preocupado com a promessa feita pelo operador [] de ser feita em tempo constante. Em uma lista vinculada, o acesso arbitrário deve ser O (n), certo?
E se é um array dinâmico, como ele pode adicionar elementos em tempo constante? Deve-se mencionar que a realocação pode ocorrer e que O (1) é um custo amortizado, como para um vetor .
Então, eu me pergunto o que é essa estrutura que permite acesso arbitrário em tempo constante e, ao mesmo tempo, nunca precisa ser movida para um novo local maior.
deque
meios de fila de dupla extremidade , embora, obviamente, o requisito estrito de O (1) o acesso a elementos intermédios é determinada para C ++Respostas:
Um deque é definido recursivamente: internamente, ele mantém uma fila dupla de pedaços de tamanho fixo. Cada pedaço é um vetor, e a fila (“mapa” no gráfico abaixo) dos próprios pedaços também é um vetor.
Há uma ótima análise das características de desempenho e como ela se compara com a
vector
do CodeProject .A implementação da biblioteca padrão do GCC usa internamente a
T**
para representar o mapa. Cada bloco de dados é umT*
que é alocado com algum tamanho fixo__deque_buf_size
(que dependesizeof(T)
).fonte
Imagine isso como um vetor de vetores. Só que eles não são padrão
std::vector
.O vetor externo contém ponteiros para os vetores internos. Quando sua capacidade é alterada por meio de realocação, em vez de alocar todo o espaço vazio até o fim
std::vector
, ele divide o espaço vazio em partes iguais no início e no final do vetor. Isto permitepush_front
epush_back
neste vector para ambos ocorrem em O (1) tempo amortizado.O comportamento do vetor interno precisa mudar, dependendo da parte frontal ou traseira do
deque
. Na parte de trás, pode se comportar como um padrão,std::vector
onde cresce no final epush_back
ocorre no tempo O (1). Na frente, ele precisa fazer o oposto, crescendo no início de cada umpush_front
. Na prática, isso é facilmente alcançado adicionando um ponteiro ao elemento frontal e a direção do crescimento, juntamente com o tamanho. Com esta modificação simples,push_front
também pode ser o tempo O (1).O acesso a qualquer elemento requer deslocamento e divisão no índice de vetor externo adequado que ocorre em O (1) e indexação no vetor interno que também é O (1). Isso pressupõe que todos os vetores internos tenham tamanho fixo, exceto aqueles no início ou no final do
deque
.fonte
Um recipiente que pode crescer em qualquer direção.
Deque é normalmente implementado como um
vector
devectors
(uma lista de vetores não pode fornecer acesso aleatório em tempo constante). Enquanto o tamanho dos vetores secundários depende da implementação, um algoritmo comum é usar um tamanho constante em bytes.fonte
array
em nada ouvector
nada pode prometerO(1)
push_front amortizado . O interior das duas estruturas, pelo menos, deve ser capaz de ter umO(1)
push_front, que nem umarray
nem umvector
podem garantir.vector
não faz isso, mas é uma modificação simples o suficiente para fazê-lo.(Esta é uma resposta que eu dei em outro tópico . Essencialmente, estou argumentando que mesmo implementações bastante ingênuas, usando uma única
vector
, estão em conformidade com os requisitos de "push constante não amortizado_ {frente, trás}". Você pode se surpreender , e acho que isso é impossível, mas eu encontrei outras citações relevantes no padrão que definem o contexto de uma maneira surpreendente.Por favor, tenha paciência comigo: se eu cometer um erro nesta resposta, seria muito útil identificar quais coisas Eu disse corretamente e onde minha lógica foi quebrada.)Nesta resposta, não estou tentando identificar uma boa implementação, estou apenas tentando nos ajudar a interpretar os requisitos de complexidade no padrão C ++. Estou citando o N3242 , que é, segundo a Wikipedia , o mais recente documento de padronização C ++ 11 disponível gratuitamente. (Parece estar organizado de maneira diferente do padrão final e, portanto, não citarei os números exatos das páginas. É claro que essas regras podem ter sido alteradas no padrão final, mas acho que isso não aconteceu.)
A
deque<T>
pode ser implementado corretamente usando avector<T*>
. Todos os elementos são copiados para a pilha e os ponteiros armazenados em um vetor. (Mais sobre o vetor posteriormente).Por que ao
T*
invés deT
? Porque o padrão exige que(minha ênfase). As
T*
ajudas para satisfazer isso. Também nos ajuda a satisfazer isso:Agora, o pouco (controverso). Por que usar a
vector
para armazenar oT*
? Isso nos dá acesso aleatório, o que é um bom começo. Vamos esquecer a complexidade do vetor por um momento e desenvolver isso com cuidado:O padrão fala sobre "o número de operações nos objetos contidos". Pois
deque::push_front
isso é claramente 1, porque exatamente umT
objeto é construído e zero dosT
objetos existentes é lido ou verificado de qualquer maneira. Esse número, 1, é claramente uma constante e é independente do número de objetos atualmente no deque. Isso nos permite dizer que:'Para nós
deque::push_front
, o número de operações nos objetos contidos (os Ts) é fixo e é independente do número de objetos já existentes no deque.'Obviamente, o número de operações no
T*
não será tão bem-comportado. Quando ovector<T*>
tamanho for muito grande, ele será realocado e muitosT*
s serão copiados. Portanto, sim, o número de operações noT*
variará muito, mas o número de operações noT
não será afetado.Por que nos preocupamos com essa distinção entre contar operações
T
e contar operaçõesT*
? É porque o padrão diz:Para o
deque
, os objetos contidos sãoT
, e não oT*
, o que significa que podemos ignorar qualquer operação que copie (ou realoque) aT*
.Não falei muito sobre como um vetor se comportaria em um deque. Talvez o interpretássemos como um buffer circular (com o vetor sempre ocupando seu máximo
capacity()
e realocando tudo em um buffer maior quando o vetor estiver cheio. Os detalhes não importam.Nos últimos parágrafos, analisamos
deque::push_front
e a relação entre o número de objetos no deque e o número de operações executadas por push_front em objetos-contidosT
. E descobrimos que eles eram independentes um do outro. Como o padrão exige que a complexidade seja em termos de operaçõesT
, podemos dizer que isso tem complexidade constante.Sim, a complexidade Operações-em-T * é amortizada (devido a
vector
), mas estamos interessados apenas na complexidade em Operações-em-T e isso é constante (não-amortizado).A complexidade do vetor :: push_back ou vector :: push_front é irrelevante nesta implementação; essas considerações envolvem operações
T*
e, portanto, são irrelevantes. Se o padrão estivesse se referindo à noção teórica "convencional" de complexidade, eles não teriam explicitamente se restringido ao "número de operações nos objetos contidos". Estou interpretando demais essa frase?fonte
list
independentemente do tamanho atual da lista; se a lista for muito grande, a alocação será lenta ou falhará. Portanto, até onde posso ver, o comitê tomou a decisão de especificar apenas as operações que podem ser objetivamente contadas e medidas. (PS: Eu tenho outra teoria sobre isso para outra resposta.)O(n)
o número de operações é assintoticamente proporcional ao número de elementos. IE, contagem de meta-operações. Caso contrário, não faria sentido limitar a pesquisaO(1)
. Portanto, as listas vinculadas não se qualificam.list
pode ser implementada comovector
ponteiro (as inserções no meio resultarão em uma chamada de construtor de cópia única , independentemente do tamanho da lista, e oO(N)
embaralhamento dos ponteiros pode ser ignorado porque eles não são operações em T).deque
dessa maneira e (2) "trapaceando" dessa maneira (mesmo que permitidas pelo padrão) ao computar a complexidade algorítmica não é útil para escrever programas eficientes .Na visão geral, você pode pensar
deque
como umdouble-ended queue
Os dados
deque
são armazenados por chuncks de vetor de tamanho fixo, que sãoapontado por um
map
(que também é um pedaço do vetor, mas seu tamanho pode mudar)O código principal da peça
deque iterator
é o seguinte:O código principal da peça
deque
é o seguinte:Abaixo, apresentarei o código
deque
principal, principalmente sobre três partes:iterador
Como construir um
deque
1. iterador (
__deque_iterator
)O principal problema do iterador é que, quando ++, - iterator, ele pode pular para outro pedaço (se apontar para a borda do pedaço). Por exemplo, há três blocos de dados:
chunk 1
,chunk 2
,chunk 3
.Os
pointer1
ponteiros para o início dechunk 2
, quando o operador--pointer
apontará para o final dechunk 1
, para opointer2
.Abaixo darei a função principal de
__deque_iterator
:Primeiro, pule para qualquer parte:
Observe que, a
chunk_size()
função que calcula o tamanho do pedaço, você pode pensar em retornar 8 para simplificar aqui.operator*
obter os dados no pedaçooperator++, --
// prefixo formas de incremento
iterador pular n etapas / acesso aleatório2. Como construir um
deque
função comum de
deque
Vamos supor que
i_deque
tenha 20 elementos int0~19
cujo tamanho do bloco seja 8 e, agora, push_back 3 elementos (0, 1, 2) parai_deque
:É estrutura interna como abaixo:
Então push_back novamente, ele chamará alocar novo pedaço:
Se nós
push_front
, ele alocará novo pedaço antes do anteriorstart
Observe quando o
push_back
elemento dentro do deque, se todos os mapas e partes forem preenchidos, isso fará com que aloque um novo mapa e ajuste partes. Mas o código acima pode ser suficiente para você entenderdeque
.fonte
Eu estava lendo "Estruturas de dados e algoritmos em C ++" por Adam Drozdek, e achei isso útil. HTH.
Você pode notar no meio a matriz de ponteiros para os dados (pedaços à direita) e também pode notar que a matriz no meio está mudando dinamicamente.
Uma imagem vale mais que mil palavras.
fonte
deque
parte e é muito bom.Embora o padrão não exija nenhuma implementação específica (apenas acesso aleatório em tempo constante), um deque é geralmente implementado como uma coleção de "páginas" de memória contígua. Novas páginas são alocadas conforme necessário, mas você ainda tem acesso aleatório. Ao contrário
std::vector
, não é prometido que os dados sejam armazenados de forma contígua, mas, como os vetores, as inserções no meio exigem muita realocação.fonte
insert
requer muita deslocalização como é que experimento 4 aqui mostrar impressionante diferença entrevector::insert()
edeque::insert()
?