Verknüpfte Listen sind eine gängige Datenstruktur in der Informatik. Du kannst eine verknüpfte Liste verwenden, um z. B. eine Musikwiedergabeliste oder den Verlauf von Aktionen darzustellen.
Übersicht
Eine verknüpfte Liste ist eine lineare Datenstruktur, in der jedes Element einen Verweis auf das nächste Element in der Liste speichert. Jedes Element in einer verknüpften Liste wird als Knoten bezeichnet. Ein Knoten ist ein Container, der Daten und einen Verweis auf den nächsten Knoten enthält.
Durch die Verknüpfung verschiedener Knoten in einer Sequenz entsteht eine verknüpfte Liste. Der erste Knoten in der Liste wird als Kopfknoten und der letzte Knoten als Endknoten bezeichnet. Um die Liste zu durchlaufen, beginne am Kopf und gehe über den Verweis in jedem Knoten zum nächsten Element.

Wann verwenden
Verknüpfte Listen sind ein guter Ausgangspunkt für die Implementierung anderer Datenstrukturen wie Stapel oder Warteschlangen. Verknüpfte Listen sind eine einfache Möglichkeit, Daten für den sequentiellen Zugriff zu speichern, und sie unterstützen das schnelle Einfügen neuer Elemente. Im Gegensatz zu Arrays sind verknüpfte Listen im Speicher nicht zusammenhängend (nebeneinander). Das bedeutet, dass sich ein Knoten überall im Speicher befinden kann, und der Verweis auf den nächsten Knoten gibt Auskunft über die Position. Um Knoten aus einer verknüpften Liste einzufügen oder zu löschen, musst du lediglich die Verweise auf den nächsten Knoten ändern.
Wann nicht verwenden
Wenn dein Programm einen zufälligen Zugriff auf Elemente erfordert (z. B. die Auswahl eines zufälligen Spielers aus einer Liste). Diese Funktion fehlt bei verknüpften Listen. Verknüpfte Listen sind weniger effizient als Arrays, wenn du auf einen bestimmten Knoten in der Mitte der Liste zugreifen musst, da du am Kopf der Liste beginnen und zu dem gewünschten Knoten durchlaufen musst.
Implementierung in Verse
Die Verse-API verfügt nicht über eine eingebaute Implementierung von verknüpften Listen, aber du kannst selbst eine erstellen.
Verknüpfte Listen bestehen aus Knoten, und du musst eine Klasse definieren, die sie repräsentiert. Ein Beispiel für einen allgemeinen Knotentyp findest du unten:
list_node := class<unique>:
var Next:?list_node = false
Data:any = {}
Hier speichert die Optionsvariable Next
einen Verweis auf den nächsten Knoten in der Liste, der vom Typ list_node
ist. Beachte, dass Next
eine option
-Variable sein muss, da ein Knoten nicht immer einen nächsten Knoten hat, auf den er zeigen kann (wie z.B. der Endknoten einer verknüpften Liste). Wenn du die Verknüpfung deiner Liste dynamisch ändern möchtest, muss Next
eine var
sein. Der Knoten enthält auch einen Wert, der in Data
gespeichert wird. Da es sich um einen generischen Knotentyp handelt, ist Data
vom Typ any
, und der Knoten kann jeden Datentyp enthalten. Es könnte ein allgemeiner Typ, ein Container oder sogar eine andere verknüpfte Liste sein!
Node1 := list_node{ Next := false, Data := 1 } # Ein int
Node2 := list_node{ Next := false, Data := "String" } # Ein String
Node3 := list_node{ Next := false, Data := []int} # Ein Array
Node4 := list_node{ Next := false, Data := list_node } # list_node-Klasse
Node5 := list_node{ Next := false, Data := Node1 } # Der erste Knoten
Der Bezeichner <unique>
macht list_node
vergleichbar. So kannst du prüfen, ob ein Knoten gleich einem anderen Knoten ist, z. B. ob dein Knoten der Kopf oder das Ende einer Liste ist.
Wenn du eine spezifischere Implementierung benötigst, z. B. einen Knoten, der nur Zahlen oder Strings verarbeitet, stelle sicher, dass du den Typ von Data
entsprechend änderst. Wenn Data
vom Typ comparable
ist, kannst du den Wert in Data
mit einem anderen Wert vergleichen, um einen bestimmten Knoten zu finden.
Einzeln verknüpfte Listen
Der grundlegendste Typ einer verknüpften Liste ist eine einzeln verknüpfte Liste, bei der jeder Knoten einen Verweis auf den nächsten Knoten enthält (ein einzelner Link). Um deine Knoten in eine Liste zu verwandeln, verknüpfe sie miteinander, indem du die Next
-Verweise in jedem Knoten verwendest.

