Je regardais les conteneurs STL et essayais de comprendre ce qu'ils sont vraiment (c'est-à-dire la structure de données utilisée), et le deque m'a arrêté: j'ai d'abord pensé que c'était une double liste chaînée, ce qui permettrait l'insertion et la suppression des deux extrémités dans temps constant, mais je suis troublé par la promesse faite par l'opérateur [] de se faire en temps constant. Dans une liste chaînée, l'accès arbitraire doit être O (n), non?
Et si c'est un tableau dynamique, comment peut-il ajouter des éléments en temps constant? Il faut mentionner qu'une réallocation peut se produire, et que O (1) est un coût amorti, comme pour un vecteur .
Je me demande donc quelle est cette structure qui permet un accès arbitraire en temps constant, et en même temps n'a jamais besoin d'être déplacée vers un nouvel endroit plus grand.
deque
signifie file d'attente double , bien que, évidemment, l'exigence stricte d'accès O (1) aux éléments du milieu soit particulière à C ++Réponses:
Un deque est défini de manière quelque peu récursive: en interne, il maintient une file d'attente à deux extrémités de blocs de taille fixe. Chaque morceau est un vecteur, et la file d'attente («carte» dans le graphique ci-dessous) des morceaux elle-même est également un vecteur.
Il y a une excellente analyse des caractéristiques de performance et de la façon dont elles se comparent
vector
à celles de CodeProject .L'implémentation de la bibliothèque standard GCC utilise en interne a
T**
pour représenter la carte. Chaque bloc de données est unT*
qui est alloué avec une taille fixe__deque_buf_size
(qui dépend desizeof(T)
).la source
Imaginez-le comme un vecteur de vecteurs. Seulement, ils ne sont pas des
std::vector
articles standard .Le vecteur externe contient des pointeurs vers les vecteurs internes. Lorsque sa capacité est modifiée par réallocation, plutôt que d'allouer tout l'espace vide à la fin comme le
std::vector
fait, il divise l'espace vide en parties égales au début et à la fin du vecteur. Cela permet àpush_front
etpush_back
sur ce vecteur de se produire tous les deux en temps O (1) amorti.Le comportement du vecteur interne doit changer selon qu'il se trouve à l'avant ou à l'arrière du fichier
deque
. À l'arrière, il peut se comporter comme un standardstd::vector
là où il pousse à la fin etpush_back
se produit en temps O (1). À l'avant, il doit faire le contraire, en grandissant au début avec chacunpush_front
. En pratique, ceci est facilement réalisé en ajoutant un pointeur sur l'élément avant et la direction de croissance avec la taille. Avec cette simple modificationpush_front
peut également être le temps O (1).L'accès à n'importe quel élément nécessite un décalage et une division à l'indice de vecteur externe approprié qui se produit dans O (1), et une indexation dans le vecteur interne qui est également O (1). Cela suppose que les vecteurs internes sont tous de taille fixe, à l'exception de ceux du début ou de la fin du
deque
.la source
Un contenant qui peut pousser dans les deux sens.
Deque est généralement implémenté comme un
vector
ofvectors
(une liste de vecteurs ne peut pas donner un accès aléatoire à temps constant). Alors que la taille des vecteurs secondaires dépend de l'implémentation, un algorithme courant consiste à utiliser une taille constante en octets.la source
array
rien ni à quoi que ce soitvector
qui puisse promettreO(1)
push_front amorti . L'intérieur des deux structures au moins doit pouvoir avoir unO(1)
push_front, ce que ni unarray
ni unvector
ne peuvent garantir.vector
ne fait pas cela, mais c'est une modification assez simple pour le rendre ainsi.(C'est une réponse que j'ai donnée dans un autre thread . Essentiellement, je soutiens que même les implémentations assez naïves, en utilisant un seul
vector
, sont conformes aux exigences de "push_ {front, back} constant non amorti". Vous pourriez être surpris , et je pense que cela est impossible, mais j'ai trouvé d'autres citations pertinentes dans la norme qui définissent le contexte de manière surprenante. Veuillez me supporter; si j'ai fait une erreur dans cette réponse, il serait très utile d'identifier les éléments J'ai dit correctement et où ma logique est tombée en panne.)Dans cette réponse, je n'essaie pas d'identifier une bonne implémentation, j'essaie simplement de nous aider à interpréter les exigences de complexité de la norme C ++. Je cite N3242 , qui est, selon Wikipédia , le dernier document de normalisation C ++ 11 disponible gratuitement. (Il semble être organisé différemment de la norme finale, et je ne citerai donc pas les numéros de page exacts. Bien sûr, ces règles ont peut-être changé dans la norme finale, mais je ne pense pas que cela se soit produit.)
A
deque<T>
pourrait être implémenté correctement en utilisant unvector<T*>
. Tous les éléments sont copiés sur le tas et les pointeurs stockés dans un vecteur. (Plus d'informations sur le vecteur plus tard).Pourquoi
T*
au lieu deT
? Parce que la norme l'exige(je souligne). Les
T*
aides pour satisfaire cela. Cela nous aide également à satisfaire ceci:Maintenant pour le peu (controversé). Pourquoi utiliser un
vector
pour stocker leT*
? Cela nous donne un accès aléatoire, ce qui est un bon début. Oublions un instant la complexité du vecteur et construisons-y soigneusement:La norme parle du "nombre d'opérations sur les objets contenus". Car
deque::push_front
c'est clairement 1 car exactement unT
objet est construit et aucun desT
objets existants n'est lu ou scanné de quelque manière que ce soit. Ce nombre, 1, est clairement une constante et est indépendant du nombre d'objets actuellement dans le deque. Cela nous permet de dire que:"Pour notre
deque::push_front
, le nombre d'opérations sur les objets contenus (les Ts) est fixe et est indépendant du nombre d'objets déjà dans le deque."Bien sûr, le nombre d'opérations sur le
T*
ne sera pas aussi bien comporté. Lorsque levector<T*>
fichier devient trop grand, il sera réaffecté et de nombreuxT*
s seront copiés. Alors oui, le nombre d'opérations sur leT*
variera énormément, mais le nombre d'opérations surT
ne sera pas affecté.Pourquoi nous soucions-nous de cette distinction entre compter les opérations
T
et compter les opérationsT*
? C'est parce que la norme dit:Pour le
deque
, les objets contenus sont leT
, pas leT*
, ce qui signifie que nous pouvons ignorer toute opération qui copie (ou reallocs) aT*
.Je n'ai pas beaucoup parlé de la façon dont un vecteur se comporterait dans une deque. Peut-être que nous l'interpréterions comme un tampon circulaire (avec le vecteur prenant toujours son maximum
capacity()
, puis réallouer tout dans un tampon plus grand lorsque le vecteur est plein. Les détails n'ont pas d'importance.Dans les derniers paragraphes, nous avons analysé
deque::push_front
et la relation entre le nombre d'objets dans le deque déjà et le nombre d'opérations effectuées par push_front sur lesT
objets contenus . Et nous avons constaté qu'ils étaient indépendants les uns des autres. Comme la norme stipule que la complexité est en termes d'opérations surT
, nous pouvons dire que cela a une complexité constante.Oui, la complexité Operations-On-T * est amortie (en raison de la
vector
), mais nous ne nous intéressons qu'à la complexité Operations-On-T * et c'est constant (non amorti).La complexité de vector :: push_back ou vector :: push_front n'est pas pertinente dans cette implémentation; ces considérations impliquent des opérations
T*
et ne sont donc pas pertinentes. Si le standard faisait référence à la notion théorique «conventionnelle» de complexité, alors ils ne se seraient pas explicitement limités au «nombre d'opérations sur les objets contenus». Est-ce que je surinterprète cette phrase?la source
list
quelle que soit la taille actuelle de la liste; si la liste est trop grande, l'allocation sera lente ou échouera. Par conséquent, pour autant que je sache, le comité a pris la décision de ne spécifier que les opérations qui peuvent être objectivement comptées et mesurées. (PS: j'ai une autre théorie à ce sujet pour une autre réponse.)O(n)
que le nombre d'opérations est asymptotiquement proportionnel au nombre d'éléments. IE, les méta-opérations comptent. Sinon, limiter la recherche àO(1)
. Ergo, les listes liées ne sont pas éligibles.list
pourrait également être implémenté en tant quevector
pointeurs (les insertions au milieu se traduiront par un seul appel de constructeur de copie, quelle que soit la taille de la liste, et leO(N)
brassage des pointeurs peut être ignoré car ce ne sont pas des opérations sur T).deque
cette manière et (2) "tricher" de cette manière (même si cela est autorisé par la norme) lorsque le calcul de la complexité algorithmique n'est pas utile pour écrire des programmes efficaces .Vue d'ensemble, vous pouvez penser
deque
comme undouble-ended queue
Les données dans
deque
sont stockées par des morceaux de vecteur de taille fixe, qui sontpointé par a
map
(qui est aussi un morceau de vecteur, mais sa taille peut changer)Le code de la partie principale du
deque iterator
est comme ci-dessous:Le code de la partie principale du
deque
est comme ci-dessous:Ci-dessous, je vais vous donner le code de base de
deque
, principalement environ trois parties:itérateur
Comment construire un
deque
1. itérateur (
__deque_iterator
)Le principal problème de l'itérateur est, quand ++, - iterator, il peut passer à un autre morceau (s'il pointe vers le bord du morceau). Par exemple, il y a trois morceaux de données:
chunk 1
,chunk 2
,chunk 3
.Les
pointer1
pointeurs vers le début dechunk 2
, lorsque l'opérateur--pointer
pointera vers la fin dechunk 1
, de manière à ce que lepointer2
.Ci-dessous, je vais donner la fonction principale de
__deque_iterator
:Tout d'abord, passez à n'importe quel morceau:
Notez que, la
chunk_size()
fonction qui calcule la taille du morceau, vous pouvez penser qu'elle renvoie 8 pour simplifier ici.operator*
obtenir les données dans le blocoperator++, --
// formes de préfixe d'incrément
itérateur sauter n étapes / accès aléatoire2. Comment construire un
deque
fonction commune de
deque
Supposons qu'il y
i_deque
ait 20 éléments int0~19
dont la taille de bloc est de 8, et maintenant push_back 3 éléments (0, 1, 2) ài_deque
:C'est la structure interne comme ci-dessous:
Puis push_back à nouveau, il invoquera allouer un nouveau bloc:
Si nous
push_front
, il allouera un nouveau morceau avant le précédentstart
Notez lorsque l'
push_back
élément dans deque, si toutes les cartes et les morceaux sont remplis, cela provoquera l'allocation d'une nouvelle carte et l'ajustement des morceaux.Mais le code ci-dessus peut être suffisant pour que vous compreniezdeque
.la source
Je lisais "Structures de données et algorithmes en C ++" par Adam Drozdek, et j'ai trouvé cela utile. HTH.
Vous pouvez remarquer au milieu se trouve le tableau de pointeurs vers les données (morceaux à droite), et vous pouvez également remarquer que le tableau au milieu change dynamiquement.
Une image vaut mille mots.
la source
deque
partie et c'est assez bon.Bien que la norme n'impose aucune implémentation particulière (uniquement un accès aléatoire à temps constant), un deque est généralement implémenté comme une collection de "pages" mémoire contiguës. De nouvelles pages sont allouées selon les besoins, mais vous avez toujours un accès aléatoire. Contrairement à
std::vector
, vous n'êtes pas promis que les données sont stockées de manière contiguë, mais comme le vecteur, les insertions au milieu nécessitent beaucoup de déplacement.la source
insert
nécessite beaucoup de délocalisation, comment l'expérience 4 montre-t-elle ici une différence stupéfiante entrevector::insert()
etdeque::insert()
?