ALP II Dynamische Datenmengen Datenabstraktion (Teil 2) O1 O2 O3 O4 SS 2012 Prof. Dr. Margarita Esponda 49
Einfach verkettete Listen O1 O2 O3 50
Einführung Einfach verkettete Listen sind die einfachsten dynamischen Datenstrukturen, die zur Laufzeit an den tatsächlichen Speicherbedarf anpassen können. Eine Liste besteht aus einer Menge von Knoten, die untereinander verkettet sind. Jeder Knoten besteht aus einer Referenz auf das eigentliche zu speichernde Objekt und eine Referenz auf das nächste Element der Liste. Das letzte Element der Liste zeigt auf die Konstante null O1 O2 O3 51
Implementierung O1 O2 O3 null Wir haben eine rekursive Klassendefinition, class ListNode <T> { T element; ListNode <T> next;... weil das next-element zu der gleichen Objekt- Klasse gehört, die wir gerade definieren. 52
Implementierung O1 O2 O3 null class ListNode <T> { T element; ListNode<T> next; // Konstruktoren ListNode( T element, ListNode<T> next ){ this.element = element; this.next = next; ListNode() { this( null, null ); Zwei Konstruktoren: Einer, der nur einen leeren Knoten erzeugt und einen, der gleichzeitig ein Objekt in dem Knoten speichert und die Referenz auf den nächsten Knoten bekommt. 53
Stapel als verkettete Liste Mit Hilfe von verketteten Listen lässt sich sehr einfach ein Stapel implementieren. Wir müssen dabei nicht mehr überprüfen, ob der Stapel voll ist. Wir brauchen ein -Element, das eine Referenz auf ein ListNode- Objekt ist. Mit dieser Referenz können wir bei einer push-operation neue Elemente am Anfang der Liste verketten oder entfernen, wenn eine pop- Operation stattfindet. O1 O2 O3 null 54
Stapel-Schnittstelle In dieser Implementierung werden wir eine EmptyStackException erzeugen bei dem Versuch, ein Element zu entfernen oder zu lesen (pop- und top- Operationen), wenn die Liste leer ist. public interface Stack <T>{ public void push( T element ); public T pop() throws EmptyStackException; public T top() throws EmptyStackException; public boolean empty(); 55
Einfache Implementierung der Stapel-Schnittstelle Ein Konstruktor wird definiert, der mit der Konstante null initialisiert. public class ListStapel<T> implements Stack <T> {... /* Instanzvariablen */ private ListNode<T> ; /* Konstruktor */ public ListStapel() { = null; /* Methoden */ 56
Implementierung der empty-operation null Der Stapel ist leer, wenn das -Element auf die Konstante null zeigt public boolean empty () { return == null; 57
Implementierung der push-operation O1 O2 O3 Das T-Objekt e soll am Anfang der Liste eingefügt werden public void push ( T element ) { ListNode<T> temp = new ListNode<T> (); temp.element = element; temp.next = ; = temp; 58
Implementierung der push-operation O1 O2 O3 temp public void push ( T element ) { ListNode<T> temp = new ListNode<T> (); temp.element = element; temp.next = ; = temp; 59
Implementierung der push-operation e O1 O2 O3 temp public void push ( T element ) { ListNode<T> temp = new ListNode<T> (); temp.element = element; temp.next = ; = temp; 60
Implementierung der push-operation e O1 O2 O3 temp public void push ( T element ) { ListNode<T> temp = new ListNode<T> (); temp.element = element; temp.next = ; = temp; 61
Implementierung der push-operation e O1 O2 O3 temp public void push ( T element ) { ListNode<T> temp = new ListNode<T> (); temp.element = element; temp.next = ; = temp; 62
Implementierung der push-operation e O1 O2 O3 Mit Hilfe eines ListNode-Konstruktors, der als erster Parameter das zu speichernde Objekt element bekommt, und als zweiter Parameter eine Referenz auf ein ListNode-Objekt bekommt, können wir sehr einfach unsere push-operation wie folgt implementieren. public void push ( T element ) { = new ListNode<T> ( element, ); 63
Implementierung der pop-operation null Wenn innerhalb einer pop-operation festgestellt wird, dass der Stapel leer ist, wird ein EmptyStackException- Objekt geworfen und die pop-operation unterbrochen. public T pop() throws EmptyStackException { if ( empty() ) throw new EmptyStackException(); T element =.element; =.next; return element; 64
Implementierung der pop-operation Wenn der Stapel nicht leer ist element e O1 O2 O3 public T pop() throws EmptyStackException { if ( empty() ) throw new EmptyStackException(); T element =.element; =.next; return element; 65
Implementierung der pop-operation Wenn der Stapel nicht leer ist element e e O1 O2 O3 public T pop() throws EmptyStackException { Die Objekt-Referenz, die sich in dem ersten Knoten befindet, wird in die lokale Variable element gespeichert. if ( empty() ) throw new EmptyStackException(); T element =.element; =.next; return element; 66
Implementierung der pop-operation element e e O1 O2 O3 public T pop() throws EmptyStackException { if ( empty() ) throw new EmptyStackException(); T element =.element; =.next; return element; 67
Implementierung der pop-operation element garbage O1 O2 O3 e return public T pop() throws EmptyStackException { if ( empty() ) throw new EmptyStackException(); T element =.element; =.next; return element; 68
Implementierung der pop-operation garbage O1 O2 O3 Das entfernte ListNode-Objekt bleibt ohne eine einzige Referenz, das auf es zeigt, und verwandelt sich in Datenspeichermüll, der später von dem Java- garbage collector beseitigt wird. 69
Einfache Implementierung einer Warteschlange dequeue FIFO - Datenstruktur tail O1 O2 O3 enqueue "First In - First Out" Wenn wir eine Warteschlange mit Hilfe einer verketteten Liste implementieren, brauchen wir nicht mehr zu überprüfen, ob die Warteschlange voll ist. Operationen der Warteschlange enqueue dequeue empty 70
Warteschlange-Schnittstelle In dieser Implementierung werden wir eine EmptyQueueException erzeugen bei dem Versuch, ein Element zu entfernen oder zu lesen (dequeue-operation und -Operationen), wenn die Liste leer ist. public interface Queue<T> { public void enqueue( T newelement ) ; public T dequeue() throws EmptyQueueException; public T () throws EmptyQueueException; public boolean empty(); 71
Warteschlange-Implementierung public class ListQueue<T> implements Queue<T> { ListNode<T> ; ListNode<T> tail; public ListQueue() { this. = null; this.tail = null;... 72
Implementierung der empty-operation tail null Die Warteschlange ist leer, wenn das -Element auf die Konstante null zeigt public boolean empty () { return == null; 73
enqueue-operation tail null Wenn die Liste leer ist public void enqueue ( T newelement ) { if ( empty() ) = tail = new ListNode<T> ( newelement ); else tail = tail.next = new ListNode<T>( newelement ); 74
O1 tail enqueue-operation Das zu speichernde Element muss zuerst in ein ListNode-Objekt verpackt werden, und und tail bekommen eine Referenz aus den neuen Knoten zugewiesen. public void enqueue ( T newelement ) { if ( empty() ) = tail = new ListNode<T> ( newelement ); else tail = tail.next = new ListNode<T>( newelement ); 75
enqueue-operation tail O0 O1 O2 O3 Wenn die Liste nicht leer ist public void enqueue ( T newelement ) { if ( empty() ) = tail = new ListNode<T> ( newelement ); else tail = tail.next = new ListNode<T>( newelement ); 76
enqueue-operation tail O0 O1 O2 O3 newe Zuerst wird das zu speichernde neue Objekt in einen neu erzeugten Listenknoten (ListNode) verpackt. public void enqueue ( T newelement ) { if ( empty() ) = tail = new ListNode<T> ( newelement ); else tail = tail.next = new ListNode<T>( newelement ); 77
enqueue-operation tail O0 O1 O2 O3 newe Die Referenz, die von dem ListNode- Konstruktor erzeugt wird, bekommt tail.next zugewiesen. public void enqueue ( T newelement ) { if ( empty() ) = tail = new ListNode<T> ( newelement ); else tail = tail.next = new ListNode<T>( newelement ); 78
enqueue-operation tail O0 O1 O2 O3 newe Zum Schluss bekommt tail auch die gleiche Referenz wie tail.next. public void enqueue ( T newelement ) { if ( empty() ) = tail = new ListNode<T> ( newelement ); else tail = tail.next = new ListNode<T>( newelement ); 79
dequeue-operationen tail e O1 O2 O3 Wenn die Liste nicht leer ist public T dequeue() throws EmptyQueueException { if (empty () ) throw new EmptyQueueException(); T element =.element; =.next; return element; 80
dequeue-operationen tail element e e O1 O2 O3 Die Referenz des Elements, das am Anfang der Warteschlange gespeichert ist, wird der Variablen element zugewiesen. public T dequeue() throws EmptyQueueException { if (empty () ) throw new EmptyQueueException(); T element =.element; =.next; return element; 81
dequeue-operationen tail element e e O1 O2 O3 public T dequeue() throws EmptyQueueException { if (empty () ) throw new EmptyQueueException(); T element =.element; =.next; return element; 82
dequeue-operationen tail element e return e O1 O2 O3 public T dequeue() throws EmptyQueueException { if (empty () ) Die Referenz des entfernten Objekts wird als Ergebnis zurückgegeben. throw new EmptyQueueException(); T element =.element; =.next; return element; 83
dequeue-operationen tail e O1 O2 O3 Weil keine Referenz auf das entfernte ListNode-Objekt zeigt, bleibt es Speicherdatenmüll, bis es vom Java- garbage collector gelöscht wird. 84
Allgemeine dynamische Datenmenge Wenn wir allgemeine dynamische Datenmengen mit Hilfe von Listen implementieren möchten, müssen wir an einer beliebigen Stelle der Liste Elemente einfügen und löschen können. Dafür brauchen wir ein weiteres Referenz-Objekt (current), das sich durch die Liste bewegt. Wir werden eine Einfüge-Operation definieren, die ein Element nach dem current - Zeiger einfügt und eine Lösch-Operation, die ein Element nach dem current-zeiger löscht. insert delete current O1 O2 O4 O3 85
Einfügen an einer beliebigen Stelle der Liste current........ O1 O4 O3 temp... ListNode<T> temp = new ListNode<T>(); temp.element = newe; temp.next = current.next; current.next = temp;... 86
Einfügen an eine beliebige Stelle der Liste current........ O1 O4 O3 temp newe... ListNode<T> temp = new ListNode<T>(); temp.element = newe; temp.next = current.next; current.next = temp;... 87
Einfügen an eine beliebige Stelle der Liste current........ O1 O4 O3 temp newe... ListNode<T> temp = new ListNode<T>(); temp.element = newe; temp.next = current.next; current.next = temp;... 88
Einfügen an eine beliebige Stelle der Liste current........ O1 O4 O3 temp newe... ListNode<T> temp = new ListNode<T>(); temp.element = newe; temp.next = current.next; current.next = temp;... 89
Einfügen an eine beliebige Stelle der Liste current........ O1 O4 O3 temp newe current.next = new ListNode<T>( newe, current.next ); 90
Löschen an einer beliebigen Stelle der Liste current next next next........ O1 O4 O3 current.next = current.next.next; 91
Löschen an einer beliebigen Stelle der Liste current garbage next next next........ O1 O4 O3 current.next = current.next.next; Das entfernte ListNode-Objekt bleibt ohne eine einzige Referenz, die auf es zeigt, und verwandelt sich in Datenspeichermüll, der später von dem Java- garbage collector beseitigt wird. 92
"Header"-Knoten ("Dummy"-Knoten) Die Einfüge- und Löschoperationen, wie wir bis jetzt diskutiert haben, gehen davon aus, dass immer ein Vorgänger-Element vorhanden ist. Das macht unsere Implementierung einfacher, weil die speziellen Fälle - Löschen des ersten Elements der Liste- - Einfügen, wenn die Liste leer istnicht berücksichtigt werden müssen. Um unsere Implementierung übersichtlich und einfach zu halten, benutzen wir einen Dummy-Knoten (sentinel), der selbst keine Elemente speichert und nur benutzt wird, um diese speziellen Fälle zu vermeiden, weil auf diese Weise jeder Knoten der Liste immer einen Vorgänger haben wird. 93
"Dummy"-Knoten current Dummy O2 O4 O3 Unsere Liste ist jetzt leer, wenn sich nur der "Dummy"-Knoten in ihr befindet. public boolean empty () { Dummy return.next == null; 94
Einschränkungen bei einfach verketteten Listen Probleme: Der current-zeiger kann nur nach vorne bewegt werden. Um den Vorgänger desjenigen Knotens zu finden, auf den current zeigt, müssen wir die ganze Liste von an durchlaufen. Das ist im allgemeinen sehr ineffizient. current current current Dummy O2 O4 O3 95
Doppelt verkettete Listen tail Dummy O1 O2 Dummy class ListNode <T> { T element; ListNode <T> next; ListNode <T> prev;... Bei doppelt verketteten Listen kann sich current problemlos in beide Richtungen bewegen. Die Einfüge- und Lösch- Operation ändert sich, weil wir jetzt immer eine doppelte Verkettung erstellen müssen. 96
Doppelt verkettete leere Liste Dummy Dummy tail Bei doppelt verketteten Listen ist es sinnvoll, einen Zeiger auf das letzte Element zu haben. public boolean empty { return.next == tail; 97
Einfügen current prev O0 next prev next prev O1 O3 next...... ListNode<T> temp = new ListNode<T>( x ); temp.prev = current; temp.next = current.next; temp.prev.next = temp; temp.next.prev = temp; current = temp; 98
Einfügen current prev O0 next prev next prev O1 O3 next temp prev x next...... ListNode<T> temp = new ListNode<T>( x ); temp.prev = current; temp.next = current.next; temp.prev.next = temp; temp.next.prev = temp; current = temp; 99
Einfügen current prev O0 next prev next prev O1 O3 next temp prev x next...... ListNode<T> temp = new ListNode<T>( x ); temp.prev = current; temp.next = current.next; temp.prev.next = temp; temp.next.prev = temp; current = temp; 100
Einfügen current prev O0 next prev next prev O1 O3 next temp prev x next...... ListNode<T> temp = new ListNode<T>( x ); temp.prev = current; temp.next = current.next; temp.prev.next = temp; temp.next.prev = temp; current = temp; 101
Einfügen current prev O0 next prev next prev O1 O3 next temp prev x next...... ListNode<T> temp = new ListNode<T>( x ); temp.prev = current; temp.next = current.next; temp.prev.next = temp; temp.next.prev = temp; current = temp; 102
Einfügen current prev O0 next prev next prev O1 O3 next temp prev x next...... ListNode<T> temp = new ListNode<T>( x ); temp.prev = current; temp.next = current.next; temp.prev.next = temp; temp.next.prev = temp; current = temp; 103
Einfügen current prev O0 next prev next prev O1 O3 next temp prev x next...... ListNode<T> temp = new ListNode<T>( x ); temp.prev = current; temp.next = current.next; temp.prev.next = temp; temp.next.prev = temp; current = temp; 104
Löschen current prev next prev next prev x O1 O3 next Mit doppelt verketteten Listen können wir genau das Objekt löschen, auf das current zeigt.... current.prev.next = current.next;... current.next.prev = current.prev; current = ; 105
Löschen current prev next prev next prev x O1 O3 next... current.prev.next = current.next;... current.next.prev = current.prev; current = ; 106
Löschen current prev next prev next prev x O1 O3 next... current.prev.next = current.next;... current.next.prev = current.prev; current = ; 107
Löschen prev next prev next prev x O1 O3 next... current.prev.next = current.next;... current.next.prev = current.prev; current = ; 108
Löschen garbage prev next prev next prev x O1 O3 next... current.prev.next = current.next;... current.next.prev = current.prev; current = ; 109
Löschen garbage prev next prev next prev x O1 O3 next... current.prev.next = current.next;... current.next.prev = current.prev; current = ; 110