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