it-swarm.com.de

Ist list :: size () wirklich O (n)?

Kürzlich bemerkte ich, dass einige Leute erwähnten, dass std::list::size() eine lineare Komplexität hat.
Laut einigenQuellen ist dies tatsächlich von der Implementierung abhängig, da der Standard nicht angibt, wie komplex er sein muss.
Der Kommentar in diesem Blogeintrag sagt:

Tatsächlich hängt es davon ab, welche STL Sie verwenden. Microsoft Visual Studio V6 implementiert size () als {return (_Size); } in der Erwägung, dass gcc (zumindest in den Versionen 3.3.2 und 4.1.0) dies als {return std :: distance (begin (), end ()) ausführt; } Die erste hat eine konstante Geschwindigkeit, die zweite eine o(N) Geschwindigkeit

  1. Ich vermute also, dass für die VC++ - Menge size() eine konstante Komplexität aufweist, da Dinkumware diese Tatsache seit VC6 wahrscheinlich nicht mehr geändert hat. Bin ich da richtig
  2. Wie sieht es aktuell in gcc aus? Wenn es wirklich O (n) ist, warum haben sich die Entwickler dafür entschieden?
58
foraidt

Pre-C++ 11 Antwort

Sie haben Recht, dass der Standard nicht angibt, wie komplex list :: size () sein muss. Es wird jedoch empfohlen, dass die Komplexität konstant bleibt (Anmerkung A in Tabelle 65).

