Eine doppelt verknüpfte Liste hat nur minimalen Overhead (nur einen weiteren Zeiger pro Zelle) und ermöglicht es Ihnen, an beide Enden anzuhängen, hin und her zu gehen und im Allgemeinen viel Spaß zu haben.
data-structures
functional-programming
Elliot Gorokhovsky
quelle
quelle
next
Zeiger des vorherigen Elements muss auf das nächste Element und derprev
Zeiger des nächsten Elements auf das vorherige Element zeigen. Eines dieser beiden Elemente wird jedoch vor dem anderen erstellt, was bedeutet, dass eines dieser Elemente einen Zeiger haben muss, der auf ein Objekt zeigt, das noch nicht existiert! Denken Sie daran, dass Sie nicht zuerst ein Element, dann das andere erstellen und dann die Zeiger setzen können - sie sind unveränderlich. (Hinweis: Ich weiß, dass es einen Weg gibt, Faulheit auszunutzen, genannt "Tying the Knot".)Antworten:
Wenn Sie etwas genauer hinschauen, enthalten beide auch Arrays in der Basissprache:
Die funktionale Programmieranweisung hat jedoch lange Zeit einfach verknüpfte Listen gegenüber Arrays oder doppelt verknüpften Listen hervorgehoben. Wahrscheinlich sogar überbetont. Es gibt jedoch mehrere Gründe dafür.
Erstens sind einfach verknüpfte Listen einer der einfachsten und dennoch nützlichsten rekursiven Datentypen. Ein benutzerdefiniertes Äquivalent zum Listentyp von Haskell kann folgendermaßen definiert werden:
Die Tatsache, dass Listen ein rekursiver Datentyp sind, bedeutet, dass die Funktionen, die mit Listen arbeiten, im Allgemeinen eine strukturelle Rekursion verwenden . In Haskell Bedingungen: Sie Mustererkennung auf der Liste Bauer, und Sie Rekursion auf einem subpart der Liste. In diesen beiden grundlegenden Funktionsdefinitionen verwende ich die Variable
as
, um auf das Ende der Liste zu verweisen. Beachten Sie also, dass die rekursiven Aufrufe in der Liste "absteigen":Diese Technik garantiert, dass Ihre Funktion für alle endlichen Listen beendet wird, und ist auch eine gute Technik zur Problemlösung - sie teilt Probleme auf natürliche Weise in einfachere, haltbarere Unterabschnitte auf.
Einfach verknüpfte Listen sind daher wahrscheinlich der beste Datentyp, um die Schüler in diese Techniken einzuführen, die für die funktionale Programmierung sehr wichtig sind.
Der zweite Grund ist weniger ein Grund für "Warum einfach verknüpfte Listen" als vielmehr ein Grund für "Warum nicht doppelt verknüpfte Listen oder Arrays": Diese letzteren Datentypen erfordern häufig eine Mutation (modifizierbare Variablen), die sehr häufig funktioniert scheut sich vor. So wie es passiert:
vector
haben jedoch Techniken gefunden, die dieses Problem erheblich verbessern).Der dritte und letzte Grund gilt in erster Linie für faule Sprachen wie Haskell: Faule, einfach verknüpfte Listen ähneln in der Praxis häufig eher Iteratoren als eigentlichen In-Memory-Listen. Wenn Ihr Code die Elemente einer Liste nacheinander verbraucht und sie unterwegs auswirft, materialisiert der Objektcode nur die Listenzellen und ihren Inhalt, wenn Sie die Liste durchgehen.
Dies bedeutet, dass nicht die gesamte Liste gleichzeitig im Speicher vorhanden sein muss, sondern nur die aktuelle Zelle. Zellen vor der aktuellen können durch Müll gesammelt werden (was mit einer doppelt verknüpften Liste nicht möglich wäre). Zellen, die später als die aktuelle sind, müssen erst berechnet werden, wenn Sie dort ankommen.
Es geht noch weiter. In mehreren gängigen Haskell-Bibliotheken, der so genannten Fusion , wird eine Technik verwendet , bei der der Compiler Ihren Listenverarbeitungscode analysiert und Zwischenlisten erkennt, die nacheinander generiert und konsumiert und dann "weggeworfen" werden. Mit diesem Wissen kann der Compiler dann die Speicherzuordnung der Zellen dieser Listen vollständig eliminieren. Dies bedeutet, dass eine einfach verknüpfte Liste in einem Haskell-Quellprogramm nach der Kompilierung möglicherweise tatsächlich in eine Schleife anstelle einer Datenstruktur umgewandelt wird.
Fusion ist auch die Technik, mit der die oben genannte
vector
Bibliothek effizienten Code für unveränderliche Arrays generiert. Gleiches gilt für die äußerst beliebten Bibliothekenbytestring
(Byte-Arrays) undtext
(Unicode-Strings), die als Ersatz für Haskells nicht sehr guten nativenString
Typ (der[Char]
mit einer einfach verknüpften Liste von Zeichen identisch ist ) erstellt wurden. Im modernen Haskell gibt es also einen Trend, bei dem unveränderliche Array-Typen mit Fusionsunterstützung sehr verbreitet sind.Die Listenfusion wird durch die Tatsache erleichtert, dass Sie in einer einfach verknüpften Liste vorwärts, aber niemals rückwärts gehen können . Dies wirft ein sehr wichtiges Thema in der funktionalen Programmierung auf: Verwenden der "Form" eines Datentyps, um die "Form" einer Berechnung abzuleiten. Wenn Sie Elemente nacheinander verarbeiten möchten, ist eine einfach verknüpfte Liste ein Datentyp, der Ihnen bei Verwendung mit struktureller Rekursion dieses Zugriffsmuster auf ganz natürliche Weise bietet. Wenn Sie eine "Divide and Conquer" -Strategie verwenden möchten, um ein Problem anzugreifen, unterstützen Baumdatenstrukturen dies in der Regel sehr gut.
Viele Leute verlassen den funktionalen Programmierwagen frühzeitig, um sich mit den einfach verknüpften Listen vertraut zu machen, aber nicht mit den fortgeschritteneren zugrunde liegenden Ideen.
quelle
Weil sie gut mit Unveränderlichkeit arbeiten. Angenommen, Sie haben zwei unveränderliche Listen
[1, 2, 3]
und[10, 2, 3]
. Dargestellt als einfach verknüpfte Listen, bei denen jedes Element in der Liste ein Knoten ist, der das Element und einen Zeiger auf den Rest der Liste enthält, sehen sie folgendermaßen aus:Sehen Sie, wie die
[2, 3]
Portionen identisch sind? Bei veränderlichen Datenstrukturen handelt es sich um zwei verschiedene Listen, da der Code, der neue Daten in eine von ihnen schreibt, keinen Einfluss auf den Code haben muss, der die andere verwendet. Bei unveränderlichen Daten wissen wir jedoch, dass sich der Inhalt der Listen niemals ändern wird und Code keine neuen Daten schreiben kann. So können wir die Schwänze wiederverwenden und die beiden Listen einen Teil ihrer Struktur gemeinsam nutzen:Da Code, der die beiden Listen verwendet, diese niemals mutiert, müssen wir uns nie um Änderungen an einer Liste kümmern, die sich auf die andere auswirken. Dies bedeutet auch, dass Sie beim Hinzufügen eines Elements zur Vorderseite der Liste keine neue Liste kopieren und erstellen müssen.
Wenn Sie jedoch versuchen,
[1, 2, 3]
und[10, 2, 3]
als doppelt verknüpfte Listen darzustellen :Jetzt sind die Schwänze nicht mehr identisch. Der erste
[2, 3]
hat einen Zeiger auf1
am Kopf, der zweite hat einen Zeiger auf10
. Wenn Sie dem Kopf der Liste ein neues Element hinzufügen möchten, müssen Sie außerdem den vorherigen Kopf der Liste mutieren, damit er auf den neuen Kopf verweist.Das Problem mit mehreren Köpfen könnte möglicherweise behoben werden, indem jeder Knoten eine Liste bekannter Köpfe speichert und die Erstellung neuer Listen dies ändert. Anschließend müssen Sie jedoch daran arbeiten, diese Liste in Garbage Collection-Zyklen zu verwalten, wenn Versionen der Liste unterschiedliche Köpfe haben haben unterschiedliche Lebensdauern, da sie in verschiedenen Codeteilen verwendet werden. Es erhöht die Komplexität und den Overhead und ist es meistens nicht wert.
quelle
xs
konstruiert wird .1:xs
10:xs
Die Antwort von @ sacundim ist größtenteils richtig, aber es gibt auch einige andere wichtige Erkenntnisse zum Kompromiss zwischen Sprachdesigns und praktischen Anforderungen.
Objekte und Referenzen
Diese Sprachen schreiben normalerweise Objekte mit ungebundenen dynamischen Ausmaßen vor (oder nehmen diese an) (oder in Cs Sprache die Lebensdauer , obwohl sie aufgrund der Bedeutungsunterschiede von Objekten zwischen diesen Sprachen nicht exakt gleich sind, siehe unten), wobei erstklassige Referenzen vermieden werden (oder). zB Objektzeiger in C) und unvorhersehbares Verhalten in den semantischen Regeln (zB das undefinierte Verhalten von ISO C in Bezug auf Semantik).
Darüber hinaus ist der Begriff (erstklassiger) Objekte in solchen Sprachen konservativ einschränkend: Es werden standardmäßig keine "lokalen" Eigenschaften angegeben und garantiert. Dies ist in einigen ALGOL-ähnlichen Sprachen, deren Objekte keine ungebundenen dynamischen Ausmaße aufweisen (z. B. in C und C ++), völlig anders, wobei Objekte im Grunde genommen eine Art "typisierten Speicher" bedeuten, der normalerweise mit Speicherorten gekoppelt ist.
Das Codieren des Speichers innerhalb der Objekte bietet einige zusätzliche Vorteile, z. B. das Anhängen deterministischer Recheneffekte während ihrer gesamten Lebensdauer. Dies ist jedoch ein anderes Thema.
Probleme der Datenstruktursimulation
Ohne erstklassige Referenzen können einfach verknüpfte Listen aufgrund der Art der Darstellung dieser Datenstrukturen und der begrenzten primitiven Operationen in diesen Sprachen viele traditionelle (eifrige / veränderbare) Datenstrukturen nicht effektiv und portabel simulieren. (Im Gegenteil, in C können Sie verknüpfte Listen auch in einem streng konformen Programm recht einfach ableiten .) Und solche alternativen Datenstrukturen wie Arrays / Vektoren haben in der Praxis einige überlegene Eigenschaften im Vergleich zu einfach verknüpften Listen. Deshalb führt R 5 RS neue primitive Operationen ein.
Es gibt jedoch Unterschiede zwischen Vektor- / Array-Typen und doppelt verknüpften Listen. Ein Array wird häufig mit einer Komplexität der O (1) -Zugriffszeit und einem geringeren Speicherplatzaufwand angenommen. Dies sind hervorragende Eigenschaften, die von Listen nicht gemeinsam genutzt werden. (Obwohl genau genommen, wird beides nicht durch ISO C garantiert, aber Benutzer erwarten es fast immer und keine praktische Implementierung würde diese impliziten Garantien zu offensichtlich verletzen.) OTOH, eine doppelt verknüpfte Liste macht beide Eigenschaften oft noch schlimmer als eine einfach verknüpfte Liste , während die Rückwärts- / Vorwärtsiteration auch von einem Array oder einem Vektor (zusammen mit ganzzahligen Indizes) mit noch weniger Overhead unterstützt wird. Daher ist eine doppelt verknüpfte Liste im Allgemeinen nicht leistungsfähiger. Noch schlimmer, Die Leistung in Bezug auf die Cache-Effizienz und die Latenz bei der dynamischen Speicherzuweisung von Listen ist katastrophal schlechter als die Leistung für Arrays / Vektoren, wenn der Standardzuweiser verwendet wird, der von der zugrunde liegenden Implementierungsumgebung (z. B. libc) bereitgestellt wird. Ohne eine sehr spezifische und "clevere" Laufzeit, die solche Objekterstellungen stark optimiert, werden Array- / Vektortypen häufig verknüpften Listen vorgezogen. (Bei Verwendung von ISO C ++ gibt es beispielsweise eine Einschränkung
std::vector
solltestd::list
standardmäßig bevorzugt werden.) Daher ist die Einführung neuer Grundelemente zur spezifischen Unterstützung von (doppelt) verknüpften Listen definitiv nicht so vorteilhaft, dass Array- / Vektordatenstrukturen in der Praxis unterstützt werden.Um fair zu sein, haben Listen immer noch einige spezifische Eigenschaften, die besser sind als Arrays / Vektoren:
Diese Eigenschaften sind jedoch nicht allzu wichtig für eine Sprache mit integrierter Unterstützung für einfach verknüpfte Listen, die bereits für eine solche Verwendung geeignet ist. Obwohl es immer noch Unterschiede gibt, kann in Sprachen mit vorgeschriebenen dynamischen Ausmaßen von Objekten (was normalerweise bedeutet, dass ein Garbage Collector die baumelnden Referenzen fernhält) die Invalidierung je nach Absicht auch weniger wichtig sein. Die einzigen Fälle, in denen doppelt verknüpfte Listen gewinnen, können sein:
Unveränderlichkeit und Aliasing
In einer reinen Sprache wie Haskell sind Objekte unveränderlich. Das Objekt des Schemas wird häufig ohne Mutation verwendet. Diese Tatsache ermöglicht es, die Speichereffizienz durch Objektinternierung effektiv zu verbessern - implizite gemeinsame Nutzung mehrerer Objekte mit demselben Wert im laufenden Betrieb.
Dies ist eine aggressive Optimierungsstrategie auf hoher Ebene im Sprachdesign. Dies ist jedoch mit Implementierungsproblemen verbunden. Tatsächlich werden implizite Aliase in zugrunde liegende Speicherzellen eingeführt. Dies erschwert die Aliasing-Analyse. Infolgedessen gibt es wahrscheinlich weniger Möglichkeiten, den Aufwand für nicht erstklassige Referenzen zu beseitigen, selbst Benutzer berühren sie überhaupt nicht. Wenn in Sprachen wie Scheme die Mutation nicht vollständig ausgeschlossen ist, stört dies auch die Parallelität. In einer faulen Sprache (die ohnehin schon Leistungsprobleme durch Thunks hat) ist dies möglicherweise in Ordnung.
Für die allgemeine Programmierung kann eine solche Wahl des Sprachdesigns problematisch sein. Aber mit einigen gängigen funktionalen Codierungsmustern scheinen die Sprachen immer noch gut zu funktionieren.
quelle