12. Rekursion Grundlagen der Programmierung 1 (Java) Fachhochschule Darmstadt Haardtring 100 D-64295 Darmstadt Prof. Dr. Bernhard Humm FH Darmstadt, 24. Januar 2006
Einordnung im Kontext der Vorlesung 1. Einführung 2. Einfache Programme 3. Kontrollstrukturen 4. Objekt-Orientierung I 5. Algorithmen und Datenstrukturen I 6. Interfaces 7. Pakete 8. Parametrisierte Typen (Generics) 10. Gutes Programmieren 11. Komponenten 12. Rekursion 13. Algorithmen und Datenstrukturen II 14. Objektorientierung II 15. Design 16. Die Java Klassenbibliothek I 17. Die Java Klassenbibliothek II 9. Fehler und Ausnahmen 24.1.2006, Seite 2
Agenda Definition Beispiele Eigenschaften rekursiver Algorithmen 24.1.2006, Seite 3
Definition von Rekursion Beispiel: Fakultät Eine Methode m() heißt rekursiv, wenn sie sich selbst aufruft m( ) m( ) m( ) n( ) m( ) direkt rekursiv indirekt rekursiv Beispiel: Berechnung der Fakultät (n!) n! = 1 * 2 * 3 *... * (n-1) * n (n-1)! rekursive Definition n! = (n-1)! * n 1! = 1 Rekursive Methode zur Berechnung der Fakultät long fact (long n) { if (n == 1) return 1; else return fact(n-1) * n; Allgemeines Muster if (Problem klein genug) nichtrekursiver Zweig; else rekursiver Zweig 24.1.2006, Seite 4
Ablauf einer rekursiven Methode Beispiel: Fakultät n = 4 24 long fact (long n) { if (n == 1) return 1 else return fact(n-1) * n; Jede Aktivierung von fact hat ihr eigenes n und rettet es über den rekursiven Aufruf hinweg n = 3 6 long fact (long n) { if (n == 1) return 1 else return fact(n-1) * n; n = 2 2 long fact (long n) { if (n == 1) return 1 else return fact(n-1) * n; n = 1 1 long fact (long n) { if (n == 1) return 1 else return fact(n-1) * n; 24.1.2006, Seite 5
Agenda Definition Beispiele Eigenschaften rekursiver Algorithmen 24.1.2006, Seite 6
Beispiel: binäres Suchen rekursiv z.b. Suche von 17 (Array muss sortiert sein) a 0 2 1 3 2 5 3 7 4 11 5 13 6 17 7 19 Index m des mittleren Element bestimmen 17 > a[m] in rechter Hälfte weitersuchen low m high a 0 2 1 3 2 5 3 7 4 11 5 13 6 17 7 19 low m high static int search (int elem, int[] a, int low, int high) { if (low > high) return -1; // empty int m = (low + high) / 2; if (elem == a[m]) return m; if (elem < a[m]) return search(elem, a, low, m-1); return search(elem, a, m+1, high); nichtrekursiver Zweig rekursiver Zweig 24.1.2006, Seite 7
Ablauf des rekursiven binären Suchens elem = 17, low = 0, high = 7 6 static int search (int elem, int[] a, int low, int high) { if (low > high) return -1; int m = (low + high) / 2; if (elem == a[m]) return m; if (elem < a[m]) return search(elem, a, low, m-1); return search(elem, a, m+1, high); m = 3 0 1 2 3 4 5 6 7 2 3 5 7 11 13 17 19 low m high low = 4, high = 7 6 static int search (int elem, int[] a, int low, int high) { if (low > high) return -1; int m = (low + high) / 2; if (elem == a[m]) return m; if (elem < a[m]) return search(elem, a, low, m-1); return search(elem, a, m+1, high); m = 5 0 1 2 3 4 5 6 7 2 3 5 7 11 13 17 19 low m high low = 6, high = 7 static int search (int elem, int[] a, int low, int high) { if (low > high) return -1; int m = (low + high) / 2; if (elem == a[m]) return m; if (elem < a[m]) return search(elem, a, low, m-1); return search(elem, a, m+1, high); 6 m = 6 0 1 2 3 4 5 6 7 2 3 5 7 11 13 17 19 low m high 24.1.2006, Seite 8
Beispiel: größter gemeinsamer Teiler rekursiv static int gcd (int x, int y) { int rest = x % y; if (rest == 0) return y; else return gcd(y, rest); iterativ static int gcd (int x, int y) { int rest = x % y; while (rest!= 0){ x = y; y = rest; rest = x % y; return y; 24.1.2006, Seite 9
Beispiel: Die Türme von Hanoi Gegeben sind 3 Pfosten mit n Scheiben (unterschiedlicher Größe) Grundstellung: alle Scheiben nach Größe geordnet auf Pfosten A A B C Ziel: Lege alle n Scheiben von A nach C Restriktion 1: jeweils nur eine Scheibe darf bewegt werden Restriktion 2: niemals darf eine größere auf einer kleineren Scheibe liegen Lösungsstrategie: Problem allg. für Turm der Höhe n lösen; folgende Fälle sind zu unterscheiden n = 0: gar nichts machen n > 0: (1) Turm der Höhe n-1 von A nach B bewegen (mittels C) (2) Scheibe von A nach C legen (3) Turm der Höhe n - 1 von B nach C bewegen (mittels A) 24.1.2006, Seite 10
Beispiel: Türme von Hanoi Rekursiver Algorithmus import java.io.*; public class Hanoi { public static void verlegeturm(int hoehe, int von, int nach, int ueber) { if (hoehe > 0) { verlegeturm(hoehe-1, von, ueber, nach); System.out.println("von "+von + " nach "+ nach); verlegeturm(hoehe-1, ueber, nach, von); A B C public static void main(string[] args) throws IOException { BufferedReader in = Text.open(System.in); int hoehe = Text.readInt(in); verlegeturm(hoehe, 1, 2, 3); 24.1.2006, Seite 11
Beispiel: Fibonacci-Funktion Rekursiver Algorithmus Fibonacci-Funktion fib(n) = 1 für n= 1, 2 und fib(n) = fib(n-1) + fib(n-2) für n > 2 Rekursiver Algorithmus zur Berechnung der Fibonacci-Zahlen public class Fib { public static int fib(int n) { if (n <= 2) else return 1; return fib(n-1) + fib(n-2); Algorithmus heißt effektiv, wenn er nach endlich vielen Schritten das korrekte Ergebnis liefert Algorithmis heißt effizient, wenn das Ergebnis mit einem Aufwand erreicht wird, der innerhalb vorgegebener Grenzen liegt 24.1.2006, Seite 12
Effizienzuntersuchung Fibonacci: Rekursive Implementierung da bei Aufrufen der fib()-methode stets die gleichen Schritte ausgeführt werden, ist Aufwand (mit c bezeichnet) proportional zur Anzahl der Aufrufe der Methode Abschätzung Anzahl rekursiver Aufrufe von fib in Abhängigkeit von n es gilt: c 1 = c 2 = 1 für n > 2: c n = 1 + c n-1 + c n-2 bei n > 3: c n-1 = 1 + c n-2 + c n-3, also c n-2 = c n-1-1 - c n-3. Einsetzen von c n-1 in Gleichung c n c n = 2 + 2c n-2 + c n-3 > 2c n-2 > 2 2 c n-4 > 2 3 c n-6 >...> 2 n DIV2-1 c 2 also c n > 2 n DIV2-1 (ist eine Untergrenze für c n ) c n = 1 + c n-1 + c n-2 = 2c n-1 - c n-3 < 2c n-1 < 2 2 c n-2 <...< 2 n-1 c 1 also c n < 2 n-1 (ist eine Obergrenze für c n ) Fibonacci-Methode erfordert mit n exponentiell wachsenden Aufwand, folglich ist Implementierung nicht effizient für große n. Gibt es effizientere Implementierungen? 24.1.2006, Seite 13
Effizienzuntersuchung Fibonacci-Funktion: Iterative Implementierung public static long fibit(int n) { long fibn = 0; long fibn1 = 1; // fuer Fib(n-1); long fibn2 = 1; // fuer Fib(n-2); if (n == 1) return 1; else if (n == 2) return 1; for (int i=3; i <= n; i++) { fibn = fibn1 + fibn2; fibn2 = fibn1; fibn1 = fibn; System.out.println("Fib "+n+ " = "+fibn); return fibn; Abschätzung der Anzahl der ausgeführte Schritte (Anweisungen) c n = 5 + (n-2)*3 + 1 iterative Fibonacci-Algorithmus erfordert mit n linear wachsenden Aufwand 24.1.2006, Seite 14
Beispiel: Ackermann-Funktion ack(n,m) = m + 1 falls n=0 ack(n-1,1) falls m=0 ack(n-1,ack(n,m-1)) sonst Vorsicht: Funktion wächst sehr stark: ack(4,2) besitzt 19729 Stellen ack(4,4) ist größer als 10 hoch 10 hoch 10 hoch 1900 public static int ack(int n, int m) { if (n == 0) return m + 1; else if (m == 0) return ack(n-1,1); else return ack(n-1,ack(n,m-1)); 24.1.2006, Seite 15
Agenda Definition Beispiele Eigenschaften rekursiver Algorithmen 24.1.2006, Seite 16
Vor- und Nachteile rekursiver Algorithmen Anmerkung zu jedem rekursiv formulierten Algorithmus gibt es einen äquivalenten iterativen Algorithmus Vorteile rekursiver Algorithmen kürzere Formulierung leichter verständliche Lösung Einsparung von Variablen teilweise sehr effiziente Problemlösungen (z.b. Quicksort) Bei rekursiven Datenstrukturen (zum Beispiel Bäume, Graphen) besonders empfehlenswert Nachteile rekursiver Algorithmen weniger effizientes Laufzeitverhalten (Overhead bei Funktionsaufruf) Verständnisprobleme bei Programmieranfängern Konstruktion rekursiver Algorithmen "gewöhnungsbedürftig" 24.1.2006, Seite 17
Rekursion in Programmiersprachen Nicht alle Programmiersprachen unterstützen rekursive Algorithmen Rekursion erlaubt: ALGOL-Familie (ALGOL 60, Simula 67, ALGOL 68, PASCAL, MODULA-2, Ada) PL/1 C und C++ Java C# Rekursion nicht möglich: FORTRAN COBOL LISP arbeitet überwiegend mit rekursiven Algorithmen (gilt i.d.r. auch für PROLOG) 24.1.2006, Seite 18