In einer doppelt verketteten Liste enthält jedes Listenelement jeweils einen Verweis zum Vorgänger und zum Nachfolger in der Liste. Durch diese Verweise reihen sich die Listenelemente zu einer Liste.
Bei einer einfach verketteten Liste enthält jedes Listenelement nur einen Verweis zum Nachfolger in der Liste. Hierbei gestaltet sich jedoch das Einfügen und Entfernen von Listenelementen als schwieriger. Daher werden in der Praxis meist doppelt verkettete Listen verwendet.
Die Listenelemente, hier bezeichnet als Knoten, enthalten folgende Informationen:
ein Datenelement data,
einen Verweis pred auf den vorhergehenden Knoten in der Liste (predecessor - Vorgänger),
einen Verweis succ auf den nachfolgenden Knoten in der Liste (successor - Nachfolger),.
public class LinkedListNode<Type>
{
Type data;
LinkedListNode<Type> pred;
LinkedListNode<Type> succ;
}
Der Datentyp des Attributs data ist hier zunächst als Typ-Parameter Type angegeben. Wenn du später eine Liste erzeugst, gibst du den tatsächlichen Datentyp der Listenelemente an, also zum Beispiel Integer oder String.
Liste implementieren: Version 1
Im Prinzip sieht eine Liste mit drei Knoten folgendermaßen aus:
Der erste Knoten der Liste ist mit init bezeichnet. Er dient als Einstieg in die Liste. Am Anfang der Liste kannst du Knoten besonders einfach einfügen oder entfernen. In der Mitte der Liste dagegen ist dies insofern schwieriger, als dass du dich erst entlang der Nachfolger-Verweise vom Anfang bis zur Mitte vorarbeiten musst. Damit du auch am Ende der Liste mit wenig Aufwand Knoten einfügen und entfernen kannst, bezeichnest du den letzten Knoten der Liste mit last.
Im Prinzip kannst du eine doppelt verkettete Liste so implementieren. Die Implementierung ist aber wenig elegant, weil für das Einfügen und Entfernen von Knoten mehrere Fallunterscheidungen erforderlich sind, je nachdem, ob dies am Anfang, am Ende oder irgendwo in der Mitte der Liste geschieht.
Das Hauptproblem aber ist: Wie implementierst du eine leere Liste? Eine Liste kann leer sein, also kein Listenelement enthalten.
Stell dir vor, du führst eine Liste von Bestellungen. Wenn eine Bestellung hereinkommt, wird sie in die Liste eingefügt. Wenn eine Bestellung ausgeführt ist, wird sie aus der Liste entfernt. Wenn alle Bestellungen ausgeführt sind, ist die Liste leer – sie soll aber weiter existieren, für den Fall, dass wieder eine Bestellung hereinkommt.
Liste implementieren: Version 2
Du implementierst eine doppelt verkettete Liste in sehr eleganter Weise, wenn du die Knoten init und last quasi als Pseudo-Knoten realisierst: Knoten, die kein Datenelement enthalten, sondern nur dazu da sind, den Anfang und das Ende der Liste zu markieren.
Die leere Liste sieht dann so aus:
Wenn du eine Liste neu erzeugst, ist sie zunächst leer, sie enthält noch keinen Eintrag, sondern besteht nur aus den Pseudo-Knoten init und last.
Das Einfügen und Entfernen von Knoten kommt ohne Fallunterscheidungen aus. Schau dir die folgende Implementierung einmal an. Was beim Einfügen eines Knotens passiert, ist im Anschluss daran noch einmal gezeigt.
// doppelt verkettete Liste mit Pseudo-Knoten init und last
public class LinkedList<Type>
{
LinkedListNode<Type> init; // Pseudo-Knoten
LinkedListNode<Type> last; // Pseudo-Knoten
public LinkedList()
{
init=new LinkedListNode<Type>();
last=new LinkedListNode<Type>();
init.succ=last;
last.pred=init;
}
// Knoten am Ende hinzufügen, Verweise entsprechend ändern
public void add(Type v)
{
LinkedListNode<Type> node=new LinkedListNode<Type>();
node.data=v;
node.succ=last;
last.pred.succ=node;
node.pred=last.pred;
last.pred=node;
}
// Knoten entfernen, Verweise entsprechend ändern
public Type remove(LinkedListNode<Type> node)
{
checkEmpty();
node.pred.succ=node.succ;
node.succ.pred=node.pred;
return node.data;
}
public Type removeFirst()
{
return remove(init.succ);
}
public Type removeLast()
{
return remove(last.pred);
}
public boolean isEmpty()
{
return init.succ==last;
}
private void checkEmpty()
{
if (isEmpty())
throw new RuntimeException("Liste ist leer");
}
}
Knoten in die Liste einfügen
Mit der Methode add fügst du einen neuen Knoten am Ende der Liste ein. Hierzu änderst du die Verweise zwischen den Knoten last und last.pred so, dass sich der neue Knoten node dazwischen einreiht.
Die Liste durchlaufen
Du durchläufst die Liste von vorne bis hinten mithilfe eines Iterators. Der Iterator enthält einen Verweis auf das aktuelle Objekt, bezeichnet als Cursor-Objekt. Mit der Funktion next gibt er das aktuelle Objekt zurück und schaltet zum nächsten Objekt weiter. Die Funktion hasNext gibt true zurück, solange noch weitere Objekte kommen, ansonsten gibt sie false zurück.
// Standard-Iterator, der eine LinkedList durchläuft
public class LinkedListIterator<Type> implements Iterator<Type>
{
private LinkedListNode<Type> x, last;
public LinkedListIterator(LinkedList<Type> list)
{
x=list.init.succ;
last=list.last;
}
public boolean hasNext()
{
return x!=last;
}
public Type next()
{
Type v=x.data;
x=x.succ;
return v;
}
}
Iterator nutzen
Um den Iterator nutzen zu können, änderst du noch zwei Dinge an der Klasse LinkedList:
Du ergänzt die Kopfzeile der Klasse:
public class LinkedList<Type> implements Iterable<Type>
Und du fügst die Methode iterator ein:
// gibt den Standard-Iterator zurück
public Iterator<Type> iterator()
{
return new LinkedListIterator<Type>(this);
}
Dann kannst du im Hauptprogramm mit einer For-Each-Schleife die Liste durchlaufen. Am besten fügst du in die Klasse LinkedList die folgende Main-Methode ein, um einen kurzen Test durchzuführen:
// Test
public static void main(String[] args)
{
LinkedList<Integer> list=new LinkedList<Integer>();
list.add(3);
list.add(4);
list.add(5);
for (Integer v : list)
System.out.println(v);
list.removeFirst();
list.removeLast();
System.out.println();
for (Integer v : list)
System.out.println(v);
}
Liste implementieren Version 3: Zirkuläre Liste
Besonders elegant implementierst du eine doppelt verkettete Liste, indem du sie zu einem Kreis zusammen biegst. Dann übernimmt der Knoten init gleichzeitig die Rolle des Knotens last. Der Knoten last wird nicht mehr gebraucht.
Die leere Liste als zirkuläre Liste
In dieser Implementierung besteht die leere Liste nur aus dem Pseudo-Knoten init. Dieser verweist mit den Verweisen predecessor und successor auf sich selbst.
Im Konstruktor der Liste erzeugst du den Knoten init mit den entsprechenden Verweisen.
// Konstruktor
public LinkedList()
{
init=new LinkedListNode<Type>();
init.succ=init;
init.pred=init;
}
Knoten einfügen
In ähnlicher Weise wie in der Implementierung Version 2 fügst du mit der Methode add einen neuen Knoten am Ende der Liste ein.
Die Methode add funktioniert auch zu Beginn, wenn die Liste noch leer ist und nur aus dem Knoten init besteht.
Zum Üben
Nun bist du am Zug:
Ändere die Implementierung von Version 2 entsprechend ab, dass eine zirkuläre Liste entsprechend Version 3 entsteht.
Ändere den Iterator entsprechend ab, dass er die zirkuläre Liste durchläuft.
Verwende die ungeänderte Main-Funktion, um deine Implementierung zu testen.
Wenn alles funktioniert und du noch Lust dazu hast, leite die Klassen Queue und Stack mit entsprechenden Änderungen von deiner neuen Klasse LinkedList ab.