Wie lässt sich eine doppelt verkettete Liste implementieren?

Im Gegensatz zu Arrays, deren Elemente im Speicher als fortlaufende Reihe abgelegt werden und deren Größe aus diesem Grund ohne Neuinitialisierung unveränderbar ist, sind Listen Container, die flexible Mengen an Objekten enthalten können. Diesem nicht unerheblichen Vorteil steht der Nachteil des etwas zeitintensiveren Suchens nach einzelnen Elementen gegenüber, da die Liste zu diesem Zweck jedes Mal erneut durchlaufen werden muss. Listen werden aus diesem Grund hauptsächlich für Zwecke verwendet, bei denen es auf die Arbeit mit dem Anfang oder dem Ende der Liste ankommt.
Eine Liste besteht aus einzelnen Elementen, den Knoten. Bei einer doppelt verketteten Liste kennt jeder Knoten seinen Vorgänger und seinen Nachfolger, besitzt somit also zwei Referenzen auf Objekte des gleichen Typs. Das erste Element hat jedoch keinen Vorgänger, das letzte keinen Nachfolger.

Die Klasse ListElem repräsentiert im Beispiel die Knoten. Sie enthält insgesamt drei Felder: Zwei Instanzvariablen verweisen jeweils auf den nächsten und den vorhergehenden Knoten, Object obj zeigt auf den Inhalt des Knotens. Man bezeichnet solche Klassen als rekursiv, da sie Elemente des eigenen Typs enthalten. Ergänzt wird die Klasse nur noch durch die üblichen Getter- und Setter-Methoden.

class ListElement {

    Object obj;
    ListElement nextElem, prevElem;

    public ListElement(Object obj) {
        this.obj = obj;
        nextElem = null;
    }

    public void setNextElem(ListElement nextElem) {
        this.nextElem = nextElem;
    }
    
    public void setPrevElem(ListElement prevElem) {
        this.prevElem = prevElem;
    }

    public ListElement getNextElem() {
        return nextElem;
    }
    
    public ListElement getPrevElem() {
        return this.prevElem;
    }

    public Object getObj() {
        return obj;
    }
} 

Die Klasse DoppeltVerketteteListe stellt die eigentliche Listenimplementierung dar. 1 In ihr werden zunächst zwei Listenelemente, der 'Kopf' und der 'Schwanz' angelegt. Sie verweisen als Nachfolge- und Vorgängerelemente gegenseitig auf sich. Die Methode getFirstElem() liefert den Kopf der Liste, die Methode getLastElem() durchläuft die Liste und gibt das letzte Element zurück.
In addLast(Object o) wird das letzte Element über das Durchlaufen der Liste ermittelt und dies mit einem neuen Listenelement so verknüpft, dass dies als Nachfolger des ehemals letzten, nunmehr vorletzten Elementes dient.
Die Methode insertAfter(Object prevItem, Object newItem) fügt ein neues Listenelement nach einem vorgegebenen Element ein. Hierzu wird als erstes das erste Element hinter dem Kopf in der Variablen pointerElem abgelegt. Die Liste wird anschließend von vorne nach hinten so lange durchlaufen, bis der Einfügepunkt erreicht wird. Er wird über den Inhalt der Elemente ermittelt. Hier liegt ein Haken dieser Listenimplementierung: Der Inhalt eines Listenelementes muss in der Liste einmalig sein. Falls dies nicht der Fall ist, wird als Einfügepunkt das Element mit dem ersten Vorkommen des entsprechenden Inhaltes verwendet.
Ist der Einfügepunkt erreicht, wird ein neu gebildetes Listenelement mit dem Einfügepunkt als Vorgängerelement und dem dem Einfügepunkt nachfolgenden als Nachfolgeelement verbunden. Wichtig ist hierbei, dass die Verbindungen auch bei den bereits vorhandenen Elementen entsprechend erfolgen.

Ähnlich aufgebaut ist die Methode insertBefore(Object insertItem, Object newItem). Auch hier wird die Liste durchlaufen, der Listendurchlauf wird jedoch in dem Moment abgebrochen, an dem der Inhalt der gesuchten Elementes erreicht ist. Die Verknüpfungen finden dann so statt, dass das gesuchte Element als Nachfolgeelement des neuen fungiert. Ein Element wird durch die Methode delete(Object o) gelöscht. Hierzu werden die Verknüpfungen des Elementes mit dem Inhalt o gelöst und das Vorgänger- und Nachfolgerelement des zu löschenden neu miteinander verbunden. Hierbei muss darauf geachtet werden, dass das Nachfolgeelement des bisherigen Nachfolgeelementes nicht null ist. Ist dies der Fall, so handelt es sich um das letzte Element der Liste, das keinen Nachfolger besitzt.

