Warum hat die Umkehrfunktion für die std::list
Klasse in der C ++ - Standardbibliothek eine lineare Laufzeit? Ich würde denken, dass für doppelt verknüpfte Listen die Umkehrfunktion O (1) gewesen sein sollte.
Das Umkehren einer doppelt verknüpften Liste sollte nur das Umschalten der Kopf- und Endzeiger beinhalten.
c++
c++11
stl
linked-list
Neugierig
quelle
quelle
Reverse
Funktion in O (1) implementiert wird?Antworten:
Hypothetisch
reverse
könnte O (1) gewesen sein . Es könnte (wieder hypothetisch) ein boolesches Listenmitglied gegeben haben, das angibt, ob die Richtung der verknüpften Liste derzeit dieselbe oder entgegengesetzt zu der ursprünglichen ist, in der die Liste erstellt wurde.Leider würde dies die Leistung grundsätzlich jeder anderen Operation verringern (allerdings ohne die asymptotische Laufzeit zu ändern). Bei jeder Operation müsste ein Boolescher Wert herangezogen werden, um zu prüfen, ob einem "nächsten" oder "vorherigen" Zeiger eines Links gefolgt werden soll.
Da dies vermutlich als relativ seltene Operation angesehen wurde, spezifizierte der Standard (der keine Implementierungen vorschreibt, sondern nur die Komplexität), dass die Komplexität linear sein könnte. Dies ermöglicht, dass "nächste" Zeiger immer eindeutig dieselbe Richtung bedeuten, was Operationen im allgemeinen Fall beschleunigt.
quelle
reverse
mit derO(1)
Komplexität des Big-os eines anderen Betriebes ohne Beeinträchtigung durch diesen Flag mit dem angegebenen Trick. In der Praxis ist jedoch eine zusätzliche Verzweigung bei jeder Operation kostspielig, selbst wenn es sich technisch um O (1) handelt. Im Gegensatz dazu können Sie keine Listenstruktur erstellen, in dersort
O (1) ist und alle anderen Operationen die gleichen Kosten verursachen. Der Punkt der Frage ist, dass Sie anscheinendO(1)
kostenlos rückgängig machen können, wenn Sie sich nur für Big O interessieren. Warum haben sie das nicht getan?std::uintptr_t
. Dann können Sie sie xor.std::uintptr_t
, Sie könnten in einchar
Array umwandeln und dann die Komponenten XOR. Es wäre langsamer, aber 100% tragbar. Wahrscheinlich können Sie zwischen diesen beiden Implementierungen wählen und die zweite nur dann als Ersatz verwenden, wenn sieuintptr_t
fehlt. Einige, wenn es in dieser Antwort beschrieben wird: stackoverflow.com/questions/14243971/…Es könnte sein , O (1) , wenn die Liste ein Flag speichern würde , die die Bedeutung der „ermöglicht Swapping
prev
“ und „next
“ Zeiger jeder Knoten. Wenn das Umkehren der Liste eine häufige Operation wäre, könnte eine solche Hinzufügung tatsächlich nützlich sein, und ich kenne keinen Grund, warum die Implementierung nach dem aktuellen Standard verboten wäre . Ein solches Flag würde jedoch das gewöhnliche Durchlaufen der Liste verteuern (wenn auch nur um einen konstanten Faktor), weil stattim der
operator++
Liste Iterator würden Sie bekommenDas ist nicht etwas, das Sie einfach hinzufügen möchten. Angesichts der Tatsache, dass Listen normalerweise viel häufiger durchlaufen werden als umgekehrt, wäre es für den Standard sehr unklug, dies zu tun beauftragen , diese Technik. Daher darf die umgekehrte Operation eine lineare Komplexität aufweisen. Beachten Sie jedoch, dass t ∈ O (1) ⇒ t ∈ O ( n ) ist, sodass, wie bereits erwähnt, die technische Implementierung Ihrer „Optimierung“ zulässig ist.
Wenn Sie aus Java oder einem ähnlichen Hintergrund stammen, fragen Sie sich möglicherweise, warum der Iterator das Flag jedes Mal überprüfen muss. Könnten wir nicht stattdessen zwei unterschiedliche Iteratortypen haben, die beide von einem gemeinsamen Basistyp abgeleitet sind
std::list::begin
undstd::list::rbegin
den entsprechenden Iterator haben und polymorph zurückgeben? Während dies möglich wäre, würde dies das Ganze noch schlimmer machen, da das Vorrücken des Iterators jetzt ein indirekter (schwer zu inline) Funktionsaufruf wäre. In Java zahlen Sie diesen Preis ohnehin routinemäßig, aber dies ist einer der Gründe, warum viele Menschen nach C ++ greifen, wenn die Leistung kritisch ist.Wie Benjamin Lindley in den Kommentaren
reverse
hervorhob, scheint der einzige Ansatz, der vom Standard zugelassen wird, darin zu bestehen, einen Zeiger zurück auf die Liste im Iterator zu speichern, was einen doppelt indirekten Speicherzugriff verursacht.quelle
std::list::reverse
macht Iteratoren nicht ungültig.next
undprev
in einem Array und die Richtung als0
oder1
. Um vorwärts zu iterieren, folgen Siepointers[direction]
und iterieren rückwärtspointers[1-direction]
(oder umgekehrt). Dies würde immer noch ein wenig Overhead hinzufügen, aber wahrscheinlich weniger als ein Zweig.swap()
wird als konstante Zeit angegeben und macht keine Iteratoren ungültig.Da alle Container, die bidirektionale Iteratoren unterstützen, das Konzept von rbegin () und rend () haben, ist diese Frage sicherlich umstritten.
Es ist trivial, einen Proxy zu erstellen, der die Iteratoren umkehrt und über diesen auf den Container zugreift.
Diese Nichtoperation ist in der Tat O (1).
sowie:
erwartete Ausgabe:
In Anbetracht dessen scheint es mir, dass sich das Normungskomitee keine Zeit genommen hat, um O (1) die umgekehrte Reihenfolge des Containers zu bestimmen, da dies nicht erforderlich ist, und die Standardbibliothek basiert weitgehend auf dem Prinzip, nur das zu beauftragen, was währenddessen unbedingt erforderlich ist Vermeidung von Doppelarbeit.
Nur mein 2c.
quelle
Weil es jeden Knoten (
n
insgesamt) durchlaufen und seine Daten aktualisieren muss (der Aktualisierungsschritt ist in der TatO(1)
). Dies macht den gesamten VorgangO(n*1) = O(n)
.quelle
Außerdem werden der vorherige und der nächste Zeiger für jeden Knoten ausgetauscht. Deshalb braucht es Linear. Obwohl dies in O (1) möglich ist, wenn die Funktion, die dieses LL verwendet, auch Informationen über LL als Eingabe verwendet, z. B. ob sie normal oder umgekehrt zugreift.
quelle
Nur eine Algorithmuserklärung. Stellen Sie sich vor, Sie haben ein Array mit Elementen und müssen es dann invertieren. Die Grundidee besteht darin, bei jedem Element zu iterieren und das Element an der ersten Position in die letzte Position, das Element an der zweiten Position in die vorletzte Position usw. zu ändern. Wenn Sie in die Mitte des Arrays gelangen, werden alle Elemente geändert, also in (n / 2) Iterationen, die als O (n) betrachtet werden.
quelle
Es ist O (n), einfach weil es die Liste in umgekehrter Reihenfolge kopieren muss. Jede einzelne Elementoperation ist O (1), aber es gibt n davon in der gesamten Liste.
Natürlich sind einige Operationen mit konstanter Zeit erforderlich, um den Platz für die neue Liste einzurichten und anschließend die Zeiger zu ändern usw. Die O-Notation berücksichtigt keine einzelnen Konstanten, sobald Sie einen n-Faktor erster Ordnung einschließen.
quelle