Algorithmen und Datenstrukturen Dipl. Inform. Andreas Wilkens [email protected] Elementare Datenstrukturen Array Linked List Stack Queue Tree (Feld) (Verkettete Liste) (Stapel) (Warteschlange) (Baum) Einschub: Rekursion 2 1 Rekursion Rekursion ist keine elementare Datenstruktur, sondern eine Programmiertechnik. 3 Rekursion ein rekursives Programm ruft sich selbst auf bzw. eine rekursive Funktion ruft sich selbst auf zu jeder Rekursion gehört eine Abbruchbedingung 4 2 Beispiele für Rekursionen Das Fernsehbild, das sich selbst enthält die Fakultätsfunktion n! aus der Mathematik 5 Fakultätsfunktion n! n! = n * (n-1)! für alle natürlichen Zahlen n größergleich 1 0! = 1 (wichtig: die Abbruchbedingung!) 6 3 Rekursion – Iteration Rekursion ruft sich selbst auf enthält Abbruchbedingung Iteration wiederholtes Durchlaufen von Anweisungen Abbruch z.B. nach fest vorgegebener Durchlaufzahl oder durch erfüllte Abbruchbedingung Beispiel: Schleifen (for, while) 7 Terminierung Eine Rekursion muß genauso terminieren wie eine Iteration Bei Rekursion muß irgendwann ein Zustand erreicht werden, in dem kein weiterer rekursiver Aufruf erzeugt wird 8 4 direkt und indirekt direkte Rekursion Funktion ruft sich selbst auf indirekte Rekursion func_a() ruft func_b() auf, die wieder func_a() aufruft usw. 9 indirekte Rekursion void func_a(void) { .. func_b(); .. } void func_b(void) { .. func_a(); .. } void main(void) { .. func_a(); .. } 10 5 Speicherarten im PC Codebereich Register nimmt globale Variablen auf Stack dienen als Ablage zur Programmsteuerung Globaler Speicher enthält Programmcode nimmt u.a. lokale Variablen auf Heap restlicher Speicher, der vom Programmierer mit „new“ angefordert werden kann 11 Stackframe beim Aufruf einer Funktion wird ein Stackframe auf dem Laufzeitstack angelegt dieser enthält die lokalen Variablen die Parameter Rückkehrinformationen wird die Funktion beendet, wird der Stackframe wieder vom Laufzeitstack entfernt 12 6 Stackframes bei Rekursion Laufzeitstack mit 4 rekursiven Instanzen Top rekur(4) rekur(3) rekur(2) rekur(1) main Bottom 13 Teile und herrsche viele rekursive Programme wenden das sogenannte „Teile-und-herrsche“Prinzip an engl.: divide and conquer dabei erzeugt die rekursive Funktion zwei rekursive Aufrufe von sich selbst, jeweils mit der Hälfte der Eingabewerte 14 7 Beispiel für Teile und herrsche gegeben sei ein Maßstab dieser soll liniert werden mit Teilstrichen (siehe Lineal) die Teilstriche sollen verschiedene Höhen haben 15 Funktion „liniere“ // // // // // zeichnet Teilstriche an eine vorgegebene Strecke innerhalb der angegebenen Grenzen l und r Schema: Mittelstrich erhält die Höhe h, die Mittelstriche der entfallenden linken und rechten Hälfte erhalten die Höhe h-1 usw. void liniere(int l, int r, int h) { int m = (l+r)/2; markiere(m, h); if (h>1) { liniere(l, m, h-1); liniere(m, r, h-1); } } 16 8 Merkmale der Rekursion Rekursion ruft sich selbst auf enthält Abbruchbedingung void liniere(int l, int r, int h) { int m = (l+r)/2; markiere(m, h); if (h>1) { liniere(l, m, h-1); liniere(m, r, h-1); } } 17 Rekursionsbaum für einen Maßstab der Länge 8 18 9 Noch ein Rekursionsbeispiel Türme von Hanoi 19 Türme von Hanoi Scheiben unterschiedlicher Größen sollen von einem Lagerplatz zu einem anderen Transportiert werden Regeln: es darf nur eine Scheibe zur Zeit transportiert werden es darf nur eine kleinere auf eine größere Scheibe gelegt werden es steht ein zusätzlicher Hilfslagerplatz zur Verfügung 20 10 Beispiellösung Algorithmus zum Lösen des Problems Türme von Hanoi Siehe hanoi.cpp 21 Ablauf für 3 Scheiben: main: Anzahl der Scheiben: 3 towers( 3, A, B, C ); towers( 2, A, C, B ); towers( 1, A, B, C ); Ausgabe: Scheibe 1 von A nach Ausgabe: Scheibe 2 von A nach towers( 1, C, A, B ); Ausgabe: Scheibe 1 von C nach Ausgabe: Scheibe 3 von A nach towers( 2, B, A, C ); towers( 1, B, C, A ); Ausgabe: Scheibe 1 von B nach Ausgabe: Scheibe 2 von B nach towers( 1, A, B, C ); Ausgabe: Scheibe 1 von A nach Fertig C B B C A C C 22 11 Rekursionsbaum Wie sieht der Rekursionsbaum für den Aufruf towers(3, A, B, C) aus? 23 Türme von Hanoi die rekursive Funktion „towers“ besteht nur aus sehr wenigen Zeilen trotzdem kann damit ein recht hoher Aufwand geleistet werden 24 12 Frage Wie kommt man vom Problem „Türme von Hanoi“ zum Algorithmus? 25 Antwort durch nachdenken und vereinfachen 26 13 Einfachster Fall Türme von Hanoi mit nur einer Scheibe Transportiere die Scheibe vom Quellplatz zum Zielplatz Türme von Hanoi mit zwei Scheiben Transportiere die obere, kleinere Scheibe vom Quellplatz zum Hilfplatz Transportiere die untere, größere Scheibe vom Quellplatz zum Zielplatz Transportiere die kleinere Scheibe vom Hilfsplatz zum Zielplatz 27 Allgemeiner Fall (für die größte Scheibe) Um die unterste (größte) Scheibe vom Startplatz zum endgültigen Zielplatz transportieren zu können, muss man 1. 2. Erst alle anderen Scheiben aus dem Weg räumen (zum Hilfsplatz bringen) Jetzt die größte Scheibe zum Zielplatz bringen. 28 14 Allgemeiner Fall (für alle Scheiben) Um alle Scheiben vom Startplatz zum Zielplatz zu transportieren, muss man 1. 2. 3. Alle Scheiben (außer der größten) aus dem Weg räumen (zum Hilfsplatz bringen) Jetzt die größte Scheibe zum Zielplatz bringen. Dann alle anderen (vorher aus dem Weg geräumten Scheiben) wieder oben auf die größte Scheibe legen. (dies gilt immer, unabhängig von der Anzahl der Scheiben) 29 Funktion „towers“ Machen Sie sich die Bedeutung der (nur) drei Zeilen der Funktion „towers“ deutlich! Die drei genannten Punkte auf der vorangegangenen Folie entsprechen jeder genau einer Zeile der towers-Funktion! 30 15 Durchlaufen von Binärbäumen Um z.B. alle Werte eines Binärbaumes auszugeben, muß dieser durchlaufen werden. Das Durchlaufen nennt man auch “traversieren”. 31 Durchlaufen von Binärbäumen 1. 2. 3. 4. 5. 6. Verschiedene Möglichkeiten: WLR – Wurzel-Links-Rechts WRL – Wurzel-Rechts-Links LWR – Links-Wurzel-Rechts LRW – Links-Rechts-Wurzel RWL – Rechts-Wurzel-Links RLW – Rechts-Links-Wurzel 32 16 Beispiel 20 14 17 8 3 33 11 26 39 30 33 Auf einen Blick derselbe Baum, verschiedene Durchlaufvarianten: WLR: LWR: LRW: RWL: 20, 14, 8, 3, 11, 17, 33, 26, 30, 39 3, 8, 11, 14, 17, 20, 26, 30, 33, 39 3, 11, 8, 17, 14, 30, 26, 39, 33, 20 39, 33, 30, 26, 20, 17, 14, 11, 8, 3 34 17 Erkenntnis Durchläuft man einen Binärbaum nach LWR (Inorder), so erhält man eine aufsteigende Sortierung. Durchläuft man einen Binärbaum nach RWL, so erhält man eine absteigende Sortierung. 35 Programmieren Das Durchlaufen eines binären Suchbaums kann mittels Rekursion sehr einfach programmiert werden. Wiederholung Rekursion: eine rekursive Funktion ruft sich selbst auf zu jeder Rekursion gehört eine Abbruchbedingung 36 18 Datenstruktur eines Knotens struct node { ItemType wert; struct node left, right; }; 37 Preorder (WLR) programmiert void preorder(node *root) { /* Was muß hier stehen ?*/ } 38 19 Preorder (WLR) programmiert void preorder(node *root) { if (root!=NULL) { print_value(root); preorder(root->left); preorder(root->right); } } 39 Inorder (LWR) programmiert void inorder(node *root) { if (root!=NULL) { inorder(root->left); print_value(root); inorder(root->right); } } 40 20 Postorder (LRW) programmiert void postorder(node *root) { if (root!=NULL) { postorder (root->left); postorder (root->right); print_value(root); } } 41 Löschen im binären Suchbaum Neben dem Einfügen von Knoten ist auch das Löschen von Knoten zu betrachten. Dabei sind verschiedene Fälle zu berücksichtigen. 42 21 Löschen im binären Suchbaum 20 14 17 8 3 33 26 39 30 11 Fall 1: Löschen eines Blattes, z.B. Knoten mit Wert 30. Trivial! 43 Löschen im binären Suchbaum 20 14 17 8 3 33 11 26 39 30 Fall 2: Löschen eines Knotens mit nur einem Nachfolger (Knoten mit Wert 26) 44 22 Löschen im binären Suchbaum Fall 2: Der Nachfolger (-Teilbaum) kann den zu löschenden Knoten ersetzen. 45 Löschen im binären Suchbaum Fall 2: Vorgehen: 20 33 17 26 39 30 je ein Arbeitspointer verweist auf Vaterknoten 33 und auf Löschknoten 26 left-Pointer im Vaterknoten 33 wird auf den Nachfolgerteilbaum (Knoten 30) des Löschknotens umgesetzt Löschknoten 26 wird entsorgt, nachdem Wert 26 entnommen wurde 46 23 Löschen im binären Suchbaum 20 14 17 8 3 33 11 26 39 30 Fall 3: Löschen eines Knotens mit zwei Nachfolgeteilbäumen, z.B. Wurzelknoten 20 47 Löschen im binären Suchbaum Fall 3, Lösung a: einer der beiden Teilbäume ersetzt den Löschknoten der zweite Teilbaum wird in geeigneter Weise unter dem ersten Teilbaum gehängt 48 24 Löschen im binären Suchbaum Fall 3, Lösung a: Ergebnis: Teilbaum mit Wurzel 14 ersetzt Löschknoten 20 Teilbaum mit Wurzel 33 wird als rechter Nachfolger von Knoten 17 eingehängt 49 Löschen im binären Suchbaum Wie findet man den richtigen Punkt zum Einhängen des Teilbaums mit Wurzel 33? 50 25 Löschen im binären Suchbaum Fall 3, Nachteil von Lösung a: die Höhe des Baums wird größer im ungünstigsten Fall kann sich die Höhe verdoppeln 51 Löschen im binären Suchbaum 20 14 17 8 3 33 11 26 39 30 Fall 3: Löschen eines Knotens mit zwei Nachfolgeteilbäumen, z.B. Wurzelknoten 20 52 26 Löschen im binären Suchbaum Fall 3, Lösung b: Der zu löschende Knoten wird durch den Knoten mit dem in der Sortierreihenfolge nächstgrößeren oder nächstkleineren Wert ersetzt Achtung, der nächstgrößere oder nächstkleinere Knoten kann maximal einen Nachfolger haben, deshalb hierbei vorgehen wie in Fall 2, Löschen eines Knotens mit einem Nachfolger 53 Löschen im binären Suchbaum Ergebnis Fall 3, Lösung b: Knoten 20 wurde durch nächstgrößeren (Knoten 26) ersetzt Knoten 26 wurde an der ursprünglichen Stelle gelöscht 26 14 17 8 3 33 11 30 39 54 27 Löschen im binären Suchbaum Fall 3, Vorteil Lösung b: die Höhe des Baums hat sich nicht vergrößert 26 14 17 8 3 33 30 39 11 55 Löschen im binären Suchbaum Ablauf Fall 3, Lösung b: je ein Arbeitspointer verweist auf den Löschknoten (20) und auf dessen Vorgänger (hier nicht existent, deshalb root-Pointer verwenden) nächstgrößerer Knoten (26) wird lokalisiert und Arbeitspointer auf ihn und seinen Vorgänger (33) gesetzt Knoten 26 wird herausgeschnitten und durch seinen Nachfolgerknoten 30 ersetzt56 28 Löschen im binären Suchbaum left-Pointer im Vaterknoten 33 wird auf Knoten 30 umgebogen left-Pointer von 26 wird auf 14 gesetzt right-Pointer von 26 wird auf 33 gesetzt Vorgängerknoten des Löschknotens (hier der root-Pointer) wird auf die neue Wurzel 26 umgesetzt Wert 20 der alten Wurzel wird entnommen und der Knoten gelöscht fertig 57 Löschen im binären Suchbaum Fall 3, Fazit von Lösung b: deutlich aufwendiger zu programmieren als Lösung a aber auch deutliche Vorteile beim dadurch entstehenden Baum, insbesondere bei der Höhe deshalb wird Lösung b in der Regel bevorzugt 58 29