Datenstrukturen

advertisement
Grundzüge der Informatik
V15. Datenstrukturen
Prof. Dr. Max Mühlhäuser
FG Telekooperation
TU-Darmstadt
9.1.2003 © MM ...
GdI1 - Datenstrukturen
1
Agenda
Datenstrukturen
Warum abstrakte Datentypen?
Programmierung und Benutzung
einiger Datentypen
Implementierung dynamischer Strukturen
“Verzeigerte” Datenstrukturen
Effizienzbetrachtungen
Polymorphie
Einsatz spezieller Knotentypen
9.1.2003 © MM ...
GdI1 - Datenstrukturen
2
Lernziele
• Die Eigenschaften der Datenstrukturen
Liste, Stack und Queue zu kennen
• Selbstständig einfache dynamische
Datenstrukturen mit ihren Operationen
entwerfen, programmieren, beurteilen und
adäquat einsetzen zu können
• Polymorphie sinnvoll bei der
Implementierung von Datentypen
einsetzen zu können
9.1.2003 © MM ...
GdI1 - Datenstrukturen
3
Warum abstrakte Datentypen?
Warum ist es sinnvoll neben den bereits bekannten
primitiven und Behälterdatentypen (z.B. Array) noch
weitere zur Verfügung zu stellen?
• Prinzipiell wären die bereits bekannten Typen in
Kombination mit entsprechenden Algorithmen ausreichend
• Die Formulierung von Algorithmen wird jedoch erheblich
einfacher, wenn sie sich auf Datenstrukturen mit
Funktionalitäten abstützen können
• Ein wichtiger Aspekt der objektorientierten
Programmierung ist die Entwicklung neuer Typen und
deren Wiederverwendung (Reuse).
9.1.2003 © MM ...
GdI1 - Datenstrukturen
4
Datenstruktur: Liste
• Eine verkettete Liste (linked list) ist eine dynamische
Struktur mit einfachen Einfüge- und Löschmöglichkeiten.
Im Gegensatz zu Arrays können Listen zur Laufzeit
beliebig wachsen und verändert werden.
• Jedes Listenelement besteht aus zwei Komponenten:
– Einem Objekt oder primitiven Datentyp. Diese Komponente ist
das in der Liste abgelegte Element.
– Einem Zeiger auf das nächste Element in Form eines Zeigers auf
ein Listenelement.
• Schematischer Aufbau
eines Listenelements:
9.1.2003 © MM ...
GdI1 - Datenstrukturen
5
Datenstruktur: Liste
• Eine verkettete Liste besteht nun aus dem Aneinanderfügen von
mehreren Listenelementen.
• Da die next-Komponente eine Referenz auf ein Listenelement ist,
kann dort der Verweis auf ein weiteres Element abgelegt werden.
• Die so entstehende Struktur sieht wie folgt aus:
• Das Zeichen am Listenende steht dabei für eine leere Referenz,
auch als null bezeichnet. null bedeutet, dass kein
entsprechendes Objekt existiert. Im Beispiel gibt es bei Objekt 4
kein nächstes Element – die Liste endet hier.
9.1.2003 © MM ...
GdI1 - Datenstrukturen
6
Navigation in Listen
• Die Navigation in Listen, d.h. das Laufen von einem Element zu einem
anderen, erfolgt durch Zugriffe auf das next Element:
Element head;
// Verweis auf das erste Element
Element current; // Verweis auf das aktuelle Element
// Gehe zum nächsten Element der Liste
// Ist das aktuelle Element null, gehe zum Anfang
public void navigate()
{
if (current != null) // Gibt es ein aktuelles Element?
current = current.next();
// Ja => gehe zum nächsten Element
else
current = head;
// Sonst gehe zum ersten Element
}
9.1.2003 © MM ...
GdI1 - Datenstrukturen
7
Einfügen von Elementen
• Beim Einfügen eines neuen Elements newElem in eine Liste sind drei
Fälle zu unterscheiden:
– Einfügen vor dem ersten Listenelement („prepend“)
Das erste Listenelement wird zum Nachfolger des neuen Elements;
head zeigt auf das neue Element.
– Einfügen hinter einem gegebenen Element („insert“)
Es wird zu dem gewünschten Element gelaufen. next des neuen
Elements übernimmt den Wert next des aktuellen Elements;
anschließend wird current.next auf das neue Element gesetzt.
– Einfügen als letztes Element („append“)
Die Liste wird bis zum letzten Element durchlaufen, d.h. bis
current.getNext()==null. current.next wird auf die
Adresse des neuen Elements gesetzt.
=> Ein Sonderfall von „insert“.
• In den folgenden Abbildungen geben gepunktete Linien die neue
Position der vorherigen Elemente an; gestrichelte Linien markieren
geänderte Einträge.
9.1.2003 © MM ...
GdI1 - Datenstrukturen
8
Listen: Einfügen
Einfügen am Anfang
1. newElem.setNext(head) // im Beispiel: newElem == Objekt 1
2. head = newElem
9.1.2003 © MM ...
GdI1 - Datenstrukturen
9
Listen: Einfügen
Einfügen innerhalb der Liste
1. Laufe zu dem Listenelement, hinter dem eingefügt werden soll
(hier: Objekt 2)
2. newElem.setNext(current.next()) // hier: newElem==Objekt 3
3. current.setNext(newElem)
9.1.2003 © MM ...
GdI1 - Datenstrukturen
10
Listen: Einfügen
Einfügen am Ende der Liste
1. Laufe zum letzten Listenelement (current.next()==null),
hier: Objekt 4
2. current.setNext(newElem) // im Beispiel: newElem == Objekt 5
9.1.2003 © MM ...
GdI1 - Datenstrukturen
11
Löschen von Elementen
• Beim Löschen des Elements delElem einer verketteten
Liste sind drei Fälle zu unterscheiden:
– Löschen des ersten Listenelements
Setze first = first.getNext().
– Löschen innerhalb der Liste
Die Liste wird bis zum Vorgänger des zu löschenden Elements
durchlaufen, d.h. bis zu current.getNext() == delElem.
Setze dann current.setNext(delElem.getNext()).
– Löschen des letzten Elements
Ein einfacher Sonderfall des Löschens innerhalb der Liste.
9.1.2003 © MM ...
GdI1 - Datenstrukturen
12
Listen: Löschen
Löschen des ersten Listenelements
1. first = first.next()
2. delElem.setNext(null) (optional)
9.1.2003 © MM ...
GdI1 - Datenstrukturen
13
Listen: Löschen
Löschen eines inneren Listenelements
1. Durchlaufe die Liste ab head bis current.next()==delElem.
2. current.setNext(delElem.next()) // hier: delElem ==
Objekt 3
3. delElem.setNext(null) (optional)
9.1.2003 © MM ...
GdI1 - Datenstrukturen
14
Listen: Löschen
Löschen des letzten Listenelements
1. Durchlaufe die Liste ab first bis current.next() == delElem.
Im Beispiel zeigt danach also current auf Objekt 4.
2. current.setNext(null)
9.1.2003 © MM ...
GdI1 - Datenstrukturen
15
Verkettete Liste
• Vorteile:
– Dynamische Länge
– Es ist kein Kopieren der Elemente bei Löschen oder Einfügen
erforderlich
– Keine Speicherverschwendung durch Vorreservierung des
Speicherplatzes
– Einfache Einfüge- und Löschoperationen
– In den Listenelementen enthaltene Objekte können
verschiedenen Typ haben
• Nachteile:
– Zugriff erfolgt immer sequentiell
– Positionierung liegt in O(n)
– Vorgänger sind nur aufwändig erreichbar: Suche ab first nach
Element elem mit elem.getNext() == current
– Operationen Einfügen / Löschen nur hinter dem aktuellen
Element möglich, sonst komplette Positionierung erforderlich
9.1.2003 © MM ...
GdI1 - Datenstrukturen
16
Doppelt verkettete Liste
• Die doppelt verkettete Liste (doubly-linked list) behebt die
Einschränkung der Navigation nur zum Nachfolger der verketteten
Liste.
• Hierzu wird neben dem Zeiger next noch ein Zeiger prev eingefügt,
der auf das vorherige Element der Liste zeigt, bzw. null ist, falls es
sich um das erste Element handelt.
• Durch diesen Zeiger ist nun auch die Navigation zum vorhergehenden
Element möglich.
• Allerdings aufwändigere Einfüge-/Löschoperationen und höherer
Speicherbedarf.
9.1.2003 © MM ...
GdI1 - Datenstrukturen
17
Datenstruktur: Stack
• Ein Stack (Stapel- oder Kellerspeicher)
Tellerstapel in
der Mensa:
– ist eine Folge von Elementen
– zugreifbar ist immer nur das oberste Element (top)
– Das zuletzt eingefügte Element wird zuerst
entfernt = LIFO-Prinzip (Last-In First-Out)
• Operationen
– void push(Element e)
• Legt ein Element e auf den Stack.
– Element pop()
• Entfernt das oberste Element vom Stack und
liefert dieses zurück.
– Element top() (Java: peek())
• Liest das oberste Element, ohne es zu entfernen.
Feder schiebt
Teller nach
oben
– boolean isEmpty()
• Ergibt true, wenn der Stack leer ist, sonst false.
• Die Operationen pop() und top() sind nur zulässig, wenn der Stack
nicht leer ist.
9.1.2003 © MM ...
GdI1 - Datenstrukturen
18
Datenstruktur: Stack
• Typische Anwendungen für Stacks sind beispielsweise
– Verwaltung von Rücksprungadressen
Dies tritt vor allem bei Rekursion oder
Funktionsaufrufen auf.
– Hilfestellung bei der Syntaxanalyse von Programmen
Insbesondere wird der Stack dabei für die Zuteilung
der passenden Regel der BNF zu den gelesenen
Zeichenfolgen notwendig.
– Auswertung von logischen oder arithmetischen
Ausdrücken. Ein einfaches Beispiel dazu folgt auf
den nächsten Folien.
9.1.2003 © MM ...
GdI1 - Datenstrukturen
19
Beispiel für Stackanwendung
• Als Beispiel soll ein arithmetischer Ausdruck in Postfix-Notation
ausgewertet werden unter Benutzung eines Stacks.
• Während bei der herkömmlichen (Infix-)Notation das Rechenzeichen immer zwischen den Operatoren steht („5 + 3 * 7“), steht
es bei der Postfix-Notation hinter den Operatoren („5 3 + 7 *“).
• Der Vorteil dieser Notation ist die leichte Überprüf- und
Auswertbarkeit. Zusätzlich werden keine Klammern benötigt.
• Im folgenden schematischen Beispiel wird angenommen, dass eine
Routine nextToken existiert, die ein Zeichen zurückgibt, das
entweder „+“, „*“ oder eine Ziffer von 0 bis 9 ist. Das Programm
schreibt nun der Reihe nach die gelesenen Zahlen auf den Stack.
Trifft es auf eine Operation (+, *), so berechnet es das Ergebnis
der Operation und schreibt dieses wieder auf den Stack, bis das
Zeichen '.' als Eingabeende erkannt wurde.
9.1.2003 © MM ...
GdI1 - Datenstrukturen
20
Beispiel für Stackanwendung
// Werte einen eingegebenen Ausdruck aus mittels eines Stacks
public int evaluate() {
IntStack s = new IntStack();
char value;
// Gelesenes Zeichen
while ((value = nextToken()) != '.') // Zeichen lesen; weiter?
{
if (value == '+')
// Addition
s.push(s.pop() + s.pop());
// Summe der beiden letzten Werte
else
if (value == '*')
// Multiplikation
s.push(s.pop() * s.pop()); // Produkt der letzten Werte
else if (value >= '0' && value <= '9') // Zahl?
s.push(value – '0');
// Wert auf den Stack schreiben
}
return s.pop();
}
9.1.2003 © MM ...
GdI1 - Datenstrukturen
21
Beispiel für Stackanwendung
• Als Ergebnis der Operation wird s.pop() = 258
ausgegeben.
9.1.2003 © MM ...
GdI1 - Datenstrukturen
22
Stack: Implementierung
Implementierung mit Array
– Elemente werden in einem Array gespeichert. Die maximale
Größe des Stacks ist somit durch die Arraygröße limitiert.
– Es ist nicht notwendig, bei einem pop das oberste Element
tatsächlich zu löschen.
public class IntStack
{
private int[] elements = new int[256];
// Elementespeicher
private int count = 0;
// Anzahl gespeicherter Elemente
public boolean isEmpty() { return count == 0; }
public void push(int e) { elements[count++] = e; }
// Element speichern und Anzahl erhöhen
public int pop()
{ return elements[--count]; }
// Oberstes Element zurückgeben und Anzahl verringern
public int top()
{ return elements[count-1]; }
}
9.1.2003 © MM ...
GdI1 - Datenstrukturen
23
Stack: Implementierung
Implementierung mit verketteter Liste
public class IntStack
{
private Node top;
// erster Knoten
public Stack() {
top = null;
// Noch kein Element enthalten
}
public boolean isEmpty() { return top == null; }
public void push(int e) { top = new Node(e, top); }
// Neuer Knoten zeigt auf das alte top
public int pop() { int v = top.element();
top = top.next();
return v; }
// top auf nachfolgenden Knoten setzen
public int top() { return top.element(); }
// Auf Inhalt des top-Knotens zugreifen
}
9.1.2003 © MM ...
GdI1 - Datenstrukturen
24
Stack: Implementierung
Implementierung mit verketteter Liste
class Node
{
int element;
Node next;
// Gespeicherter Inhalt
// Nachfolgender Knoten
Node(int e, Node n) {
element = e; // Element speichern
next = n;
// Verweis auf nachfolgenden Knoten speichern
}
int element() { return element; }
Node next()
{ return next; }
}
9.1.2003 © MM ...
GdI1 - Datenstrukturen
25
Stack
Vergleich der Implementierungen
• Zugriffe sind bei beiden gleich effizient: O(1)
• „Knoten“-Stapel kann dynamisch wachsen
• Die Größe muss nicht vorher bekannt sein
• Wachsen kann ohne „Erweiterungskosten“ erfolgen
• Größe nur durch Hauptspeicher begrenzt; für Arrays
muss ein kontinuierlicher Block zur Verfügung stehen
• Speicherplatzvorteil hängt von der Anzahl der Elemente
ab
• Arrays sind speichereffizienter, wenn sie gut
ausgelastet sind
• Verkettete Knoten sind speichereffizienter,
wenn viel vom entsprechenden Array ungenützt bliebe
9.1.2003 © MM ...
GdI1 - Datenstrukturen
26
Datenstruktur: Queue
• Eine Queue (Warteschlange)
– ist eine Folge von Elementen
– Das zuerst eingefügte Element wird zuerst
entfernt = FIFO-Prinzip (First-In First-Out)
Warteschlange in
der Mensa:
• Operationen
– void enqueue(Element e)
• Fügt e am Ende der Warteschlange ein.
– void dequeue()
• Entfernt das erste Element.
– Element front()
• Liefert das erste Element der Warteschlange.
Hinten
anstellen
– boolean isEmpty()
Vorne
bedient
werden
• Ergibt true, wenn die Warteschlange leer ist, sonst false.
• Anwendungen
– Pufferung von Ereignissen/Daten, Aufreihung von „Threads“,
Breitensuche bei Graphen, usw.
9.1.2003 © MM ...
GdI1 - Datenstrukturen
27
Datenstruktur: Queue
Implementierung mit Array
• Naive Implementierung erfordert das Verschieben von
Elementen beim „Bedienen“
head
• „Zirkuläres“ Array vermeidet
Verschieben
• head & tail
werden einfach
Hier wird
erhöht
„bedient“
• Beim Erreichen der
Arraygrenze wird
Hier wird
wieder am Anfang
„angestellt“
begonnen
tail
• „Modulo-Arithmetik“
9.1.2003 © MM ...
GdI1 - Datenstrukturen
28
Datenstruktur: Queue
Implementierung mit Array
public class Queue
{
private int[] elements;
// Elementespeicher
private int head=0, tail=0; // Bedien- & Anstellposition
private int c;
// Nur zur kürzeren Schreibweise
public Queue(int n) {
// Konstruktor mit Anzahlangabe
elements=new int[n];
// Entsprechendes Array erzeugen
c=elements.length;
// Nur zur kürzeren Schreibweise
}
public boolean isEmpty() { return head==tail; }
public void enqueue(int e)
{ elements[tail]=e; tail=(tail+1)%c; }
public void dequeue()
{ head=(head+1)%c; }
public int front()
{ return elements[head]; }
}
Nur gültig, wenn !isEmpty()
9.1.2003 © MM ...
GdI1 - Datenstrukturen
29
Datenstruktur: Queue
Implementierung mit verketteter Liste
public class Queue {
private Node head=null, tail=null;// Bedien- & Anstellknoten
public Queue() {}
// keine Anzahlangabe nötig
public boolean isEmpty() { return head==null; }
public void dequeue()
{ head=head.next(); }
// head auf nachfolgenden Knoten setzen
public int front()
{ return head.element(); }
public void enqueue(int e)
Leere Schlange =>
{ if (tail==null) {
Ende & Anfang auf
head=tail=new Node(e, null);
gleichen Knoten setzen
} else {
tail.setNext(new Node(e, null)); Schlange nicht leer =>
Knoten hinten anhängen
tail=tail.next();
}
tail-Zeiger auffrischen
}
}
9.1.2003 © MM ...
GdI1 - Datenstrukturen
30
Datenstruktur: Queue
Analyse der Implementierungen
• „Knoten“-Schlange kann dynamisch wachsen
• analog zu Stack...
• Zugriffe sind bei beiden gleich effizient: O(1)
• Die Array-Implementierung erreicht dies nur durch die
„zirkuläre“ Nutzung des Arrays.
Ansonsten hätten beim Anstellen eines Elements
immer alle Element verschoben werden müssen: O(n).
• Die Knoten-Implementierung erreicht dies nur durch
Hinzunahme eines „tail“ Zeigers.
Ansonsten hätte für das Anstellen eines Elements
immer die ganze Schlange durchlaufen werden
müssen: O(n).
9.1.2003 © MM ...
GdI1 - Datenstrukturen
31
Vergleich Datentypen
• Die unterschiedlichen Datentypen haben spezifische Vor- und
Nachteile. Die Auswahl erfolgt je nach Anwendung.
• Array
– Array-basierter Datentyp, der dynamisch wächst:
java.util.Vector.
– Elemente adressierbar, daher direkter Zugriff möglich.
– Einfüge/Löschoperationen im inneren der Liste ineffizient: O(n).
– Verbinden mehrerer Listen ebenfalls ineffizient: Verweise
kopieren und ggf. Array vergrößern.
• Verkettete Liste
– Einfügen und Löschen effizient: O(1).
– Einfaches Verbinden von Listen möglich.
– Nur sequentieller Zugriff möglich, kein direkter Zugriff.
9.1.2003 © MM ...
GdI1 - Datenstrukturen
32
Polymorphe Knotentypen
Im allgemeinen müssen Operationen auf Listen
Fallunterscheidungen vornehmen
– Liste leer / Cursor ganz vorne
– Nur ein Element enthalten / Cursor am Ende
– Mehrere Elemente enthalten / Cursor in der Mitte
Probleme
• Wird die Funktionalität erweitert (z.B. Elemente zählen),
so muss die gleiche Fallunterscheidung wieder
implementiert werden
• Einzelne Fallunterscheidungen können komplex ausfallen
(verschachtelte if-statements)
9.1.2003 © MM ...
GdI1 - Datenstrukturen
33
Polymorphe Knotentypen
• Leerer Stack
count → 0
top
• Ein Element
top
count → 1
3
count → 1 + next().count()
• Mehrere Elemente
top
9.1.2003 © MM ...
3 next
count → 1 + next().count()
1 next
GdI1 - Datenstrukturen
1
count → 1
34
Polymorphe Knotentypen
Knoten für leeren Stack
Gemeinsame Oberklasse
aller Knotentypen
class EmptyNode extends Node
{
int element()
{ throw new IllegalStateException(); }
Node next()
{ throw new IllegalStateException(); }
Node pop()
{ throw new IllegalStateException(); }
Node push(int e)
{ return new EndNode(e, null); }
int size() { return 0; }
}
• Elementanzahl = 0
• Abheben nicht erlaubt
• Drauflegen ergibt Endknoten-Objekt
9.1.2003 © MM ...
GdI1 - Datenstrukturen
35
Polymorphe Knotentypen
Knoten für einelementigen Stack
Stellt element(),
next() bereit
class EndNode extends DataNode
{
EndNode(int e, Node n) { super(e,n); }
Node pop()
{ return new EmptyNode(); }
Node push(int e) { return new InnerNode(e, this); }
int size()
{ return 1; }
}
• Elementanzahl = 1
• Abheben ergibt das Leere-Stack-Objekt
• Drauflegen ergibt Innerer-Knoten-Objekt
9.1.2003 © MM ...
GdI1 - Datenstrukturen
36
Polymorphe Knotentypen
Knoten für mehrelementigen Stack
class InnerNode extends DataNode
{
InnerNode(int e, Node n) { super(e,n); }
Node pop()
{ return next(); }
Node push(int e) { return new InnerNode(e, this); }
int size()
{ return 1 + next().size(); }
}
• Elementanzahl = 1 + Elementanzahl des Rests
• Abheben ergibt den restlichen Stack
• Drauflegen ergibt Innerer-Knoten-Objekt
9.1.2003 © MM ...
GdI1 - Datenstrukturen
37
Polymorphe Knotentypen
Benutzung durch einen „polymorphen“ Stack
public class PolyStack
{
private Node top;
public PolyStack() {
top=new EmptyNode();
}
public int size()
public void pop()
public void push(int e)
// Noch kein Element enthalten
{ return top.size(); }
{ top = top.pop(); }
{ top = top.push(e); }
}
• Es wird keine einzige Fallunterscheidung benötigt!
• Verschiedene Fälle auf Knotentypen entzerrt
9.1.2003 © MM ...
GdI1 - Datenstrukturen
38
Datenstrukturen: Zusammenfassung
• Datenstrukturen erleichtern die Formulierung von
Algorithmen
– Grundprinzip der OOP: Wiederverwendung
• Datenstrukturen mit einer „Knoten“Implementierung können dynamisch wachsen
• Doppelt-verkettete Listen können alle
sequentiellen Operationen in O(1) unterstützen
• Polymorphe Knotentypen können die
Programmierung vereinfachen
– Fälle/Verantwortlichkeiten werden aufgeteilt
9.1.2003 © MM ...
GdI1 - Datenstrukturen
39
Herunterladen