return null - TU Darmstadt

Werbung
Grundzüge der Informatik 1
Teil 6: Algorithmen und Datenstrukturen
6.2 Datenstrukturen
Prof. Dr. Max Mühlhäuser
FG Telekooperation
TU-Darmstadt
Agenda
Datenstrukturen
Warum abstrakte Datentypen?
Programmierung und Benutzung
einiger Datentypen
Implementierung dynamischer Strukturen
“Verzeigerte” Datenstrukturen
Effizienzbetrachtungen
2003 © MM ...
GdI1-6.2: Datenstrukturen
2
Lernziele
• Die Eigenschaften der Datenstrukturen Liste,
Stack, Queue und (Binär-)Baum zu kennen
• Selbstständig einfache dynamische
Datenstrukturen mit ihren Operationen
entwerfen, programmieren, beurteilen und
adäquat einsetzen zu können
2003 © MM ...
GdI1-6.2: Datenstrukturen
3
Warum abstrakte Datentypen?
Warum ist es sinnvoll, neben den bereits bekannten
primitiven und Behälterdatentypen wie 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).
2003 © MM ...
GdI1-6.2: 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:
2003 © MM ...
GdI1-6.2: Datenstrukturen
5
Datenstruktur: Liste
• Eine verkettete Liste besteht 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 schematisch 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.
2003 © MM ...
GdI1-6.2: Datenstrukturen
6
Implementierung von Listenelementen
public class LinkedElement {
Object element;
LinkedElement next;
// Implementiert verkettete Objekte
// Gespeicherter Inhalt
// Nachfolgendes Element
public LinkedElement(Object o) {
setElement(o);
}
public LinkedElement(Object o, LinkedElement next) {
this(o);
setNext(next);
}
public Object getElement() {
return element;
}
public LinkedElement getNext() {
return next;
}
public void setElement(Object o) {
element = o;
}
public void setNext(LinkedElement elem) {
next = elem;
}
// Wert speichern; next ist null
// Element speichern
// Verweis auf Knoten speichern
// Wert des Elements liefern
// Verweis auf Nachfolger liefern
// Wert des Elements setzen
// Verweis auf nächstes Element
}
2003 © MM ...
GdI1-6.2: Datenstrukturen
7
Einfügen von Elementen
• Im folgenden wird stets ab Listenanfang (head) zur gewünschten
Position gegangen.
– Dies erfolgt durch ein LinkedElement namens „ current“
– Das einzufügende Element „ newElem“ sei ebenfalls vom Typ
LinkedElement
• Beim Einfügen eines neuen Elements newElem in eine Liste sind drei
Fälle zu unterscheiden:
– Einfügen vor dem ersten Listenelement („prepend“)
– Einfügen hinter einem gegebenen Element („insertAfter“)
– Einfügen als letztes Element („append“)
• In den folgenden Abbildungen geben gepunktete Linien die neue
Position der vorherigen Elemente an; gestrichelte Linien markieren
geänderte Einträge.
2003 © MM ...
GdI1-6.2: Datenstrukturen
8
Listen: Einfügen
Einfügen am Anfang
1. newElem.setNext(head) // im Beispiel: newElem == Objekt 1
2. head = newElem
2003 © MM ...
GdI1-6.2: Datenstrukturen
9
Listen: Einfügen
Einfügen innerhalb der Liste
1. Laufe mit current zu dem Listenelement, hinter dem eingefügt werden soll
(hier: Objekt 2)
2. newElem.setNext(current.getNext()) // hier: newElem==Objekt 3
3. current.setNext(newElem)
2003 © MM ...
GdI1-6.2: Datenstrukturen
10
Listen: Einfügen
Einfügen am Ende der Liste
1. Laufe zum letzten Listenelement (current.getNext()==null),
hier: Objekt 4
2. current.setNext(newElem) // im Beispiel: newElem == Objekt 5
2003 © MM ...
GdI1-6.2: 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 head = head.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.
2003 © MM ...
GdI1-6.2: Datenstrukturen
12
Listen: Löschen
Löschen des ersten Listenelements
(delElem = head) zum Speichern der Referenz auf den Kopf
1. head = head.getNext()
2. delElem.setNext(null) (optional)
2003 © MM ...
GdI1-6.2: Datenstrukturen
13
Listen: Löschen
Löschen eines inneren Listenelements
1. Durchlaufe die Liste ab head bis current.getNext()==delElem.
2. current.setNext(delElem.getNext())
// hier: delElem == Objekt 3
3. delElem.setNext(null) (optional)
2003 © MM ...
GdI1-6.2: Datenstrukturen
14
Listen: Löschen
Löschen des letzten Listenelements
1. Durchlaufe Liste ab head bis current.getNext() == delElem.
Im Beispiel zeigt danach also current auf Objekt 4.
2. current.setNext(null)
2003 © MM ...
GdI1-6.2: 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
• Einfügen und Löschen am Anfang liegt in O(1)
2003 © MM ...
GdI1-6.2: Datenstrukturen
16
Verkettete Liste: Nachteile
• Zugriff erfolgt immer sequentiell
• Positionierung liegt in O(n)
• Vorgänger sind nur aufwändig erreichbar
– Suche ab head nach Element elem mit elem.getNext()
== current
• Operationen Einfügen / Löschen nur hinter dem
aktuellen Element möglich, sonst komplette
Positionierung erforderlich
– Einfügen / Löschen am Listenende daher in O(n)
2003 © MM ...
GdI1-6.2: Datenstrukturen
17
Vollständige Implementierung der Liste
public class LinkedList {
private LinkedElement head;
// Einfach verkettete Liste
// Kopf der Liste
public LinkedList() {
head = null;
}
// Leere Liste hat keinen Kopf
public LinkedElement getHead() {
return head;
}
// Kopf zurueckgeben
public boolean isEmpty() {
return head == null;
// Ist die Liste leer?
// … nur wenn kein Kopf da ist!
}
2003 © MM ...
GdI1-6.2: Datenstrukturen
18
Vollständige Implementierung der Liste 2
public void prepend(Object o) {
LinkedElement newElem = new LinkedElement(o);
newElem.setNext(head);
head = newElem;
}
// neues Element an Anfang stellen
// In Listenelement umwandeln
// Vor Kopf setzen
// Kopf umsetzen
public void append(Object o) {
LinkedElement current = head;
while (current != null
&& current.getNext() != null)
current = current.getNext();
if (current != null) {
LinkedElement newElem = new LinkedElement(o);
current.setNext(newElem);
current = null;
}
else
prepend(o);
}
// Ans Ende der Liste einfuegen
2003 © MM ...
// Solange noch "in" der Liste…
// …und noch nicht am Ende
// auf letztem Element?
// In Listenelement wandeln
// An letztes Element hängen
// Hilfselement loeschen
// Sonst war Liste leer
// Liste leer - an Anfang setzen
GdI1-6.2: Datenstrukturen
19
Vollständige Implementierung der Liste 3
public void insertAfter(Object o, Object insertAfter) { // o nach insertAfter
LinkedElement current = head;
while (current != null
// Solange noch "in" der Liste…
&& !(current.getElement().equals(insertAfter)))
// …und nicht auf „insertAfter“
current = current.getNext();
// weiter laufen
if (current != null) {
// Gefunden?
LinkedElement newElem = new LinkedElement(o); // In Listenelement wandeln
newElem.setNext(current.getNext());
// Nachfolger kopieren
current.setNext(newElem);
// Neues als Nachfolger setzen
current = null;
// Hilfselement loeschen
}
else
prepend(o);
// Liste leer - an Anfang setzen
}
public Object deleteFirst() {
if (head == null) return null;
LinkedElement delElem = head;
head = head.getNext();
delElem.setNext(null);
return delElem.getElement();
}
2003 © MM ...
// Erstes Element loeschen
// Liste leer – null liefern
// Kopf speichern
// Kopf weiterschalten
// Verweis loeschen
// Entferntes Element zurueckgeben
GdI1-6.2: Datenstrukturen
20
Vollständige Implementierung der Liste 4
public Object deleteLast() {
// Letztes Element loeschen
LinkedElement current = head, delElem = null; // Hilfszeiger
while (current != null
// Solange noch in der Liste…
&& current.getNext() != null) {
// und nicht auf letztem
delElem = current;
current = current.getNext();
// “vorheriges” Element
// “aktuelles” Element
}
if (delElem != null) {
delElem.setNext(null);
return current.getElement();
// Vor letztem Element
// Referenz loeschen
// Wert letztes Elements liefern
}
else
return deleteFirst();
// Nur ein Element – loeschen
}
2003 © MM ...
GdI1-6.2: Datenstrukturen
21
Vollständige Implementierung der Liste 5
public Object deleteAfter(Object o) { // Loeschen nach Objekt o
LinkedElement current = head;
while (current != null
// Solange noch in Liste…
&& !(current.getElement().equals(o))) // …und nicht gefunden
current = current.getNext();
// weitersuchen
if (current == null) return null;
// nicht gefunden bis Ende
LinkedElement delElem = current.getNext(); // Referenz speichern
if (delElem != null) {
// Gibt es Objekt(e) nach o?
current.setNext(delElem.getNext());
// Verweis uebergehen
delElem.setNext(null);
// Nachfolger loeschen
return delElem.getElement();
// Wert zurueckgeben
}
else
return null;
// Kein Objekt nach delElem
}
}
2003 © MM ...
GdI1-6.2: Datenstrukturen
22
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
• Damit ist nun auch Navigation zum vorhergehenden Element möglich
• Allerdings aufwändigere Einfüge-/Löschoperationen und höherer
Speicherbedarf
• Gleiche Komplexität wie bei einfach verketteter Liste
2003 © MM ...
GdI1-6.2: Datenstrukturen
23
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(Object o)
• Legt ein Element o auf den Stack.
– Object pop()
• Entfernt das oberste Element vom Stack und
liefert dieses zurück.
– Object top() (Java: peek() in java.util.Stack)
• 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.
2003 © MM ...
GdI1-6.2: Datenstrukturen
24
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.
2003 © MM ...
GdI1-6.2: Datenstrukturen
25
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.
2003 © MM ...
GdI1-6.2: Datenstrukturen
26
Beispiel für Stackanwendung
// Werte einen eingegebenen Ausdruck aus mittels eines Stacks
public int evaluate() {
IntStack s = new IntStack();
// speichert „int“ (s. Folie 28)
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();
}
2003 © MM ...
GdI1-6.2: Datenstrukturen
27
Beispiel für Stackanwendung
• Als Ergebnis der Operation wird s.pop() = 258
ausgegeben.
2003 © MM ...
GdI1-6.2: Datenstrukturen
28
Stack: Implementierung
Implementierung mit Array
• Die Elemente werden in einem Array gespeichert.
• Der Stack wird durch die Arraygröße begrenzt
• pop() muss das oberste Element nicht löschen
– es wird einfach beim nächsten Einfügen überschrieben
• Eine saubere Fehlerbehandlung mittels Exceptions fehlt
aus Platzgründen.
• Übungsaufgabe: Passen Sie die Klasse an den
„IntStack“ von Folie 26 an
– Insbesondere müssen die Methoden mit int arbeiten…
2003 © MM ...
GdI1-6.2: Datenstrukturen
29
Stack-Implementierung auf Array-Basis
public class ArrayStack {
private int capacity = 256;
private Object[] elements = new Object[capacity];
private int count = 0;
public boolean isEmpty() { return count == 0; }
public boolean isFull() { return count == capacity; }
public void push(Object o) {
if (!isFull())
elements[count++] = o;
}
public Object pop() {
if (!isEmpty())
return elements[--count];
else
return null;
}
public Object top()
{
if (!isEmpty())
return elements[count-1];
else
return null;
}
}
2003 © MM ...
// Stackumfang
// Speicher
// aktuelle Anzahl Elemente
// leer nur wenn 0 Elemente
// voll wenn maximale Elementzahl
// Speichern und Anzahl erhoehen
// geht nur wenn nicht voll!
// Oberstes entfernen und zurueckgeben
// Falls nicht leer, letztes liefern
// leer – null liefern
// Oberstes zurueckgeben
// Falls nicht leer, Position count-1 liefern
// Sonst (Stack leer) null liefern
GdI1-6.2: Datenstrukturen
30
Stack-Implementierung auf Listenbasis
public class Stack {
private LinkedList list;
public Stack() {
list = new LinkedList();
}
public boolean isEmpty() {
return list.isEmpty();
}
public void push(Object o) {
list.prepend(o);
}
public Object pop() {
return deleteFirst();
}
public Object top() {
if (isEmpty) return null;
return list.getHead().getElement();
}
}
2003 © MM ...
// Speicherung in Liste
// Liste zur Speicherung anlegen
// Test, ob Stack leer ist
// identisch zu leerer Liste
// Neues Element auf Stack stellen
// an Anfang der Liste einfuegen
// Erstes Stackelement loeschen
// Erstes Listenelement loeschen
// Zugriff auf oberstes Stackelement
// leerer Stack – null zurueckgeben
// Kopf der Liste liefern
GdI1-6.2: Datenstrukturen
31
Vergleich der Stack-Implementierungen
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
2003 © MM ...
GdI1-6.2: Datenstrukturen
32
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(Object e)
• Fügt e am Ende der Warteschlange ein.
– Object dequeue()
• Entfernt das erste Element.
– Object 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.
2003 © MM ...
GdI1-6.2: Datenstrukturen
33
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
tail
Arraygrenze wird
Hier wird
„angestellt“
wieder am Anfang
begonnen
• „Modulo-Arithmetik“
2003 © MM ...
GdI1-6.2: Datenstrukturen
34
Array-Implementierung einer Queue
public class ArrayQueue {
private Object[] elements;
private Object head=0, tail=-1;
private int capacity;
public ArrayQueue(int n) {
elements = new Object[n];
capacity = n;
}
public boolean isEmpty() {
return head == (tail+1) % capacity && elements[head] ==
}
public void enqueue(Object o) {
tail = (tail+1) % capacity;
elements[tail] = o;
}
public Object dequeue() {
if (isEmpty()) return null;
Object value = elements[head];
elements[head] = null;
head = (head+1) % capacity;
return value;
}
public Object front() {
return elements[head];
}
}
2003 © MM ...
// Elementespeicher
// Bedien- & Anstellposition
// Nur zur kürzeren Schreibweise
// Konstruktor mit Anzahlangabe
// Entsprechendes Array erzeugen
// Nur zur kürzeren Schreibweise
null;
// Objekt einstellen
// Tail weiterzaehlen
// Element neu einfuegen
// Falls leer, null liefern
// Wert des Kopfes merken
// Element loeschen
// Kopf weiterzaehlen
// Wert liefern
// Wert v. ersten Element liefern
GdI1-6.2: Datenstrukturen
35
Listen-Implementierung einer Queue
public class Queue {
private LinkedList list;
public Queue() {
list = new LinkedList();
}
public boolean isEmpty() {
return list.isEmpty();
}
public void enqueue(Object o) {
list.append(o);
}
public Object dequeue() {
return list.deleteFirst();
}
public Object front() {
if (isEmpty) return null;
return list.getHead().getElement();
}
}
2003 © MM ...
// Speicherung in Liste
// Noch kein Element enthalten
// Test, ob Queue leer ist
// identisch zu leerer Liste
// Neues Element anstellen
// -> bitte hinten anstellen!
// Entferne erstes Element
// -> loesche erstes Listenelement
// Zugriff auf erstes Element
// -> liefere Wert des Listenkopfs
GdI1-6.2: Datenstrukturen
36
Datenstruktur: Queue
Analyse der Implementierungen
• „Knoten“-Schlange kann dynamisch wachsen
• analog zu Stack...
• Lesen, Entfernen 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).
• Ohne Speicherung des Listenendes liegt das Anstellen
in O(n).
• Bei Speicherung mittels „tail“ Zeigers liegt die
Operation wieder in O(1)
2003 © MM ...
GdI1-6.2: Datenstrukturen
37
Datenstruktur Baum
• Bäume zählen zu den wichtigsten Datenstrukturen
• Ähnlich wie Listen enthält jedes Element einen oder
mehrere Verweise auf andere Baumelemente
• Typische Anwendungsgebiete umfassen etwa…
–
–
–
–
Stammbäume in der Ahnenforschung
Ablaufstrukturen bei Turnieren wie Fußball-EM oder –WM
Hierarchischer Aufbau von Unternehmensstrukturen
Syntax- und Ableitungsbäume bei der Verarbeitung von
Programmen
• Im folgenden werden erst einige Begriffe vorgestellt
2003 © MM ...
GdI1-6.2: Datenstrukturen
38
Terminologie zu Bäumen
•
•
•
•
•
•
•
•
Ein Baum besteht aus Knoten und Kanten
Knoten sind beliebige Objekte
Kanten verbinden Knoten miteinander
Kanten sind gerichtet und von einem Knoten zu seinen
Nachfolgern
Der einzige(!) Knoten ohne Vorgänger heißt Wurzel
Knoten ohne Nachfolger heißen Blätter
Knoten mit Nachfolgern heißen innere Knoten
Die Vorgänger- bzw. Nachfolgerbeziehung ist transitiv
– Alle Knoten des Baumes (außer der Wurzel selbst) sind Nachfolger
der Wurzel
• Kein Knoten hat sich selbst als Nachfolger (Zyklusfreiheit)
– Weder als direkten Nachfolger noch durch Transitivität
2003 © MM ...
GdI1-6.2: Datenstrukturen
39
Notation von Bäumen
• Die Wurzel wird stets oben gezeichnet
• Die Kanten werden ohne Pfeile gezeichnet und weisen
immer nach unten
• Jeder Knoten außer der Wurzel hat exakt einen
direkten Vorgänger
• Die Stufe eines Knotens ist die Anzahl Kanten, denen
man von der Wurzel zum Knoten folgen muss
– Entsprechend hat die Wurzel Stufe 0
– Die direkten Nachfolger der Wurzel haben Stufe 1, etc.
• Die Höhe eines Baumes ist die maximale Stufe plus 1
– Ein Baum, der nur aus einer Wurzel besteht, hat also Höhe 1
2003 © MM ...
GdI1-6.2: Datenstrukturen
40
Beispiel für einen Baum
2003 © MM ...
GdI1-6.2: Datenstrukturen
41
Binärbäume
• In einem Binärbaum hat jeder Knoten maximal 2
direkte Nachfolger
• Die Nachfolger werden links bzw. rechts vom Knoten
gezeichnet
– Daher auch als „linker und rechter Sohn“ bezeichnet
• Rekursive Definition:
– Ein Blatt ist ein Binärbaum
– Ein Knoten mit maximal zwei Nachfolgern ist ein
Binärbaum, wenn die Nachfolger leer sind oder Binärbäume
sind
2003 © MM ...
GdI1-6.2: Datenstrukturen
42
Unterformen von Binärbäumen
• In vollständigen Binärbäumen der Stufe n ist jeder
Knoten auf Stufe n ein Blatt, und jeder Knoten auf
Stufe i<n hat zwei nicht-leere Unterbäume
• Der vollständige Baum hat damit die maximal Anzahl
Knoten für seine Stufe
• Ein fast vollständiger Baum der Stufe n ist
vollständig bis Stufe n-1
• Stufe n enthält von links nach rechts keine Lücken
2003 © MM ...
GdI1-6.2: Datenstrukturen
43
Implementierung von Binärbäumen
• Prinzipiell ist die Implementierung ähnlich zu
doppelt verketteten Listen
• Allerdings gibt es keine Verweise auf die Vorgänger,
sondern nur auf Nachfolgeknoten!
• Der Verweis auf die Baumwurzel muss separat
gespeichert werden
2003 © MM ...
GdI1-6.2: Datenstrukturen
44
Implementierung von Binärbäumen
/**
* Diese Klasse implementiert das Geruest eines Binaerbaums
*/
public class BinaryTree {
/**
* Der linke Unterbaum ist wieder ein BinaryTree (oder null)
*/
BinaryTree left;
/**
* Der rechte Unterbaum ist wieder ein BinaryTree (oder null)
*/
BinaryTree right;
/**
* Der im Wurzelknoten (dem aktuellen Knoten) vermerkte Wert
*/
Object value;
2003 © MM ...
GdI1-6.2: Datenstrukturen
45
Implementierung von Binärbäumen
// Erzeuge einen neuen Binaerbaum aus den gegebenen Objekten
public BinaryTree(BinaryTree l, Object val, BinaryTree r)
{
left = l;
value = val;
right = r;
}
public BinaryTree getLeft() {
return left;
}
// Gib den linken Sohn zurueck
public BinaryTree getRight() {
return right;
}
public Object getValue() {
return value;
}
// Gib den rechten Sohn zurueck
// Gib den Wert des Knotens zurueck
}
2003 © MM ...
GdI1-6.2: Datenstrukturen
46
Alternative Array-Implementierung
• Ein Binärbäume der Höhe n kann auch in einem
Array der Größe 2n implementiert werden
• Dabei erhält die Wurzel die Position 0
• Jeder Nachfolger stufenweise von links nach rechts
– Alle fehlenden Knoten oder Blätter sind dabei mitzuzählen!
• Generell hat der linke Sohn eines Knotens mit Index
i den Index 2 * i +1, der rechte Sohn 2 * i + 2
• Für (fast) vollständige Bäume ist diese Codierung
sehr effizient
• Bei dünn besetzten Bäumen wird Speicher
verschwendet
2003 © MM ...
GdI1-6.2: Datenstrukturen
47
Baumtraversierungen
• Eine der Hauptanwendungen von Bäumen ist die Verarbeitung
der Elemente gemäß ihrer Anordnung
• Der Durchlauf („Traversierung“) erfolgt meist auf eine von vier
Arten
• W stehe nun für die aktuelle (Teilbaum-)Wurzel
• L / R stehe für die rekursive Fortsetzung der Traversierung
nach gleichem Schema im linken / rechten Teilbaum
– WLR („Pre-Order“): Wurzel, rekursiv linker Baum, rekursiv rechter
Baum
– LWR („In-Order“)
– LRW („Post-Order“)
– Stufenweise oben nach unten, links nach rechts („Level-Order“)
2003 © MM ...
GdI1-6.2: Datenstrukturen
48
Schema der Traversierungen
2003 © MM ...
GdI1-6.2: Datenstrukturen
49
Beispiel zur Traversierung
2003 © MM ...
GdI1-6.2: Datenstrukturen
50
Vor- und Nachteile von (Binär-)Bäumen
• Vorteile:
–
–
–
–
–
–
In vielen Anwendungen sehr nützlich
Gute Modellierung von hierarchischen Strukturen
Bei (fast) vollständigen Bäumen guter Zugriff: O(log2 n)
Sehr einfaches Einfügen und Löschen
Durchlauf in Pre-, In-, Post-Order sehr einfach
Einfache Umsetzung in Arrays
• Nachteile:
– Suche nach Elementen problematisch, da unsortiert
• Sonderform: binärer Suchbaum; Elemente links eines Knotens sind
immer kleiner, rechts immer größer als der Knotenwert
– Im Extremfall Entartung zur Liste (dann Zugriff in O(n))
– Navigation durch Verfolgung von Zeigern führt bis zu O(n)
2003 © MM ...
GdI1-6.2: Datenstrukturen
51
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.
2003 © MM ...
GdI1-6.2: Datenstrukturen
52
Vergleich Datentypen
• Stack
–
–
–
–
Einfaches Einfügen und Löschen: O(1)
Dynamisches Wachsen möglich bei Listenimplementierung
Sehr einfache Implementierung in Array oder Liste
Nur für spezielle Anwendungen geeignet („LIFO“)
• Queue
–
–
–
–
Einfaches Einfügen und Löschen: O(1) bzw. O(n) ohne „tail“
Dynamisches Wachsen möglich bei Listenimplementierung
Sehr einfache Listen-, etwas trickreichere Arrayumsetzung
Nur für spezielle Anwendungen geeignet („FIFO“)
• Binärbaum
– Einfaches Einfügen und Löschen
– Dynamisches Wachsen möglich
– Für sehr viele Anwendungen zum Speichern, Verwalten oder
Suchen nach Daten geeignet
2003 © MM ...
GdI1-6.2: Datenstrukturen
53
Herunterladen