public class DoppeltVerketteteListe {


    ListElement startElem = new ListElement("Kopf");
    ListElement tailElem = new ListElement("Schwanz");

    public DoppeltVerketteteListe() {
        startElem.setNextElem(tailElem);
        tailElem.setPrevElem(startElem);
    }

    public void addLast(Object o){
        ListElement newElem = new ListElement(o);
        ListElement lastElem = getLastElem();
        lastElem.setNextElem(newElem);
        newElem.setPrevElem(lastElem);
    }

    public void insertAfter(Object prevItem, Object newItem) {
        ListElement newElem, nextElem = null, pointerElem;
        pointerElem = startElem.getNextElem();
        while(pointerElem != null && !pointerElem.getObj().equals(prevItem)){
            pointerElem = pointerElem.getNextElem();
        }
        newElem = new ListElement(newItem);
        if(pointerElem != null){
            nextElem = pointerElem.getNextElem();
            pointerElem.setNextElem(newElem);
            newElem.setNextElem(nextElem);
            newElem.setPrevElem(pointerElem);
        }
        if(nextElem != null)
            nextElem.setPrevElem(newElem);
    }
    
    public void insertBefore(Object insertItem, Object newItem){
        ListElement newElem, pointerElem;
        newElem = new ListElement(newItem);
        pointerElem = startElem.getNextElem();
        while(pointerElem != null){
            if(pointerElem.getObj().equals(insertItem)){
                newElem.setPrevElem(pointerElem.getPrevElem());
                pointerElem.getPrevElem().setNextElem(newElem);
                pointerElem.setPrevElem(newElem);
                newElem.setNextElem(pointerElem);
                break;
            }
            pointerElem = pointerElem.getNextElem();
        }
    }

    public void delete(Object o){
        ListElement le = startElem;
        while (le.getNextElem() != null && !le.getObj().equals(o)){
            if(le.getNextElem().getObj().equals(o)){
                if(le.getNextElem().getNextElem()!=null){
                    le.setNextElem(le.getNextElem().getNextElem());
                    le.getNextElem().setPrevElem(le);
                }else{
                    le.setNextElem(null);
                    break;
                }
            }
            le = le.getNextElem();
        }
    }
    

    public boolean find(Object o){
        ListElement le = startElem;
        while (le != null){
            if(le.getObj().equals(o))
            return true;
            le = le.nextElem;
        }
        return false;
    }
    

    public ListElement getFirstElem() {
        return startElem;
    }

    public ListElement getLastElem() {
        ListElement le = startElem;
        while(le.getNextElem() != null){
            le = le.getNextElem();
        }
        return le;
    }

    public void writeList() {
        ListElement le = startElem;
        while(le != null){
            System.out.println(le.getObj());
            le = le.getNextElem();
        }
    }

    public static void main(String[] args) {
        DoppeltVerketteteListe list = new DoppeltVerketteteListe();
        list.addLast("1");
        list.addLast("2");
        list.addLast("3");
        list.addLast("4");
        list.addLast("5");
        list.insertAfter("2", "nach 2");
        list.delete("4");
        list.insertBefore("3", "vor 3");
        System.out.println("erstes Element: " + list.getFirstElem().getObj());
        System.out.println("ist '4' enthalten? " + list.find("4"));
        System.out.println("ist '5' enthalten? " + list.find("5"));
        System.out.println("letztes Element: " + list.getLastElem().getObj());
        System.out.println("vorletztes Element: " + list.getLastElem().getPrevElem().getObj());
        list.writeList();
    }
}

class ListElement {


    Object obj;

    ListElement nextElem, prevElem;

    public ListElement(Object obj) {
        this.obj = obj;
        nextElem = null;
    }

    public void setNextElem(ListElement nextElem) {
        this.nextElem = nextElem;
    }
    
    public void setPrevElem(ListElement prevElem) {
        this.prevElem = prevElem;
    }

    public ListElement getNextElem() {
        return nextElem;
    }
    
    public ListElement getPrevElem() {
        return this.prevElem;
    }

    public Object getObj() {
        return obj;
    }
}

1) Die Klasse LinkedList der Java-API stellt eine doppelt verkettete Liste bereit.