ALP II Dynamische Datenmengen Datenabstraktion (Teil 2)
O1
O2
O3
O4
SS 2012 Prof. Dr. Margarita Esponda
M. Esponda-Argüero
49
Einfach verkettete Listen head
O1
O2
O3
M. Esponda-Argüero
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.
head
Das letzte Element der Liste zeigt auf die Konstante null
O1
O2
O3
M. Esponda-Argüero
51
Implementierung head
O1
O2
O3
null
Wir haben eine rekursive Klassendefinition,
class ListNode { T element; ListNode next; ... }
weil das next-Element zu der gleichen ObjektKlasse gehört, die wir gerade definieren.
M. Esponda-Argüero
52
Implementierung
head
O1
O2
class ListNode { T element; ListNode next; // Konstruktoren ListNode( T element, ListNode next ){ this.element = element; this.next = next; } ListNode() { this( null, null ); }
O3
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.
} M. Esponda-Argüero
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 head-Element, das eine Referenz auf ein ListNodeObjekt ist. Mit dieser Referenz können wir bei einer push-Operation neue Elemente am Anfang der Liste verketten oder entfernen, wenn eine popOperation stattfindet.
head
O1
O2
O3
null
M. Esponda-Argüero
54
Stapel-Schnittstelle In dieser Implementierung werden wir eine EmptyStackException erzeugen bei dem Versuch, ein Element zu entfernen oder zu lesen (pop- und topOperationen), wenn die Liste leer ist. public interface Stack { public void push( T element ); public T pop() throws EmptyStackException; public T top() throws EmptyStackException; public boolean empty(); }
M. Esponda-Argüero
55
Einfache Implementierung der Stapel-Schnittstelle Ein Konstruktor wird definiert, der head mit der Konstante null initialisiert. public class ListStapel implements Stack { /* Instanzvariablen */ private ListNode head; /* Konstruktor */ public ListStapel() { head = null; } /* Methoden */ ... M. Esponda-Argüero
56
Implementierung der empty-Operation head null Der Stapel ist leer, wenn das head-Element auf die Konstante null zeigt
public boolean empty () { return head == null; }
M. Esponda-Argüero
57
Implementierung der push-Operation head
O1
O2
O3
Das T-Objekt e soll am Anfang der Liste eingefügt werden public void push ( T element ) { ListNode temp = new ListNode (); temp.element = element; temp.next = head; head = temp; } M. Esponda-Argüero
58
Implementierung der push-Operation head
O1
O2
O3
temp public void push ( T element ) { ListNode temp = new ListNode (); temp.element = element; temp.next = head; head = temp; } M. Esponda-Argüero
59
Implementierung der push-Operation head
e
O1
O2
O3
temp public void push ( T element ) { ListNode temp = new ListNode (); temp.element = element; temp.next = head; head = temp; } M. Esponda-Argüero
60
Implementierung der push-Operation head
e
O1
O2
O3
temp public void push ( T element ) { ListNode temp = new ListNode (); temp.element = element; temp.next = head; head = temp; } M. Esponda-Argüero
61
Implementierung der push-Operation head
e
O1
O2
O3
temp public void push ( T element ) { ListNode temp = new ListNode (); temp.element = element; temp.next = head; head = temp; } M. Esponda-Argüero
62
Implementierung der push-Operation head
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 ) { head = new ListNode ( element, head ); } M. Esponda-Argüero
63
Implementierung der pop-Operation head null
Wenn innerhalb einer pop-Operation festgestellt wird, dass der Stapel leer ist, wird ein EmptyStackExceptionObjekt geworfen und die pop-Operation unterbrochen.
public T pop() throws EmptyStackException { if ( empty() ) throw new EmptyStackException(); T element = head.element; head = head.next; return element; } M. Esponda-Argüero
64
Implementierung der pop-Operation
head
element
Wenn der Stapel nicht leer ist
O1
e
O2
O3
public T pop() throws EmptyStackException { if ( empty() ) throw new EmptyStackException(); T element = head.element; head = head.next; return element; } M. Esponda-Argüero
65
Implementierung der pop-Operation
head
element
Wenn der Stapel nicht leer ist
O1
e
e
O2
O3
public T pop() throws EmptyStackException { if ( empty() )
Die Objekt-Referenz, die sich in dem ersten Knoten befindet, wird in die lokale Variable element gespeichert.
throw new EmptyStackException(); T element = head.element; head = head.next; return element; } M. Esponda-Argüero
66
Implementierung der pop-Operation
head
element
e
O1
e
O2
O3
public T pop() throws EmptyStackException { if ( empty() ) throw new EmptyStackException(); T element = head.element; head = head.next; return element; } M. Esponda-Argüero
67
Implementierung der pop-Operation
head e
ga
element
e
return
g ba
O1
r
O2
O3
public T pop() throws EmptyStackException { if ( empty() ) throw new EmptyStackException(); T element = head.element; head = head.next; return element; } M. Esponda-Argüero
68
Implementierung der pop-Operation
head e
ga
r
g ba
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.
M. Esponda-Argüero
69
Einfache Implementierung einer Warteschlange dequeue
FIFO - Datenstruktur "First In - First Out"
head
O1
Wenn wir eine Warteschlange mit Hilfe einer verketteten Liste implementieren, brauchen wir nicht mehr zu überprüfen, ob die Warteschlange voll ist.
O2
Operationen der Warteschlange enqueue tail
dequeue
O3
empty enqueue
head M. Esponda-Argüero
70
Warteschlange-Schnittstelle In dieser Implementierung werden wir eine EmptyQueueException erzeugen bei dem Versuch, ein Element zu entfernen oder zu lesen (dequeue-Operation und head-Operationen), wenn die Liste leer ist. public interface Queue { public void enqueue( T newElement ) ; public T dequeue() throws EmptyQueueException; public T head() throws EmptyQueueException; public boolean empty(); }
M. Esponda-Argüero
71
Warteschlange-Implementierung public class ListQueue implements Queue { ListNode head; ListNode tail; public ListQueue() { this.head = null; this.tail = null; ... } M. Esponda-Argüero
72
Implementierung der empty-Operation tail
head
null Die Warteschlange ist leer, wenn das head-Element auf die Konstante null zeigt public boolean empty () { return head == null; }
M. Esponda-Argüero
73
enqueue-Operation
head
tail null
Wenn die Liste leer ist
public void enqueue ( T newElement ) { if ( empty() ) head = tail = new ListNode ( newElement ); else tail = tail.next = new ListNode( newElement ); }
M. Esponda-Argüero
74
enqueue-Operation
head
Das zu speichernde Element muss zuerst
tail
in ein ListNode-Objekt verpackt werden, und head und tail bekommen eine Referenz aus den neuen Knoten
O1
zugewiesen. public void enqueue ( T newElement ) { if ( empty() ) head = tail = new ListNode ( newElement ); else tail = tail.next = new ListNode( newElement ); }
M. Esponda-Argüero
75
enqueue-Operation
head
tail
O0
O1
O2
O3
public void enqueue ( T newElement ) { if ( empty() ) Wenn die Liste nicht leer ist
head = tail = new ListNode ( newElement ); else tail = tail.next = new ListNode( newElement ); }
M. Esponda-Argüero
76
enqueue-Operation
head
tail
O0
Zuerst wird das zu speichernde neue Objekt in einen neu erzeugten Listenknoten (ListNode) verpackt.
O1
O2
O3
newE
public void enqueue ( T newElement ) { if ( empty() ) head = tail = new ListNode ( newElement ); else tail = tail.next = new ListNode( newElement ); }
M. Esponda-Argüero
77
enqueue-Operation
head
tail
O0
Die Referenz, die von dem ListNodeKonstruktor erzeugt wird, bekommt tail.next zugewiesen.
O1
O2
O3
newE
public void enqueue ( T newElement ) { if ( empty() ) head = tail = new ListNode ( newElement ); else tail = tail.next = new ListNode( newElement ); }
M. Esponda-Argüero
78
enqueue-Operation head
tail
O0
O1
O2
O3
newE
public void enqueue ( T newElement ) { if ( empty() )
Zum Schluss bekommt tail auch die gleiche Referenz wie tail.next.
head = tail = new ListNode ( newElement ); else tail = tail.next = new ListNode( newElement ); }
M. Esponda-Argüero
79
dequeue-Operationen
head
e Wenn die Liste nicht leer ist
tail
O1
O2
O3
public T dequeue() throws EmptyQueueException { if (empty () ) throw new EmptyQueueException(); T element = head.element; head = head.next; return element; } M. Esponda-Argüero
80
dequeue-Operationen
head
tail
element
e Die Referenz des Elements, das am Anfang der Warteschlange gespeichert ist, wird der Variablen element zugewiesen.
e
O1
O2
O3
public T dequeue() throws EmptyQueueException { if (empty () ) throw new EmptyQueueException(); T element = head.element; head = head.next; return element; } M. Esponda-Argüero
81
dequeue-Operationen
head
tail
element
e
e
O1
O2
O3
public T dequeue() throws EmptyQueueException { if (empty () ) throw new EmptyQueueException(); T element = head.element; head = head.next; return element; } M. Esponda-Argüero
82
dequeue-Operationen
head
tail
element
e
return
e
O1
O2
O3
public T dequeue() throws EmptyQueueException { if (empty () ) throw new EmptyQueueException(); Die Referenz des entfernten Objekts wird als Ergebnis zurückgegeben.
T element = head.element; head = head.next; return element; } M. Esponda-Argüero
83
dequeue-Operationen
head
e
tail
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.
M. Esponda-Argüero
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 head
current
delete
O1
O2
O4
O3
M. Esponda-Argüero
85
Einfügen an einer beliebigen Stelle der Liste
current ....
O1
O4
....
O3
temp ... ListNode temp = new ListNode(); temp.element = newE; temp.next = current.next; current.next = temp; ... M. Esponda-Argüero
86
Einfügen an eine beliebige Stelle der Liste
current ....
O1
O4
temp
....
O3
newE
... ListNode temp = new ListNode(); temp.element = newE; temp.next = current.next; current.next = temp; ... M. Esponda-Argüero
87
Einfügen an eine beliebige Stelle der Liste
current ....
O1
O4
temp
....
O3
newE
... ListNode temp = new ListNode(); temp.element = newE; temp.next = current.next; current.next = temp; ... M. Esponda-Argüero
88
Einfügen an eine beliebige Stelle der Liste
current ....
O1
O4
temp
....
O3
newE
... ListNode temp = new ListNode(); temp.element = newE; temp.next = current.next; current.next = temp; ... M. Esponda-Argüero
89
Einfügen an eine beliebige Stelle der Liste
current ....
O1
O4
temp
O3
....
newE
… current.next = new ListNode( newE, current.next ); … M. Esponda-Argüero
90
Löschen an einer beliebigen Stelle der Liste
current
....
next O1
next O4
next O3
....
… current.next = current.next.next; …
M. Esponda-Argüero
91
Löschen an einer beliebigen Stelle der Liste
current
....
garbage
next O1
next O4
next 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. M. Esponda-Argüero
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. M. Esponda-Argüero
93
"Dummy"-Knoten
current head Dummy
O2
Unsere Liste ist jetzt leer, wenn sich nur der "Dummy"-Knoten in ihr befindet.
public boolean empty () {
O4
O3
head Dummy
return head.next == null; } M. Esponda-Argüero
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 head an durchlaufen. • Das ist im allgemeinen sehr ineffizient.
current
current
current
head Dummy
O2
O4
O3
M. Esponda-Argüero
95
Doppelt verkettete Listen head
Dummy
tail
O1
class ListNode { T element; ListNode next; ListNode prev; ...
O2
Dummy
Bei doppelt verketteten Listen kann sich current problemlos in beide Richtungen bewegen. Die Einfüge- und LöschOperation ändert sich, weil wir jetzt immer eine doppelte Verkettung erstellen müssen.
} M. Esponda-Argüero
96
Doppelt verkettete leere Liste
head
tail
Dummy
Dummy
Bei doppelt verketteten Listen ist es sinnvoll, einen Zeiger auf das letzte Element zu haben.
public boolean empty { return head.next == tail; }
M. Esponda-Argüero
97
Einfügen
current
prev
O0 next
prev
O1 next
prev
O3 next
... ListNode temp = new ListNode( x ); temp.prev = current; temp.next = current.next; temp.prev.next = temp; temp.next.prev = temp; current = temp; ...
M. Esponda-Argüero
98
Einfügen
current
prev
O0 next
prev
prev
O1 next
prev
temp
x
O3 next
next
... ListNode temp = new ListNode( x ); temp.prev = current; temp.next = current.next; temp.prev.next = temp; temp.next.prev = temp; current = temp; ...
M. Esponda-Argüero
99
Einfügen
current
prev
O0 next
prev
prev
O1 next
prev
temp
x
O3 next
next
... ListNode temp = new ListNode( x ); temp.prev = current; temp.next = current.next; temp.prev.next = temp; temp.next.prev = temp; current = temp; ...
M. Esponda-Argüero
100
Einfügen
current
prev
O0 next
prev
prev
O1 next
prev
temp
x
O3 next
next
... ListNode temp = new ListNode( x ); temp.prev = current; temp.next = current.next; temp.prev.next = temp; temp.next.prev = temp; current = temp; ...
M. Esponda-Argüero
101
Einfügen
current
prev
O0 next
prev
prev
O1 next
prev
temp
x
O3 next
next
... ListNode temp = new ListNode( x ); temp.prev = current; temp.next = current.next; temp.prev.next = temp; temp.next.prev = temp; current = temp; ...
M. Esponda-Argüero
102
Einfügen
current
prev
O0 next
prev
prev
O1 next
prev
temp
x
O3 next
next
... ListNode temp = new ListNode( x ); temp.prev = current; temp.next = current.next; temp.prev.next = temp; temp.next.prev = temp; current = temp; ...
M. Esponda-Argüero
103
Einfügen
current
prev
O0 next
prev
prev
O1 next
prev
temp
x
O3 next
next
... ListNode temp = new ListNode( x ); temp.prev = current; temp.next = current.next; temp.prev.next = temp; temp.next.prev = temp; current = temp; ...
M. Esponda-Argüero
104
Löschen
current
prev
O1 next
prev
x
next
prev
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 = head; . . .
M. Esponda-Argüero
105
Löschen
current
prev
O1 next
prev
x
next
prev
O3 next
. . .
current.prev.next = current.next; current.next.prev = current.prev; current = head; . . .
M. Esponda-Argüero
106
Löschen
current
prev
O1 next
prev
x
next
prev
O3 next
. . .
current.prev.next = current.next; current.next.prev = current.prev; current = head; . . .
M. Esponda-Argüero
107
Löschen
prev
O1 next
prev
x
next
prev
O3 next
. . .
current.prev.next = current.next; current.next.prev = current.prev; current = head; . . .
M. Esponda-Argüero
108
Löschen garbage
prev
O1 next
prev
x
next
prev
O3 next
. . .
current.prev.next = current.next; current.next.prev = current.prev; current = head; . . .
M. Esponda-Argüero
109
Löschen garbage
prev
O1 next
prev
x
next
prev
O3 next
. . .
current.prev.next = current.next; current.next.prev = current.prev; current = head; . . .
M. Esponda-Argüero
110