Experiment: Die Türme von Hanoi 8. Rekursion Mathematische Rekursion, Terminierung, der Aufrufstapel, Beispiele, Rekursion vs. Iteration Links Mitte Rechts 213 212 Mathematische Rekursion Rekursion in Java: Genauso! ( 1 falls n ≤ 1 n! = n · (n − 1)!, andernfalls Viele mathematische Funktionen sind sehr natürlich rekursiv definierbar. Das heisst, die Funktion erscheint in ihrer eigenen Definition. public static int fakultaet ( int n) { if (n <= 1) { return 1; } else { return n ∗ fakultaet (n−1); } } ( 1, falls n ≤ 1 n! = n · (n − 1)!, andernfalls 218 n! ⇔ fakultaet(n) n − 1! ⇔ fakultaet(n−1) 219 Unendliche Rekursion Rekursive Funktionen: Terminierung ist so schlecht wie eine Endlosschleife. . . . . . nur noch schlimmer (“verbrennt” Zeit und Speicher) Wie bei Schleifen brauchen wir Fortschritt Richtung Terminierung Beispiel: f() → f() → f() → f() → . . . stack overflow public static void f () { f (); } fakultaet(n) terminiert sofort für n ≤ 1, andernfalls wird die Funktion rekursiv mit Argument < n aufgerufen. “n wird mit jedem Aufruf kleiner.” 220 Rekursive Funktionen: Auswertung 221 Der Aufrufstapel Beispiel: fakultaet(4) n=1 public static int fakultaet (int n){ } Bei jedem Funktionsaufruf: Wert des Aufrufarguments kommt auf einen Stapel if (n <= 1) return 1; return n * fakultaet(n-1); // n > 1 Es wird immer mit dem obersten Wert gearbeitet Am Ende des Aufrufs wird der oberste Wert wieder vom Stapel gelöscht Initialisierung des formalen Arguments: n = 4 Rekursiver Aufruf mit Argument n − 1, also 3 fakultaet(1) n=2 fakultaet(2) n=3 fakultaet(3) n=4 fakultaet(4) 1! = 1 1 2 · 1! = 2 2 3 · 2! = 6 6 4 · 3! = 24 24 System.out.println(fakultaet(4)) 222 223 Euklidischer Algorithmus Euklidischer Algorithmus in Java ( a, falls b = 0 gcd(a, b) = gcd(b, a mod b), andernfalls findet den grössten gemeinsamen Teiler gcd(a, b) zweier natürlicher Zahlen a und b basiert auf folgender mathematischen Rekursion: gcd(a, b) = ( a, public static int gcd(int a, intb){ if (b == 0) { return a; } else { Terminierung: a mod b < b, also return gcd(b, a%b); wird b in jedem rekursiven Aufruf } kleiner. } falls b = 0 gcd(b, a mod b), andernfalls 225 224 Fibonacci-Zahlen Fibonacci-Zahlen in Java Laufzeit falls n = 0 0, Fn := 1, falls n = 1 Fn−1 + Fn−2, falls n > 1 fib(50) dauert „ewig”, denn es berechnet F48 2-mal, F47 3-mal, F46 5-mal, F45 8-mal, F44 13-mal, F43 21-mal ... F1 ca. 109 mal (!) public static int fib ( int n){ if (n == 0 || n == 1){ return n; } else { return fib (n−1) + fib(n−2); } } Resultat: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 . . . 226 Korrektheit und Terminierung sind klar, aber... 228 Schnelle Fibonacci-Zahlen Schnelle Fibonacci-Zahlen in Java public static int fib ( int n)\{ if (n == 0) return 0; if (n <= 2) return 1; int a = 1; int b = 1; for ( int i = 3; i <= n; ++i){ int a_old = a; a = b; b += a_old; } return b; } Idee: Berechne jede Fibonacci-Zahl nur einmal, in der Reihenfolge F0 , F1 , F2 , . . . , Fn ! Merke dir jeweils die zwei letzten berechneten Zahlen (Variablen a und b)! Berechne die nächste Zahl als Summe von a und b! // F(1) // F(2) // F(i−2) // F(i−1) // F(i−1) += F(i−2) −> F(i) 230 229 Das Suchproblem Gegeben Menge von Datensätzen. 9. Suchen Beispiele Telefonverzeichnis, Wörterbuch, Symboltabelle Lineare Suche, Binäre Suche, Interpolationssuche, Exponentielle Suche, Untere Schranken [Ottman/Widmayer, Kap. 3.2, Cormen et al, Kap. 2: Problems 2.1-3,2.2-3,2.3-5] Jeder Datensatz hat einen Schlüssel k . Schlüssel sind vergleichbar: eindeutige Antwort auf Frage k1 ≤ k2 für Schlüssel k1 , k2 . Aufgabe: finde Datensatz nach Schlüssel k . 231 232 Das Auswahlproblem Suche in Array Gegeben Array A mit n Elementen (A[1], . . . , A[n]). Schlüssel b Gegeben Menge von Datensätzen mit vergleichbaren Schlüsseln k . Gesucht: Index k , 1 ≤ k ≤ n mit A[k] = b oder ”nicht gefunden”. Gesucht: Datensatz, mit dem kleinsten, grössten, mittleren Schlüssel. Allgemein: finde Datensatz mit i-kleinstem Schlüssel. 22 20 32 10 35 24 42 38 28 41 1 2 3 4 5 6 7 8 9 10 233 234 Lineare Suche Suche in sortierten Array Durchlaufen des Arrays von A[1] bis A[n]. Gegeben Sortiertes Array A mit n Elementen (A[1], . . . , A[n]) mit A[1] ≤ A[2] ≤ · · · ≤ A[n]. Schlüssel b Bestenfalls 1 Vergleich. Schlimmstenfalls n Vergleiche. Annahme: Jede Anordnung der n Schlüssel ist gleichwahrscheinlich. Erwartete Anzahl Vergleiche: Gesucht: Index k , 1 ≤ k ≤ n mit A[k] = b oder ”nicht gefunden”. N 1 X N +1 i= . N i=1 2 235 10 20 22 24 28 32 35 38 41 42 1 2 3 4 5 6 7 8 9 10 236 Binärer Suchalgorithmus BSearch(A,b,l,r) Divide and Conquer! Suche b = 23. 10 20 22 24 28 32 35 38 41 42 1 2 3 4 5 6 7 8 9 10 10 20 22 24 28 32 35 38 41 42 1 2 3 4 5 6 7 8 9 10 10 20 22 24 28 32 35 38 41 42 1 2 3 4 5 6 7 8 9 10 10 20 22 24 28 32 35 38 41 42 1 2 3 4 5 6 7 8 9 10 10 20 22 24 28 32 35 38 41 42 1 2 3 4 5 6 7 8 9 10 Input : Sortiertes Array A von n Schlüsseln. Schlüssel b. Bereichsgrenzen 0 ≤ l, r ≤ n Output : Index des gefundenen Elements. 0, wenn erfolglos. if n = 0 then return 0 m ← b(l + r)/2c if l > r then // erfolglose Suche return 0 else if b = A[m] then// gefunden return m else if b < A[m] then// Element liegt links return BSearch(A, B, l, m − 1) else // b > A[m]: Element liegt rechts return BSearch(A, B, m + 1, r) b < 28 b > 20 b > 22 b < 24 erfolglos 238 237 Analyse (Schlimmster Fall) Analyse (Schlimmster Fall) Rekurrenz (n = 2k ) ( d falls n = 1, T (n) = T (n/2) + c falls n >= 1. ( d falls n = 1, T (n) = T (n/2) + c falls n >= 1. Teleskopieren: T (n) = T n 2n +c=T n 4 +i·c i 2 n =T + log2 n · c. n ⇒ Annahme: T (n) = d + c log2 n =T Vermutung : T (n) = d + c · log2 n Beweis durch Induktion: + 2c Induktionsanfang: T (1) = d. Hypothese: T (n/2) = d + c · log2 n/2 Schritt (n/2 → n) T (n) = T (n/2) + c = d + c · (log2 n − 1) + c = d + c log2 n. 239 240 Resultat Iterativer binärer Suchalgorithmus Input : Sortiertes Array A von n Schlüsseln. Schlüssel b. Output : Index des gefundenen Elements. 0, wenn erfolglos. l ← 1; r ← n while l ≤ r do m ← b(l + r)/2c if A[m] = b then return m else if A[m] < b then l ←m+1 else r ←m−1 Theorem Der Algorithmus zur binären sortierten Suche benötigt Θ(log n) Elementarschritte. return 0; 242 241 Korrektheit Geht es noch besser? Algorithmus bricht nur ab, falls A leer oder b gefunden. Annahme: Gleichverteilung der Werte im Array. Invariante: Falls b in A, dann im Bereich A[l, ..., r] Beispiel Name ”Becker” würde man im Telefonbuch vorne suchen. ”Wawrinka" wohl ziemlich weit hinten. Binäre Suche vergleicht immer zuerst mit der Mitte. Beweis durch Induktion Induktionsanfang: b ∈ A[1, .., n] (oder nicht) Hypothese: Invariante gilt nach i Schritten Schritt: Binäre Suche setzt immer m = l + b < A[m] ⇒ b ∈ A[l, .., m − 1] b > A[m] ⇒ b ∈ A[m + 1, .., r] 243 r−l 2 . 244 Interpolationssuche Erwartete relative Position von b im Suchintervall [l, r] ρ= b − A[l] ∈ [0, 1]. A[r] − A[l] Neue ”Mitte”: l + ρ · (r − l) Anzahl Vergleiche im Mittel O(log log n) (ohne Beweis). ? Ist Interpolationssuche also immer zu bevorzugen? ! Nein: Anzahl Vergleiche im schlimmsten Fall Ω(n). 245