1 / 20 Einstieg in die Informatik mit Java Rekursion Gerd Bohlender Institut für Angewandte und Numerische Mathematik
Gliederung 2 / 20 1 Überblick 2 Rekursion 3 Rekursive Sortieralgorithmen 4 Backtracking
Gliederung 3 / 20 1 Überblick 2 Rekursion 3 Rekursive Sortieralgorithmen 4 Backtracking
4 / 20 Überblick In diesem Kapitel werden rekursive Methoden beschrieben. Rekursion in Mathematik Direkte Umsetzung mathematischer Formeln Rekursion in Algorithmen Rekursive Sortieralgorithmen Backtracking
Gliederung 5 / 20 1 Überblick 2 Rekursion 3 Rekursive Sortieralgorithmen 4 Backtracking
Rekursion Bei der Rekursion ruft sich eine Methoden selbst auf. Man unterscheidet zwei Varianten: (1) Bei der direkten Rekursion ruft sich die Methode bei ihrer Definition selber auf, (2) bei der indirekten oder wechselseitigen Rekursion rufen sich zwei oder mehrere Methoden gegenseitig auf, d. h. beispielsweise ruft Methode a() Methode b() auf und umgekehrt. Zu beachten ist: Es muss darauf geachtet werden, dass die Rekursion nach endlich vielen Schritten endet, dass also nach endlich vielen Aufrufen ein nicht-rekursiver Zweig durchlaufen wird. Es handelt sich bei der Rekursion um ein sehr leistungsfähiges Werkzeug, mit dem z.b. mathematische Definitionen direkt umgesetzt werden können. In vielen Fällen ist die Iteration mit einer Schleife effizienter, da der Aufwand für den rekursiven Aufruf gespart wird. 6 / 20
7 / 20 Rekursion - Eigenschaften Lokale Variablen werden bei jedem rekursiven Aufruf erneut angelegt, existieren also ggf. in mehreren Varianten. Nur die aktuellen Werte des letzten Aufrufs sind zugreifbar. Die vorigen Werte bleiben jedoch erhalten und werden wieder zugänglich, sobald der letzte rekursive Aufruf beendet ist. Bei einer primitiven Rekursion wird die rekursive Methode bei ihrer Definition nur einmal aufgerufen. Solche Methoden können leicht durch eine iterative Lösung ersetzt werden. Der Aufwand ist dann meist O(n). Die rekursive Methode kann sich allerdings auch zweioder mehrfach aufrufen. In diesem Fall ist die Umwandlung in eine iterative Lösung schwieriger. Der Aufwand ist dann meist O(2 n ), steigt also exponentiell an mit der Tiefe der rekursiven Aufrufe.
Rekursion - Beispiel Exponentiation 8 / 20 Beispiel Berechnung von x n, n N, also n 0 { x n x n 1 x, falls n > 0, = 1, sonst. s t a t i c double power ( double x, i n t n ) { i f ( n > 0) return power ( x, n 1) x ; / / Rekursion else return 1; / / n i c h t r e k u r s i v e r Zweig } Hier liegt primitive Rekursion vor, eine iterative Lösung ist durch den geringeren Overhead etwas effizienter.
9 / 20 Rekursion - Beispiel Fibonacci-Zahlen Beispiel Berechnung der Fibonacci Zahlen f 0 = 1, f 1 = 1, f i = f i 1 + f i 2 für i 2. s t a t i c i n t f i b o n a c c i ( i n t i ) { i f ( i <=1) return 1; / / n i c h t r e k u r s i v e r T e i l else return f i b o n a c c i ( i 1) + f i b o n a c c i ( i 2); / / zwei (! ) r e k u r s i v e Aufrufe }
10 / 20 Rekursion - Beispiel Fibonacci-Zahlen Der Vorteil liegt hier in der direkten Umsetzung der mathematischen Formel. Allerdings ist dies in diesem konkreten Fall extrem ineffizient, da viele Werte mehrfach berechnet werden! Beispiel fibonacci(5) von Hand nachrechnen! Für die n-te Fibonaccizahl werden für n = 0, 1, 2, 3, 4, 5, 6,... insgesamt 1, 1, 3, 5, 9, 15, 25,... Aufrufe der Methode fibonacci benötigt! Der Rechenaufwand wächst also grundsätzlich exponentiell wie O(c n ) mit c 1.6 Eine iterative Lösung kommt hier mit nur n Schleifendurchläufen aus. Rechenaufwand O(n)
Gliederung 11 / 20 1 Überblick 2 Rekursion 3 Rekursive Sortieralgorithmen 4 Backtracking
Rekursive Sortieralgorithmen Rekursive Sortieralgorithmen zum Sortieren einer Menge M gehen oft von der folgenden Vorgehensweise aus: Teile M in zwei etwa gleich große Mengen M 1 und M 2. Sortiere M 1 und M 2 rekursiv mit dem gleichen Algorithmus. Kombiniere die Teillösungen zur sortierten Lösung von M. Überlegungen zum Rechenaufwand: Bei jedem Rekursionsschritt verdoppelt sich die Anzahl der rekursiven Aufrufe. Der Rechenaufwand steigt hierdurch nicht an, da M 1 und M 2 nur jeweils halb so groß sind. Hat die Menge M insgesamt n Elemente, dann bricht die Rekursion nach log 2 n rekursiven Aufrufen ab, da dann die Teilmengen nur noch ein Element haben. Der gesamte Rechenaufwand beträgt dann im Idealfall nur Anzahl der Feldelemente mal Rekursionstiefe, also O(n log n) Der Aufwand ist also wesentlich geringer als bei einem einfachen iterativen Algorithmus wie BubbleSort mit O(n 2 ) 12 / 20
13 / 20 Rekursive Sortieralgorithmen - Beispiel MergeSort MergeSort verwendet einen einfachen Algorithmus für die Aufteilung der Menge, dafür ist die Kombination zur Gesamtlösung kompliziert. M wird einfach in der Mitte aufgeteilt in M 1 und M 2. Die Teilmengen sind immer gleich mächtig (bis auf ein Element, wenn M ungerade viele Elemente hat). Bei der Kombination der Teillösungen von M 1 und M 2 zur Gesamtlösung M müssen die Elemente verglichen und entsprechend ihrer Größe zusammengemischt werden. Hierzu wird meist ein Hilfsfeld benötigt.
14 / 20 Rekursive Sortieralgorithmen - Beispiel QuickSort QuickSort verwendet einen komplizierteren Algorithmus für die Aufteilung der Menge, dafür ist die Kombination zur Gesamtlösung einfach. In M wird ein Pivotelement p gewählt. Dann werden alle Elemente p in M 1 und die restlichen in M 2 aufgeteilt. Bei günstiger Wahl von p sind die beiden Mengen etwa gleich mächtig. Bei ungünstiger Wahl kann die Aufteilung aber auch sehr ungleich sein, was sich negativ auf die Laufzeit auswirkt! Bei der Kombination von M 1 und M 2 zur Gesamtlösung M müssen die Elemente nur hintereinander kopiert werden. Je nach Speicherung ergibt sich dies meist automatisch, d.h. der Schritt entfällt.
Gliederung 15 / 20 1 Überblick 2 Rekursion 3 Rekursive Sortieralgorithmen 4 Backtracking
Backtracking 16 / 20 Backtracking (deutsch: Methode der Rückverfolgung) ist eine Vorgehensweise, um die Lösung eines Problems schrittweise zu bestimmen. Voraussetzung ist also, dass die Lösung des Problems in mehrere Teilschritte zerlegt werden kann. Backtracking verwendet das Versuch-und-Irrtum-Prinzip (trial and error): es wird versucht, eine erreichte Teillösung schrittweise zu einer Gesamtlösung auszubauen. Sobald ein Fehler erkannt wird, dass also die vorliegende Teillösung nicht zu einer Lösung des Gesamtproblems führen kann, wird der letzte Schritt zurückgenommen. Dann werden Alternativen probiert. Falls auch dies nicht zur Lösung führt, werden weitere Schritte zurückgenommen. Falls alle Alternativen durchprobiert wurden und keine weiteren Alternativen zur Verfügung stehen, dann ist bewiesen, dass keine Lösung existiert.
17 / 20 Backtracking Eigenschaften Eigenschaften von Backtracking-Algorithmen: Mit einem Backtracking-Algorithmus wird entweder eine vorhandene Lösung gefunden oder es kann bewiesen werden, dass keine Lösung existiert. Falls mehrere Lösungen existieren, können sie alle systematisch gefunden werden. Backtracking wird meistens am einfachsten rekursiv implementiert. Dies ist ein typischer Anwendungsfall von Rekursion. Unter Umständen erfordern Backtracking-Algorithmen sehr lange Laufzeit (wenn alle Lösungsvarianten durchsucht werden müssen).
Backtracking Zeitkomplexität 18 / 20 Zeitkomplexität: Zu jeder Teillösungen gebe es z mögliche Alternativen. Bei jeder Erhöhung der Rekursionstiefe N um 1 erhöht sich der Aufwand für die Suche nach der optimalen Lösung um den Faktor z. Backtracking hat damit im schlechtesten Fall mit O(z N ) eine exponentielle Laufzeit. Bei großer Suchtiefe n und Verzweigungsgrad z > 1 dauert die Suche somit oft sehr lange. Daher ist das Backtracking primär für Probleme mit einem kleinen Lösungsbaum geeignet. Es gibt jedoch Methoden, mit welchen die Zeitkomplexität eines Backtracking-Algorithmus verringert werden kann. Diese sind u. a.: Heuristiken: zielgerichtete Suche Akzeptanz von Näherungslösungen
Backtracking Typische Beispiele Typische Beispiele von Problemen, die durch Backtracking gelöst werden können: 8 Damenproblem: 8 Damen sollen auf einem Schachbrett ohne gegenseitige Bedrohung platziert werden. Springerproblem: Ein Springer soll alle Felder eines Schachbretts ohne Wiederholung besuchen. Rucksackproblem: Ein Rucksack mit der Tragfähigkeit B soll mit einer Auswahl aus N Gegenständen bepackt werden. Der Wert der ausgewählten Gegenstände soll maximal sein, aber ihr Gewicht darf die Tragfähigkeit des Rucksacks nicht überschreiten. Dieses Problem tritt in abgewandelter Form in industriellen Anwendungen auf. Färbeproblem: Eine Landkarte mit B Ländern soll mit N verschiedenen Farben eingefärbt werden. Benachbarte Länder müssen unterschiedlich eingefärbt sein. Labyrinth: Suche einen Weg durch ein Labyrinth. Navigationssystem: Suche den optimalen Weg vom Startort zum Ziel. 19 / 20
20 / 20 Backtracking Beispiel Sudoku Lösung eines Sudokus: Ein Sudoku besteht aus 9 9, also 81 Feldern. In jedes Feld soll eine Zahl zwischen 1 und 9 eingetragen werden. Einige Zahlen sind vorgegeben. In jeder Zeile und jeder Spalte darf jede Zahl nur einmal vorkommen. Das Sudoku ist in 9 Blocks zu je 3 3 Feldern aufgeteilt. Auch in jedem Block darf jede Zahl nur einmal auftreten. Zur Lösung müssen theoretisch 9 81 Lösungsvarianten untersucht werden, dies würde Milliarden von Jahren an Rechenzeit erfordern. In der Praxis fallen fast alle Varianten sofort weg, die Rechenzeit in der Praxis ist unter einer Sekunde.