Praktische Einführung und Programmierung
Stefan Bosse
Universität Koblenz - AG Praktische Informatik
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen ::
Im Gegensatz zu Tabellenstrukturen werden hier dynamische Strukturen eingeführt:
Basisoperationen auf Listen:
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Dynamische Strukturen
Die Probleme statischer Datenstrukturen lassen sich durch die Verwendung dynamischer Strukturen umgehen.
Ein typischer Vertreter einer dynamischen Datenstruktur ist die verkettete Liste.
Im Vergleich zu Tabellen sind Listen nicht für echtzeitfähige Systeme geeignet da ihre Laufzeiten nicht im voraus bestimmbar sind.
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Entwurf von Datentypen
Etwas systematischer als heuristisch: Wir haben bisher einige einfache Beispiele für abstrakte Datentypen kennen gelernt und deren Spezifikationen mit Gleichungen angegeben.
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Listen
Ein typischer Vertreter einer dynamischen Datenstruktur ist die verkettete Liste.
K=<data∣next→K>L={ki∈K,∀i=1..n∧ki∣next=ki+1∀i=1..n−1}
Listen sind keine Mengen! Listen sind geordnet bzw. haben eine feste Reihenfolge der Knoten, wonhingegen Mengen ungeordnet sind und jede Permutation der Reihenfolge von Knotenelementen möglich und zuässig ist.
Das letzte Element verweist demzufolge auf null. In der Liste selbst muss nur noch der erste Knoten direkt verankert sein, alle anderen Knoten sind durch das Navigieren über die Zeiger erreichbar. Der »Anker« für den Beginn der Liste wird mit head bezeichnet.
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Einfach verkettete Listen
addp Prinzip der einfach verketteten Liste. Dier Daten werden i.A. separat gespeichert und in der Elementstruktur gibt es nur einen Zeiger auf die Daten. Eine Integatrion der Daten (z.B. ganze Zahlen) ist aber auch möglich.
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Einfach verkettete Listen
Arrays sind in dynamische typisierten Programmiersprachen polymorph und mehrsortig, d.h. jedes Element kann einen Wert mit anderen Datentyp besitzen. Es bedarf daher einer getrennten Speicherung von Organisationsstruktur (Tabelle) und Daten.
Einfache (einelementige) Listen in mehrsortigen Arrays - oder Urform der Bereichslisten
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Einfach verkettete Listen
Bei Stack und Queue gab es ein Paar aus (verschiedenen) Operation um Elemente einzufügen und zu entfernen. Diese können auch bei verketteten Listen eingesetzt werden:
addHead(L,x)
fügt das Objekt x als erstes Element in die Liste ein.getFirst(L)
liefert das erste Element der Liste.removeFirst(L)
entfernt das erste Element der Liste und gibt es gleichzeitig als Ergebnis zurück.addLast(L,x)
hängt das Objekt x als letztes Element an die Liste an.getLast(L)
liefert das letzte Element der Liste.removeLast(L)
entfernt das letzte Element der Liste und gibt es gleichzeitig als Ergebnis zurück.Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Einfach verkettete Listen
Einfügen eines Elements x am Anfang ist durch den Anker head einfach in O(1) durch Änderung des head Zeigers auf x möglich. Das gleiche gilt für das Löschen des Kopfelements, ebenfalls nur eine Änderung von head.
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Einfach verkettete Listen
Einfügen und Löschen eines Elements x am Ende ist durch den Anker head nicht möglich. Stattdessen benötigt man das letzte und vorletzte Element beim Löschen, nur durch Suche (Iteration) in O(N) möglich! Auch ein tail Anker hilft beim Löschen nicht, beim Einfügen schon!
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Einfach verkettete Listen
Die Operationen zur Manipulation des Listenendes sind dagegen etwas komplexer.
Zum Löschen des letzten Elementes wird dagegen auch der vorletzte Knoten benötigt, da dessen Verweis auf null gesetzt werden muss.
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Einfach verkettete Listen
Operation | Komplexität (head) | Komplexität (head+tail) |
---|---|---|
addFirst | O(1) | O(1) |
getFirst | O(1) | O(1) |
removeFirst | O(1) | O(1) |
addLast | O(N) | O(1) |
getlast | O(N) | O(1) |
removeLast | O(N) | O(N) |
Der Aufwand aller Listenoperationen für einfach verkettete Listen
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Einfach verkettete Listen
L={head:null} function addLast(L,x) { node=L.head function Node(data,next) { while (node.next) return { data:data, next:next } node=node.next } node.next=Node(x,null) } function addFirst(L,x) { function getLast(L,x) { node=Node(x,L.head) node=L.head L.head=node while (node.next) } node=node.next function getFirst(L) { return node.data return L.head.data } } function removeLast(L,x) { function removeFirst(L,x) { node=L.head if (!L.head) return; while (node.next && node.next.next) first=L.head node=node.next L.head=L.head.next last=node.next node.next=null return first return last} }
Einfach verkettete Liste
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Einfach verkettete Listen
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Doppelt verkettete Listen
Ein Problem der Implementierung der verketteten Liste ist der mit dem Zugriff auf das letzte Element verbundene Aufwand.
insert(L,x,pos)
fügt das Element x an oder hinter pos in der Liste ein.remove(L,x)
entfernt ein beliebiges Element x aus der Liste.Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Doppelt verkettete Listen
Eine Verbesserung bietet daher die doppelt verkettete Liste, bei der jeder Knoten nicht nur seinen Nachfolger, sondern auch seinen Vorgänger kennt
K=<data|prev→K|next→K>L={ki∈K,∀i=1..n∧ki|next=ki+1∀i=1..n−1∧ki|prev=ki−1∀i=2..n}
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Doppelt verkettete Listen
Doppelt verkettete Liste
Für eine doppelt verkettete Liste werden zwei spezielle Knoten, head für den Listenanfang und tail für das Listenende, verwaltet. Durch die Verfügbarkeit des Listenendes und der rückwärts gerichteten Verweise vereinfachen sich die Operationen am Listenende. So kann das letzte Element für die Operation getLast direkt über tail mit konstantem Aufwand O(1) ermittelt werden. Auch beim Anhängen eines Elementes mit addLast kann über tail sofort der bisher letzte Knoten bestimmt werden, dessen next-Zeiger nun auf den neuen Knoten zeigt. Zusätzlich muss noch der prev-Zeiger des neuen Knotens auf den bisherigen letzten Knoten verweisen
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Einfach verkettete Listen
Anhängen am Ende einer doppelt verketteten Liste (analog Löschen, am Anfang wie 1 VKL)
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Doppelt verkettete Listen
Beim Löschen (removeLast) wird zunächst über den prev-Zeiger des letzten Knotens der vorletzte Knoten bestimmt, dessen next-Zeiger dann auf null gesetzt wird.
Auch für die Operationen addLast und removeLast beträgt der Aufwand daher O(1).
Ebenso einfach ist das Einfügen und Löschen von Elementen in der Mitte der Liste.
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Doppelt verkettete Listen
Operation | Komplexität (head+tail) |
---|---|
addFirst | O(1) |
getFirst | O(1) |
removeFirst | O(1) |
addLast | O(1) |
getlast | O(1) |
removeLast | O(1) |
insert | O(N) |
remove | O(N) |
Der Aufwand aller Listenoperationen für deppelt verkettete Listen
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Einfach verkettete Listen
L={head:null,tail:null} function Node(x,prev,next) { return { data:x, prev:prev, next:next } function addLast(L,x) { function insert(L,x,pos) { node=Node(x,L.tail,null) node=L.head if (L.tail) L.tail.next=node while(pos && node) { L.tail=node node=node.next if (!L.head) L.head=L.tail pos-- } } if (!node) return; function getLast(L,x) { next=node.next; return L.tail.data node.next=Node(x,node,next) } if (next) next.prev=node.next function removeLast(L,x) { } if (!L.tail) return; function remove(L,x) { last=L.tail node=L.head L.tail.prev.next=null while (node.data!==x) node=node.next; L.tail=L.tail.prev if (!node) return; return last removed=node; } node.prev.next=node.next if (node.next) node.next.prev=node.prev return removed }
Doppelt verkettete Liste (addHead, getHead, removeHead wie 1 VKL)
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Bereichslisten (Skiplisten)
Ein Nachteil von verketteten Listen gegenüber Feldern ist, dass die binäre Suche nicht einsetzbar ist, da man nicht beliebig »springen« kann. Eine Alternative ist es, mehrere Listen übereinanderzulegen, die dieses Springen ermöglichen. Eine derartige Datenstruktur wird als Skip-Liste bezeichnet.
Aufbau einer Skip-Liste
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Bereichslisten (Skiplisten)
Es gibt jeweils einen initialen und einen terminalen Listenknoten, die die Werte −∞ und + ∞ tragen. Grundsätzlich gibt es zwei Möglichkeiten zur Speicherung der Listenelemente auf mehreren Ebenen:
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Bereichslisten (Skiplisten)
Man startet in der höchsten Ebene mit dem initialen Knoten. Wenn man einen Knoten ansieht, gibt es drei Möglichkeiten:
Suche in einer Skip-Liste
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Bereichslisten (Skiplisten)
Die bisher gezeigte Liste hat eine statische Struktur mit festen Schrittlängen, die durch das Einfügen und Löschen von Werten zerstört werden würde (etwa wenn die Werte 8 und 9 eingefügt werden).
Allerdings ist der Algorithmus der Suche davon nicht betroffen: Selbst wenn 8 und 9 auf Ebene L0 eingefügt würden, würde die Suche weiterhin funktionieren – jedoch bei der Suche nach der 9 mehrere Schritte auf der untersten Ebenen durchlaufen.
Daher reicht es aus, wenn wir nur eine gute Näherung an feste Schrittlängen haben.
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Bereichslisten (Skiplisten)
Die zuerst eingeführten Skip-Listen mit Zweierpotenzen als festen Schrittlängen werden zur Abgrenzung von randomisierten Skip-Listen als perfekte Skip-Listen bezeichnet. Sie können insbesondere genutzt werden, wenn die Werte der Skip-Listen nicht mehr geändert oder ergänzt werden.
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Bereichslisten (Skiplisten)
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Das Iterator-Konzept
Die Implementierungen der Listen aus den vorigen Abschnitten weist noch ein Manko auf, welches die praktische Verwendbarkeit einschränkt. So ist es oft notwendig, eine Kollektion zu »durchwandern«, d.h. über alle Elemente zu navigieren.
Auch im Hinblick auf die Erhaltung des Prinzips der Kapselung ist daher ein Konzept wünschenswert, das eine einheitliche Behandlung des Navigierens unabhängig von der internen Realisierung unterstützt.
Das ist das Konzept der Iteratoren. Hierbei handelt es sich um Objekte zum Iterieren über Kollektionen.
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Das Iterator-Konzept
Ein Iterator verwaltet einen internen Zeiger auf die aktuelle Position in der zugrunde liegenden Datenstruktur.
Auf diese Weise ist es möglich, dass mehrere Iteratoren gleichzeitig auf einer Kollektion arbeiten.
Vier Funktionen von Iteratoren:
hasNext(I)
prüft, ob noch weitere Elemente in der Kollektion verfügbar sind. In diesem Fall wird true geliefert. Ist dagegen das Ende erreicht, wird false zurückgegeben.next(I)
liefert das aktuelle Element zurück und setzt den internen Zeiger des Iterators auf das nächste Element.remove(I)
erlaubt es, das zuletzt von next()
gelieferte Element zu löschen, wobei pro next-Aufruf nur einmal remove aufgerufen werden darf.reset(I)
setzt den internen Zustand zurück (auf Anfang).Text Lexer und Parser arbeiten nach dem Iteratorprinzip (hier über Textzeichen oder Texttokens).
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Das Iterator-Konzept
addp Iterator-Konzept
Stefan Bosse - Algorithmen und Datenstrukturen - Modul L Listen :: Das Iterator-Konzept
I={iterable:List,next:head(List)} function next(I) { node=I.next I.next=node.next return node}I={iterable:Array,next:0}function next(I) { node=I.iterable[I.next] I.next++ return node}
Iteratoren