Ich habe mir STL-Container angesehen und versucht herauszufinden, was sie wirklich sind (dh die verwendete Datenstruktur), und die Deque hat mich aufgehalten: Ich dachte zuerst, dass es sich um eine doppelt verknüpfte Liste handelt, die das Einfügen und Löschen von beiden Seiten in ermöglicht konstante Zeit, aber ich bin beunruhigt über das Versprechen des Betreibers [], in konstanter Zeit zu tun. In einer verknüpften Liste sollte der willkürliche Zugriff O (n) sein, oder?
Und wenn es sich um ein dynamisches Array handelt, wie kann es Elemente in konstanter Zeit hinzufügen ? Es sollte erwähnt werden, dass eine Neuzuweisung auftreten kann und dass O (1) wie bei einem Vektor amortisierte Kosten sind .
Ich frage mich also, was diese Struktur ist, die willkürlichen Zugriff in konstanter Zeit ermöglicht und gleichzeitig nie an einen neuen größeren Ort verlegt werden muss.
deque
steht für Double Ended Queue , obwohl die strenge Anforderung des O (1) -Zugriffs auf mittlere Elemente offensichtlich speziell für C ++ giltAntworten:
Eine Deque ist etwas rekursiv definiert: Intern wird eine doppelendige Warteschlange mit Blöcken fester Größe verwaltet. Jeder Block ist ein Vektor, und die Warteschlange („Karte“ in der Grafik unten) der Blöcke selbst ist ebenfalls ein Vektor.
Es gibt eine großartige Analyse der Leistungsmerkmale und des Vergleichs mit dem
vector
Over bei CodeProject .Die Implementierung der GCC-Standardbibliothek verwendet intern a
T**
, um die Karte darzustellen. Jeder Datenblock ist ein Datenblock,T*
der mit einer festen Größe zugeordnet ist__deque_buf_size
(die davon abhängtsizeof(T)
).quelle
Stellen Sie es sich als einen Vektor von Vektoren vor. Nur sind sie keine Standard
std::vector
s.Der äußere Vektor enthält Zeiger auf die inneren Vektoren. Wenn seine Kapazität durch Neuzuweisung geändert wird, anstatt den gesamten leeren Raum dem Ende
std::vector
zuzuweisen, teilt er den leeren Raum am Anfang und am Ende des Vektors zu gleichen Teilen auf. Dies ermöglichtpush_front
undpush_back
auf diesem Vektor können beide in der amortisierten O (1) -Zeit auftreten.Das Verhalten des inneren Vektors muss sich ändern, je nachdem, ob es sich vorne oder hinten auf dem befindet
deque
. Auf der Rückseite kann es sich wie ein Standard verhalten, beistd::vector
dem es am Ende wächst undpush_back
in O (1) -Zeit auftritt. An der Front muss es das Gegenteil tun und am Anfang mit jedem wachsenpush_front
. In der Praxis wird dies leicht erreicht, indem ein Zeiger auf das vordere Element und die Wachstumsrichtung zusammen mit der Größe hinzugefügt wird. Mit dieser einfachen Modifikationpush_front
kann auch O (1) mal sein.Der Zugriff auf ein Element erfordert das Versetzen und Teilen auf den richtigen äußeren Vektorindex, der in O (1) auftritt, und das Indizieren in den inneren Vektor, der ebenfalls O (1) ist. Dies setzt voraus, dass die inneren Vektoren alle eine feste Größe haben, mit Ausnahme derjenigen am Anfang oder am Ende des
deque
.quelle
Ein Behälter, der in beide Richtungen wachsen kann.
Deque wird normalerweise als
vector
von implementiertvectors
(eine Liste von Vektoren kann keinen zeitlich konstanten Direktzugriff ermöglichen). Während die Größe der Sekundärvektoren implementierungsabhängig ist, besteht ein üblicher Algorithmus darin, eine konstante Größe in Bytes zu verwenden.quelle
array
an nichts odervector
an irgendetwas, das amortisierteO(1)
push_front versprechen kann. Zumindest die innere der beiden Strukturen muss in der Lage sein, eineO(1)
push_front zu haben , die weder einearray
noch einevector
garantieren kann.vector
das nicht, aber es ist einfach genug, um es so zu machen.(Dies ist eine Antwort, die ich in einem anderen Thread gegeben habe . Im Wesentlichen argumentiere ich, dass selbst ziemlich naive Implementierungen, die eine einzige verwenden
vector
, den Anforderungen von "konstantem nicht amortisiertem push_ {front, back}" entsprechen. Sie könnten überrascht sein und denke, dass dies unmöglich ist, aber ich habe andere relevante Zitate im Standard gefunden, die den Kontext auf überraschende Weise definieren. Bitte nehmen Sie mit, wenn ich in dieser Antwort einen Fehler gemacht habe, wäre es sehr hilfreich zu identifizieren, welche Dinge Ich habe richtig gesagt und wo meine Logik zusammengebrochen ist.)In dieser Antwort versuche ich nicht, eine gute Implementierung zu identifizieren , sondern nur zu helfen, die Komplexitätsanforderungen im C ++ - Standard zu interpretieren. Ich zitiere aus N3242 , das laut Wikipedia das neueste frei verfügbare C ++ 11-Standardisierungsdokument ist. (Es scheint anders organisiert zu sein als der endgültige Standard, und daher werde ich die genauen Seitenzahlen nicht angeben. Natürlich haben sich diese Regeln im endgültigen Standard möglicherweise geändert, aber ich glaube nicht, dass dies geschehen ist.)
A
deque<T>
könnte mit a korrekt implementiert werdenvector<T*>
. Alle Elemente werden auf den Heap kopiert und die Zeiger in einem Vektor gespeichert. (Mehr zum Vektor später).Warum
T*
stattT
? Weil der Standard das verlangt(meine Betonung). Das
T*
hilft, das zu befriedigen. Es hilft uns auch, dies zu befriedigen:Nun zum (kontroversen) Teil. Warum ein verwenden
vector
, um das zu speichernT*
? Es gibt uns zufälligen Zugriff, was ein guter Anfang ist. Vergessen wir für einen Moment die Komplexität des Vektors und bauen wir sorgfältig darauf auf:Der Standard spricht von "der Anzahl der Operationen an den enthaltenen Objekten". Für
deque::push_front
diese deutlich 1 , da genau istT
Gegenstand ist so konstruiert und Null der bestehendenT
Objekte gelesen werden oder in irgendeiner Weise abgetastet. Diese Zahl 1 ist eindeutig eine Konstante und unabhängig von der Anzahl der Objekte, die sich derzeit in der Deque befinden. Dies erlaubt uns zu sagen, dass:"Für uns
deque::push_front
ist die Anzahl der Operationen an den enthaltenen Objekten (dem Ts) festgelegt und unabhängig von der Anzahl der Objekte, die sich bereits in der Deque befinden."Natürlich wird sich die Anzahl der Operationen auf dem
T*
nicht so gut benehmen. Wenn dasvector<T*>
zu groß wird, wird es neu zugewiesen und vieleT*
s werden kopiert. Ja, die Anzahl der Operationen auf demT*
wird stark variieren, aber die Anzahl der Operationen aufT
wird nicht beeinflusst.Warum interessiert uns diese Unterscheidung zwischen Zählvorgängen
T
und ZählvorgängenT*
? Es ist, weil der Standard sagt:Für die
deque
sind die enthaltenen Objekte dieT
, nicht dieT*
, was bedeutet, dass wir jede Operation ignorieren können, die a kopiert (oder neu zuordnet)T*
.Ich habe nicht viel darüber gesagt, wie sich ein Vektor in einer Deque verhalten würde. Vielleicht würden wir es als kreisförmigen Puffer interpretieren (wobei der Vektor immer sein Maximum
capacity()
einnimmt und dann alles in einen größeren Puffer umverteilt, wenn der Vektor voll ist. Die Details spielen keine Rolle.In den letzten Absätzen haben wir
deque::push_front
die Beziehung zwischen der Anzahl der Objekte in der Deque und der Anzahl der Operationen, die push_front für enthalteneT
Objekte ausführt, analysiert. Und wir fanden heraus, dass sie unabhängig voneinander waren. Da der Standard vorschreibt, dass Komplexität in Bezug auf den Betrieb im Betrieb istT
, können wir sagen, dass dies eine konstante Komplexität aufweist.Ja, die Operations-On-T * -Komplexität wird (aufgrund der
vector
) amortisiert , aber wir sind nur an der Operations-On-T-Komplexität interessiert und diese ist konstant (nicht amortisiert).Die Komplexität von vector :: push_back oder vector :: push_front spielt bei dieser Implementierung keine Rolle. Diese Überlegungen betreffen Operationen an
T*
und sind daher irrelevant. Wenn sich der Standard auf den "konventionellen" theoretischen Begriff der Komplexität beziehen würde, hätten sie sich nicht explizit auf die "Anzahl der Operationen an den enthaltenen Objekten" beschränkt. Interpretiere ich diesen Satz über?quelle
list
unabhängig von der aktuellen Größe der Liste. Wenn die Liste zu groß ist, ist die Zuordnung langsam oder schlägt fehl. Soweit ich sehen kann, hat der Ausschuss daher beschlossen, nur die Operationen anzugeben, die objektiv gezählt und gemessen werden können. (PS: Ich habe eine andere Theorie dazu für eine andere Antwort.)O(n)
dass die Anzahl der Operationen asymptotisch proportional zur Anzahl der Elemente ist. IE, Metaoperationen zählen. Andernfalls wäre es nicht sinnvoll, die Suche auf zu beschränkenO(1)
. Ergo qualifizieren sich verknüpfte Listen nicht.list
könnte a auch alsvector
Zeiger implementiert werden (Einfügungen in die Mitte führen unabhängig von der Listengröße zu einem Aufruf eines einzelnen Kopierkonstruktors, und dasO(N)
Mischen der Zeiger kann ignoriert werden, da Sie sind keine Operationen auf T).deque
diese Weise implementiert werden und (2) auf diese Weise "betrogen" werden (auch wenn dies nach dem Standard zulässig ist), wenn die Berechnung der algorithmischen Komplexität beim Schreiben effizienter Programme nicht hilfreich ist .Aus der Übersicht können Sie
deque
als denkendouble-ended queue
Die Daten in
deque
werden durch Chuncks mit festem Vektor gespeichert, die sindZeiger durch a
map
(das ist auch ein Stück Vektor, aber seine Größe kann sich ändern)Der Hauptteilcode von
deque iterator
lautet wie folgt:Der Hauptteilcode von
deque
lautet wie folgt:Im Folgenden werde ich Ihnen den Kerncode von
deque
hauptsächlich drei Teilen geben:Iterator
Wie konstruiere ich eine
deque
1. iterator (
__deque_iterator
)Das Hauptproblem des Iterators besteht darin, dass er beim ++, - Iterator möglicherweise zu einem anderen Block springt (wenn er auf die Kante des Blocks zeigt). Zum Beispiel gibt es drei Datenabschnitte:
chunk 1
,chunk 2
,chunk 3
.Die
pointer1
Zeiger auf den Anfang vonchunk 2
, wenn der Operator--pointer
auf das Ende von zeigtchunk 1
, auf denpointer2
.Im Folgenden werde ich die Hauptfunktion von geben
__deque_iterator
:Überspringen Sie zunächst einen beliebigen Abschnitt:
Beachten Sie, dass die
chunk_size()
Funktion, die die Blockgröße berechnet, zur Vereinfachung 8 zurückgibt.operator*
Holen Sie sich die Daten in den Blockoperator++, --
// Präfixformen des Inkrements
Iterator überspringe n Schritte / Direktzugriff2. Wie konstruiere ich a
deque
gemeinsame Funktion von
deque
Nehmen wir an, es
i_deque
gibt 20 int-Elemente mit einer0~19
Blockgröße von 8 und jetzt push_back 3 Elemente (0, 1, 2) ani_deque
:Es ist interne Struktur wie unten:
Drücken Sie dann erneut push_back, um einen neuen Block zuzuweisen:
Wenn wir
push_front
, wird es neuen Block vor dem vorherigen zuweisenstart
Beachten Sie, dass beim
push_back
Element in deque, wenn alle Karten und Blöcke gefüllt sind, eine neue Karte zugewiesen und Blöcke angepasst werden. Der obige Code kann jedoch ausreichen, um Sie zu verstehendeque
.quelle
Ich habe "Datenstrukturen und Algorithmen in C ++" von Adam Drozdek gelesen und fand dies nützlich. HTH.
Sie können in der Mitte das Array von Zeigern auf die Daten (Blöcke rechts) feststellen, und Sie können auch feststellen, dass sich das Array in der Mitte dynamisch ändert.
Ein Bild sagt mehr als tausend Worte.
quelle
deque
Teil gelesen und es ist ziemlich gut.Während der Standard keine bestimmte Implementierung vorschreibt (nur zeitlich konstanter Direktzugriff), wird eine Deque normalerweise als Sammlung zusammenhängender "Speicherseiten" implementiert. Neue Seiten werden nach Bedarf zugewiesen, Sie haben jedoch weiterhin wahlfreien Zugriff. Im Gegensatz dazu
std::vector
wird Ihnen nicht versprochen, dass Daten zusammenhängend gespeichert werden, aber wie bei einem Vektor erfordern Einfügungen in der Mitte viele Verschiebungen.quelle
insert
viel Umzug erforderlich ist, wie zeigt Experiment 4 hier einen erstaunlichen Unterschied zwischenvector::insert()
unddeque::insert()
?