Hier ist ein interessanter Artikel von Howard Hinnant das erklärt, warum manche Leute denken, list :: size () sollte O(N) Komplexität haben (im Grunde, weil sie glauben, dass O(1) list :: size () bewirkt, dass list :: splice () die Komplexität von O(N) hat, und warum eine O(1) list :: size () eine gute Idee ist (im Meinung des Autors):

Ich denke, die Hauptpunkte in der Zeitung sind:

  • es gibt nur wenige Situationen, in denen eine interne Zählung beibehalten wird, sodass list::size() O(1) bewirken kann, dass die Spleißoperation linear wird
  • es gibt wahrscheinlich noch viel mehr Situationen, in denen sich jemand der negativen Auswirkungen, die auftreten können, nicht bewusst ist, weil er eine O(N) size() aufruft (wie zum Beispiel sein einziges Beispiel, in dem list::size() aufgerufen wird, während er ein Schloss hält).
  • anstatt zuzulassen, dass size() O (N) ist, sollte der Standard im Interesse der geringsten Überraschung verlangen, dass jeder Container, der size() implementiert, es auf O(1) Weise implementiert. Wenn ein Container dies nicht kann, sollte er size() überhaupt nicht implementieren. In diesem Fall wird der Benutzer des Containers darauf hingewiesen, dass size() nicht verfügbar ist. Wenn er weiterhin die Anzahl der Elemente im Container abrufen möchte oder muss, kann er container::distance( begin(), end()) verwenden, um diesen Wert abzurufen dass es eine O(N) Operation ist.

Ich glaube, ich stimme den meisten seiner Überlegungen eher zu. Allerdings gefällt mir seine vorgeschlagene Ergänzung zu den splice()-Überladungen nicht. Ein n übergeben zu müssen, das distance( first, last) entsprechen muss, um ein korrektes Verhalten zu erzielen, scheint ein Rezept für schwer zu diagnostizierende Fehler zu sein.

Ich bin mir nicht sicher, was in Zukunft getan werden sollte oder könnte, da jede Änderung erhebliche Auswirkungen auf den vorhandenen Code haben würde. Aber im Moment denke ich, dass der vorhandene Code bereits betroffen ist - das Verhalten kann sich von Implementierung zu Implementierung für etwas, das genau definiert sein sollte, erheblich unterscheiden. Vielleicht funktioniert der Kommentar von onebyone, wonach die Größe 'zwischengespeichert' und als bekannt/unbekannt markiert ist, gut - Sie erhalten ein amortisiertes O(1) - Verhalten - das einzige Mal, dass Sie ein O(N) - Verhalten erhalten, ist wenn die Liste wird durch einige splice () -Operationen geändert. Das Schöne daran ist, dass es heute von Implementierern ohne Änderung des Standards durchgeführt werden kann (es sei denn, ich vermisse etwas).

Soweit ich weiß, ändert C++ 0x in diesem Bereich nichts.

50
Michael Burr

In C++ 11 ist es erforderlich, dass für any Standardcontainer die .size()-Operation in "konstanter" Komplexität (O (1)) abgeschlossen sein muss. (Tabelle 96 - Anforderungen an Behälter). In C++ 03 hatte .size()sollte eine konstante Komplexität, ist aber nicht erforderlich (siehe Ist std :: string size () eine O(1) - Operation? ). 

Die Änderung des Standards wird durch n2923 eingeführt: Angabe der Komplexität von size () (Revision 1)

Die Implementierung von .size() in libstdc ++ verwendet jedoch noch einen O(N) - Algorithmus in gcc bis 4.8:

  /**  Returns the number of elements in the %list.  */
  size_type
  size() const _GLIBCXX_NOEXCEPT
  { return std::distance(begin(), end()); }

Siehe auch Warum ist std :: list auf c ++ 11 größer? für Details warum es so gehalten wird.

Update: std::list::size() ist richtig O(1) , wenn gcc 5.0 im C++ 11-Modus (oder höher) verwendet wird.


Übrigens ist .size() in libc ++ korrekt O (1):

_LIBCPP_INLINE_VISIBILITY
size_type size() const _NOEXCEPT     {return base::__sz();}

...

__compressed_pair<size_type, __node_allocator> __size_alloc_;

_LIBCPP_INLINE_VISIBILITY
const size_type& __sz() const _NOEXCEPT
    {return __size_alloc_.first();}
65
kennytm

Ich musste vorher in gcc 3.4's list :: size nachsehen, also kann ich folgendes sagen:

  1. es verwendet std :: distance (head, tail)
  2. std :: distance hat zwei Implementierungen: Für Typen, die RandomAccessIterator erfüllen, wird "tail-head" verwendet, und für Typen, die nur InputIterator erfüllen, wird ein O(n) -Algorithmus verwendet, der auf "iterator ++" basiert. Zählen, bis es den gegebenen Schwanz trifft.
  3. std :: list gibt RandomAccessIterator nicht frei, daher ist die Größe O (n).

Was das "Warum" angeht, kann ich nur sagen, dass std :: list für Probleme geeignet ist, die einen sequentiellen Zugriff erfordern. Das Speichern der Größe als Klassenvariable würde bei jedem Einfügen, Löschen usw. einen Overhead verursachen, und diese Verschwendung ist ein großes Nein für die Absicht der STL. Wenn Sie wirklich eine konstante Zeitgröße () benötigen, verwenden Sie std :: deque.

14
introp

Ich persönlich sehe das Problem mit dem Splice O(N) nicht als einzigen Grund, warum die Größe O (N) sein darf. Sie zahlen nicht für das, was Sie nicht verwenden ist ein wichtiges Motto von C++. In diesem Fall erfordert das Beibehalten der Listengröße bei jedem Einfügen/Löschen ein zusätzliches Inkrement/Dekrement, unabhängig davon, ob Sie die Größe der Liste überprüfen oder nicht. Dies ist ein kleiner fester Aufwand, der aber dennoch zu berücksichtigen ist.

Das Überprüfen der Größe einer Liste ist selten erforderlich. Iterieren von Anfang bis Ende ohne sich zu kümmern, ist die Gesamtgröße unendlich viel häufiger.

11
Greg Rogers

Ich würde zur Quelle gehen. Die STL-Seite von SGI sagt, dass es erlaubt ist, eine lineare Komplexität zu haben. Ich glaube, dass die Design-Richtlinie, die sie befolgten, darin bestand, die Implementierung der Liste so allgemein wie möglich zu gestalten und somit mehr Flexibilität bei der Verwendung von Listen zu ermöglichen.

4
Yuval F

Dieser Fehlerbericht: [C++ 0x] std :: list :: size complex erfasst die Tatsache, dass die Implementierung in GCC 4.x eine lineare Zeit ist, und wie der Übergang zu einer konstanten Zeit für C + erfolgt Aufgrund von ABI-Kompatibilitätsproblemen kam es langsam zu einem Anstieg von +11 (verfügbar in 5.0).

Die Manpage für die GCC 4.9-Serie enthält noch folgenden Haftungsausschluss:

Die Unterstützung für C++ 11 ist noch experimentell Und kann sich in zukünftigen Versionen auf inkompatible Weise ändern.


Auf den gleichen Fehlerbericht wird hier verwiesen: Sollte std :: list :: size in C++ 11 eine konstante Komplexität haben?

1
nobar

Wenn Sie Listen korrekt verwenden, bemerken Sie wahrscheinlich keinen Unterschied.

Listen eignen sich für große Datenstrukturen, die Sie ohne Kopieren neu anordnen möchten, für Daten, deren gültige Zeiger nach dem Einfügen beibehalten werden sollen.

Im ersten Fall macht es keinen Unterschied, im zweiten Fall würde ich die alte (kleinere) size () - Implementierung vorziehen.

Trotzdem geht es bei std mehr um Korrektheit und Standardverhalten als um die Benutzerfreundlichkeit, als um rohe Geschwindigkeit.

0
Luke Givens