# Verknüpfung jedes Knotens mit dem nächsten Knoten in der Liste
set Node1.Next = option{Node2}
set Node2.Next = option{Node3}
set Node3.Next = option{Node4}
set Node4.Next = option{Node5}
Obwohl deine Knoten verknüpft sind, solltest du deine verknüpfte Liste in einer Klasse definieren. Dies erleichtert die Ausführung von Funktionen unter Verwendung deiner Liste, anstatt jedes Mal auf den ersten Knoten zu verweisen.
linked_list := class:
var Head:list_node
Eine einfache linked_list
Klasse enthält nur einen Verweis auf den Kopfknoten. Um eine Instanz deiner Klasse linked_list
zu erstellen, verknüpfe deine Knoten miteinander und instanziiere dann deine Liste mit Hilfe des Kopfknotens.
# Erstelle eine verknüpfte Liste mit node1 als Kopf.
BasicList:linked_list = linked_list{Head := Node1}
Um die Liste zu durchlaufen, beginne am Kopfknoten und durchlaufe die Liste, wobei du am Ende der Liste anhältst. Du kannst dies entweder in einer externen Funktion oder in einer Klassenfunktion innerhalb deiner Klasse linked_list
tun. Im Folgenden findest du eine Beispielfunktion, die die gesamte Liste vom Kopf bis zum Ende durchläuft:
linked_list := class:
var Head:list_node
TraverseList():void=
var CurrentNode:?list_node := option{Head} # Der aktuelle Knoten
var NodeNumber:int = 1 # Die Nummer des aktuellen Knotens
Print("Beginn beim Kopfknoten...")
# Wenn der aktuelle Knoten einen nächsten Knoten hat, setze den aktuellen Knoten auf
# den nächsten Knoten in der Liste
loop:
if(NextNode := CurrentNode?.Next?):
set CurrentNode = option{NextNode}
set NodeNumber += 1
Print("Gehe zu Knoten {NodeNumber}!")
else:
Print("Kein nächster Knoten, dies ist der Endknoten!")
break
Hier prüft die Funktion TraverseList()
, ob CurrentNode
einen Next
-Verweis enthält, und speichert ihn in NextNode
. Wenn dies der Fall ist, wird CurrentNode
wiederholt auf NextNode
gesetzt, bis der Endknoten erreicht ist. Der Endknoten hat keinen Next
-Verweis, so dass die Schleife hier endet.
Doppelt verknüpfte Listen
Im Gegensatz zu einfach verknüpften Listen enthalten die Knoten in einer doppelt verknüpften Liste auch einen Verweis auf den previous
(vorhergehenden) Knoten, den vorherigen Knoten.
doubly_linked_node := class<unique>:
var Next:?doubly_linked_node = false
var Previous:?doubly_linked_node = false
Data:any = {}

Dies bedeutet, dass du Knoten in einer doppelt verknüpften Liste sowohl vorwärts als auch rückwärts durchlaufen kannst. Dies kann entweder in einer externen Funktion oder in einer Klassenfunktion geschehen. Im Folgenden findest du eine Beispielfunktion, die die Liste vom Kopf bis zum Ende und zurück durchläuft:
doubly_linked_list := class():
Head:doubly_linked_node
# Eine LinkedList durchlaufen, aber sowohl rückwärts als auch vorwärts!
TraverseDoublyLinkedList():void=
var CurrentNode:?doubly_linked_node := option{Head}
var NodeNumber:int = 1
Print("Beginn bei Kopfknoten für den Vorwärtsdurchlauf...")
loop:
if(NextNode := CurrentNode?.Next?):
set CurrentNode = option{NextNode}
set NodeNumber += 1
Print("Gehe zu Knoten {NodeNumber}!")
else:
Print("Kein nächster Knoten, dies ist der Endknoten!")
break
Print("Jetzt gehen wir rückwärts zum Kopfknoten...")
loop:
if(PreviousNode := CurrentNode?.Previous?):
set CurrentNode = option{PreviousNode}
set NodeNumber -= 1
Print("Gehe zu Knoten {NodeNumber}!")
else:
Print("Kein vorheriger Knoten, wir sind wieder beim Kopfknoten!")
break
Zirkulär verknüpfte Listen
In einer zirkulär verknüpften Liste ist der Kopfknoten mit dem Endknoten verknüpft. Das bedeutet, dass du die gesamte Liste über den Endknoten hinaus durchlaufen kannst und wieder am Kopf ankommst. Zirkulär verknüpfte Listen können einzeln oder doppelt verknüpft sein. Die einzige Voraussetzung ist, dass der Kopfknoten mit dem Schwanzknoten verbunden ist oder umgekehrt.

Iteratoren
Ein Iterator ist ein Objekt, mit dem du über einen Container iterieren und dabei Operationen durchführen kannst. Du kannst dir das wie einen Zugwaggon voller Passagiere vorstellen. Der Zug fährt auf einem Gleis und hält an jeder Station auf dem Weg. An jedem Bahnhof führt der Zug eine Aufgabe aus, z. B. das Ein- oder Aussteigen von Fahrgästen. Bei verknüpften Listen ist jede Station ein Knoten in der Liste. Der Zug (Iterator) fährt die Liste bis zu jedem Knoten ab und kann dort, wo er anhält, Operationen durchführen.
Iteratoren sind für verknüpfte Listen nützlich, da sie es ermöglichen, Operationen an bestimmten Stellen durchzuführen, und nicht nur am Kopf- oder Endknoten.
linked_list_iterator := class:
List:linked_list
var CurrentNode:list_node
# Wenn es einen NextNode gibt, zu dem der Iterator reisen kann, reist er zu diesem Knoten.
Next()<transacts><decides>:list_node=
set CurrentNode = CurrentNode.Next?
Hier enthält der Iterator einen Verweis auf die Liste, die er durchläuft, und einen CurrentNode
, an dem der Iterator gerade angehalten wird. Die Funktion Next()
prüft, ob der CurrentNode
einen nachfolgenden Knoten hat, und springt dann zu diesem. Beachte, dass Next()
hier die Bezeichner <transacts>
und <decides>
umfasst. Die Funktion muss <transacts>
sein, da sie die CurrentNode
var
aktualisiert, und du willst <decides>
einschließen, da du durch die Fehlbarkeit von Next()
weißt, ob du zum nächsten Knoten iteriert hast oder nicht.
Obwohl es den Anschein hat, dass ein Iterator am Kopf einer Liste beginnen sollte, muss er tatsächlich vor dem Kopf beginnen. Dadurch kann der Iterator Operationen am Kopfknoten durchführen, wie z. B. das Einfügen von Knoten vor dem Kopf oder das vollständige Löschen des Kopfes.
Um den Iterator vor dem Kopf zu initialisieren, kannst du CurrentNode
auf einen Dummy-Knoten mit dem Kopf der Liste als nächsten Knoten initialisieren. In Verse kann man nicht auf die List
zugreifen, wenn man CurrentNode
initialisiert, da auf Klassenvariablen nicht aus demselben Bereich zugegriffen werden kann, sondern nur aus verschiedenen Bereichen, z. B. innerhalb einer Funktion.
linked_list_iterator := class:
List:linked_list
# Die folgende Zeile wird fehlschlagen, weil man von diesem Bereich aus nicht auf List zugreifen kann.
var CurrentNode:list_node := list_node{Next := option{List.Head}}
Um einen Iterator zu instanziieren, kannst du stattdessen eine Constructor-Funktion verwenden. Ein Construktor ist ein zusammengesetzter Typ, der eine Instanz der Klasse erzeugt, mit der er verbunden ist. Der Construktor legt die Anfangswerte für ein Objekt fest und ermöglicht den Zugriff auf den Wert von „List“ und die Zuweisung von CurrentNode
an seinen Kopf.
# Konstruiere einen Iterator mit CurrentNode als Dummy-Knoten vor dem Kopf der angegebenen Liste.
MakeIterator<constructor>(List:linked_list) := linked_list_iterator:
List := List
# Du kannst von diesem Bereich aus in einer Constructorfunktion auf „List“ zugreifen.
CurrentNode:= list_node{Next := option{List.Head}}
linked_list_iterator := class:
List:linked_list
var CurrentNode:list_node
Die Initialisierung eines Iterators für eine doppelt verknüpfte Liste sieht sehr ähnlich aus, nur dass du ihn mit doppelt verknüpften Knoten initialisierst.
# Konstruiere einen Iterator mit CurrentNode als Dummy-Knoten vor dem Kopf der angegebenen Liste.
MakeDoublyLinkedIterator<constructor>(List:doubly_linked_list) := doubly_linked_list_iterator:
List := List
# Du kannst von diesem Bereich aus in einer Constructorfunktion auf „List“ zugreifen.
CurrentNode:= doubly_linked_node{Next := option{List.Head}}
doubly_linked_list_iterator := class:
List:doubly_linked_list
var CurrentNode:doubly_linked_node
Iterieren zwischen Knoten
Du kannst den Iterator je nach Verknüpfung deiner Liste vorwärts oder rückwärts bewegen. Iteratoren in einer einfach verknüpften Liste können nur vorwärts laufen, während Iteratoren in einer doppelt verknüpften Liste in beide Richtungen laufen können. Diese Funktionalität ist in zwei Funktionen definiert, Next()
und Previous()
. Denke daran, dass einfach verknüpfte Listen keine Previous()
-Funktion haben. Zirkulär verknüpfte Listen können beide Funktionen haben, je nachdem, ob sie einfach oder doppelt verknüpft sind.
# Wenn es einen NextNode gibt, zu dem der Iterator reisen kann, reist er zu diesem Knoten.
Next()<transacts><decides>:doubly_linked_node=
set CurrentNode = CurrentNode.Next?
# Wenn es einen PreviousNode gibt, zu dem der Iterator reisen kann, reist er zu diesem Knoten.
Previous()<transacts><decides>:doubly_linked_node=
set CurrentNode = CurrentNode.Previous?
Hinzufügen von Knoten
Du kannst Iteratoren verwenden, um Knoten an bestimmten Stellen in der verknüpften Liste hinzuzufügen. Beim Hinzufügen eines Knotens ist es wichtig, den Ort zu wählen, an dem der Knoten hinzugefügt werden soll. Du könntest den Knoten vor oder nach dem aktuellen Knoten hinzufügen, aber das hängt von der Struktur deiner Liste ab. In einer einfach verknüpften Liste kannst du nur Knoten nach anderen Knoten hinzufügen, da du keine Referenz auf einen previous
(vorhergehenden) Knoten hast, auf den du dich beziehen könntest. In einer doppelt verknüpften Liste kannst du Knoten vor oder nach anderen Knoten hinzufügen.
# Fügt einen neuen Knoten nach CurrentNode ein.
AddNextNode(NewNode:list_node):void=
if(NextNode := CurrentNode.Next?):
set NewNode.Next = option{NextNode}
set CurrentNode.Next = option{NewNode}
Print("Neuer Knoten erfolgreich hinzugefügt!")
else:
set CurrentNode.Next = option{NewNode}
Print("Neuer Knoten erfolgreich am Ende der Liste hinzugefügt!")
Hier fügt die Funktion AddNode
einen Knoten zwischen CurrentNode
und dem nachfolgenden Knoten ein. Dies erfolgt durch:
-
Setzen der
Next
-Referenz desCurrentNode
aufNewNode
. -
Setzen der
Next
-Referenz vonNewNode
auf denNextNode
. Wenn es keinenNextNode
gibt (z.B. am Ende einer Liste), setzt die Funktion nurNewNode
als nächsten Knoten.
In a doubly linked list, you have the option of adding nodes before or after CurrentNode
. You can split this functionality into two functions, AddNodeAfter()
and AddNodeBefore()
. The implementation is very similar to the one in the singly linked list, except that you have to remember to set the Previous
reference for each node as well.
# Fügt einen neuen Knoten nach CurrentNode ein.
AddNodeAfter(NewNode:doubly_linked_node):void=
if(NextNode := CurrentNode.Next?):
set NewNode.Next = option{NextNode}
set NewNode.Previous = option{CurrentNode}
set CurrentNode.Next = option{NewNode}
set NextNode.Previous = option{NewNode}
Print("Neuer Knoten erfolgreich hinzugefügt!")
else:
set CurrentNode.Next = option{NewNode}
Print("Neuer Knoten erfolgreich am Ende der Liste hinzugefügt!")
# Fügt einen neuen Knoten vor CurrentNode ein.
AddNodeBefore(NewNode:doubly_linked_node):void=
if(PreviousNode := CurrentNode.Previous?):
set NewNode.Next = option{CurrentNode}
set NewNode.Previous = option{PreviousNode}
set CurrentNode.Previous = option{NewNode}
set PreviousNode.Next = option{NewNode}
Print("Neuer Knoten erfolgreich hinzugefügt!")
else:
set NewNode.Next = option{CurrentNode}
set CurrentNode.Previous = option{NewNode}
Print("Neuer Knoten erfolgreich am Ende der Liste hinzugefügt!")
Entfernen von Knoten
Um Knoten zu entfernen, musst du alle Verweise auf diesen Knoten löschen, wobei die Reihenfolge der Liste erhalten bleibt. Dazu musst du alle Knoten vor dem entfernten Knoten mit den Nachfolgeknoten verbinden, nenn der zu entfernende Knoten Nachfolgeknoten hat. In einer einfach verknüpften Liste kannst du nur Knoten nach dem CurrentNode
entfernen, da du keine Referenz auf einen Previous
-Knoten hast.
# Entferne den nächsten Knoten nach CurrentNode. Wenn der nächste Knoten
# eine Next-Referenz hat, setze diesen Knoten auf den nächsten Knoten von CurrentNode
RemoveNextNode():void=
if(RemovedNode := CurrentNode.Next?):
if(NodeAfter := RemovedNode.Next?, set RemovedNode.Next = false):
set CurrentNode.Next = option{NodeAfter}
Print("Der Knoten zwischen CurrentNode und dem nachfolgenden Knoten wurde entfernt")
# Wenn der entfernte Knoten der Kopfknoten ist, setze den nächsten Knoten als neuen Kopf.
if(RemovedNode = List.Head):
Print("Der entfernte Knoten war der Kopfknoten, der nachfolgende Knoten ist der neue Kopfknoten")
set List.Head = NodeAfter
else:
set CurrentNode.Next = false
Print("Der Knoten vor CurrentNode wurde entfernt")
else:
Print("Der Knoten konnte nicht entfernt werden")
Beachte, dass du zum Entfernen vom Kopfknoten einen Sonderfall benötigst. In diesem Fall musst du den Knoten nach dem Kopfknoten auf den neuen Kopf setzen, da deine verknüpfte Liste sonst keinen Ausgangspunkt hat.
Wie beim Hinzufügen von Knoten kann das Entfernen von Knoten in einer doppelt verknüpften Liste in das Entfernen des Knotens vor oder nach dem aktuellen Knoten aufgeteilt werden.
# Entferne den Knoten nach CurrentNode
RemoveNodeAfter():void=
if(RemovedNode := CurrentNode.Next?):
if:
NodeAfter := RemovedNode.Next?
set RemovedNode.Next = false
set RemovedNode.Previous = false
set NodeAfter.Previous = option{CurrentNode}
set CurrentNode.Next = option{NodeAfter}
then:
Print("Der Knoten zwischen CurrentNode und dem nachfolgenden Knoten wurde entfernt")
# Wenn der entfernte Knoten der Kopfknoten ist, setze den nächsten Knoten als neuen Kopf.
if(RemovedNode = List.Head):
Print("Der entfernte Knoten war der Kopfknoten, der nachfolgende Knoten ist der neue Kopfknoten")
set List.Head = NodeAfter
# Du möchtest nicht zum Dummy-Knoten zurückkehren
# also setze den Previous des neuen Kopfes auf False
set NodeAfter.Previous = false
else:
set CurrentNode.Next = false
Print("Der Knoten nach CurrentNode wurde entfernt")
else:
Print("Der Knoten konnte nicht entfernt werden")
# Entferne den Knoten vor CurrentNode.
RemoveNodeBefore():void=
if(RemovedNode := CurrentNode.Previous?):
if:
NodeBefore := RemovedNode.Previous?
set RemovedNode.Previous = false
set CurrentNode.Previous = option{NodeBefore}
set NodeBefore.Next = option{CurrentNode}
then:
Print("Der Knoten zwischen CurrentNode und dem Knoten davor wurde entfernt")
# Wenn der entfernte Knoten der Kopfknoten ist, setze den nächsten Knoten als neuen Kopf.
else:
set CurrentNode.Previous = false
if(RemovedNode = List.Head):
Print("Der entfernte Knoten war der Kopfknoten, der nachfolgende Knoten ist der neue Kopfknoten")
set List.Head = CurrentNode
Print("Der Knoten nach CurrentNode wurde entfernt")
else:
Print("Der Knoten konnte nicht entfernt werden")
Auf eigene Faust
Es gibt mehrere Möglichkeiten, verknüpfte Listen zu verbessern, und du solltest verschiedene Möglichkeiten zur Hinzufügung von Funktionen untersuchen. Im Folgenden findest du einige Möglichkeiten, wie du deine verknüpften Listen verbessern kannst:
-
Was wäre, wenn jede Liste auch einen Verweis auf den Endknoten speichern würde?
-
Kannst du eine
ToString()
-Methode implementieren, um den Inhalt deiner Knoten auszugeben? -
Kannst du eine Liste umkehren? Schaffst du es, eine
Length()
-Funktion zu implementieren? -
Kannst du dem Iterator weitere Methoden hinzufügen, um neue Operationen durchzuführen?