Experiment: Die Türme von Hanoi. Rekursion Mathematische Rekursion, Terminierung, der Aufrufstapel, Beispiele, Rekursion vs. Iteration Links Mitte Rechts Mathematische Rekursion Viele mathematische Funktionen sind sehr natürlich rekursiv definierbar. Das heisst, die Funktion erscheint in ihrer eigenen Definition. n! =, falls n n (n )!, andernfalls Rekursion in Java: Genauso! n! = falls n n (n )!, andernfalls public static int fakultaet ( int n) if (n <= ) return ; else return n fakultaet (n ); n! fakultaet(n) n! fakultaet(n ) 0 0
Unendliche Rekursion Rekursive Funktionen: Terminierung ist so schlecht wie eine Endlosschleife...... nur noch schlimmer ( verbrennt Zeit und Speicher) Beispiel: f() f() f() f()... stack overflow public static void f () f (); Wie bei Schleifen brauchen wir Fortschritt Richtung Terminierung fakultaet(n) terminiert sofort für n, andernfalls wird die Funktion rekursiv mit Argument < n aufgerufen. n wird mit jedem Aufruf kleiner. 0 0 Rekursive Funktionen: Auswertung Der Aufrufstapel Beispiel: fakultaet() public static int fakultaet (int n) if (n <= ) return ; return n * fakultaet(n-); // n > Initialisierung des formalen Arguments: n = Rekursiver Aufruf mit Argument n, also Bei jedem Funktionsaufruf: Wert des Aufrufarguments kommt auf einen Stapel Es wird immer mit dem obersten Wert gearbeitet Am Ende des Aufrufs wird der oberste Wert wieder vom Stapel gelöscht n =! = fakultaet() n =! = fakultaet() n =! = fakultaet() n =! = fakultaet() System.out.println(fakultaet()) 0 0
Euklidischer Algorithmus 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, falls b = 0 gcd(b, a mod b), andernfalls Euklidischer Algorithmus in Java gcd(a, b) = public static int gcd(int a, intb) if (b == 0) return a; else return gcd(b, a%b); a, falls b = 0 gcd(b, a mod b), andernfalls Terminierung: a mod b < b, also wird b in jedem rekursiven Aufruf kleiner. 0 0 Fibonacci-Zahlen 0, falls n = 0 F n :=, falls n = F n + F n, falls n > Resultat: 0,,,,,,,,,,,... Fibonacci-Zahlen in Java Laufzeit fib(0) dauert ewig, denn es berechnet F -mal, F -mal, F -mal, F -mal, F -mal, F -mal... F ca. mal (!) public static int fib ( int n) if (n == 0 n == ) return n; else return fib (n ) + fib(n ); Korrektheit und Terminierung sind klar, aber...
Schnelle Fibonacci-Zahlen Schnelle Fibonacci-Zahlen in Java Idee: Berechne jede Fibonacci-Zahl nur einmal, in der Reihenfolge F 0, F, F,..., F n! Merke dir jeweils die zwei letzten berechneten Zahlen (Variablen a und b)! Berechne die nächste Zahl als Summe von a und b! public static int fib ( int n)\ if (n == 0) return 0; if (n <= ) return ; int a = ; int b = ; for (int i = ; i <= n; ++i) int a_old = a; a = b; b += a_old; return b; // F() // F() // F(i ) // F(i ) // F(i ) += F(i ) > F(i) Das Suchproblem Gegeben. Suchen Menge von Datensätzen. Beispiele Telefonverzeichnis, Wörterbuch, Symboltabelle Jeder Datensatz hat einen Schlüssel k. Schlüssel sind vergleichbar: eindeutige Antwort auf Frage k k für Schlüssel k, k. Aufgabe: finde Datensatz nach Schlüssel k.
Suche in Array Lineare Suche Gegeben Array A mit n Elementen (A[],..., A[n]). Schlüssel b Gesucht: Index k, k n mit A[k] = b oder nicht gefunden. Durchlaufen des Arrays von A[] bis A[n]. Bestenfalls Vergleich. Schlimmstenfalls n Vergleiche. Annahme: Jede Anordnung der n Schlüssel ist gleichwahrscheinlich. Erwartete Anzahl Vergleiche: 0 n n i= i = n +. Suche in sortierten Array Divide and Conquer! Gegeben Suche b =. Sortiertes Array A mit n Elementen (A[],..., A[n]) mit A[] A[] A[n]. Schlüssel b Gesucht: Index k, k n mit A[k] = b oder nicht gefunden. 0 0 0 b < b > 0 b > 0 0 0 b < erfolglos 0
Binärer Suchalgorithmus BSearch(A,b,l,r) Input : Sortiertes Array A von n Schlüsseln. Schlüssel b. Bereichsgrenzen l r n oder l > r beliebig. Output : Index des gefundenen Elements. 0, wenn erfolglos. m (l + r)/ 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 ) else // b > A[m]: Element liegt rechts return BSearch(A, b, m +, r) Analyse (Schlimmster Fall) Rekurrenz (n = k ) Teleskopieren: T (n) = d falls n =, T (n/) + c falls n >. ( n ) ( n ) T (n) = T + c = T + c ( n ) = T + i c ( i n = T + log n) n c. Annahme: T (n) = d + c log n Analyse (Schlimmster Fall) Resultat T (n) = d falls n =, T (n/) + c falls n >. Vermutung : T (n) = d + c log n Beweis durch Induktion: Induktionsanfang: T () = d. Hypothese: T (n/) = d + c log n/ Schritt (n/ n) Theorem Der Algorithmus zur binären sortierten Suche benötigt Θ(log n) Elementarschritte. T (n) = T (n/) + c = d + c (log n ) + c = d + c log n.
Iterativer binärer Suchalgorithmus Korrektheit Input : Sortiertes Array A von n Schlüsseln. Schlüssel b. Output : Index des gefundenen Elements. 0, wenn erfolglos. l ; r n while l r do m (l + r)/ if A[m] = b then return m else if A[m] < b then l m + else r m return 0; Algorithmus bricht nur ab, falls A leer oder b gefunden. Invariante: Falls b in A, dann im Bereich A[l,..., r] Beweis durch Induktion Induktionsanfang: b A[,.., n] (oder nicht) Hypothese: Invariante gilt nach i Schritten Schritt: b < A[m] b A[l,.., m ] b > A[m] b A[m +,.., r] Min und Max. Auswählen? Separates Finden von Minimum und Maximum in (A[],..., A[n]) benötigt insgesamt n Vergleiche. (Wie) geht es mit weniger als n Vergleichen für beide gemeinsam?! Es geht mit N Vergleichen: Vergleiche jeweils Elemente und deren kleineres mit Min und grösseres mit Max.
Das Auswahlproblem Ansätze Eingabe Unsortiertes Array A = (A,..., A n ) paarweise verschiedener Werte Zahl k n. Ausgabe: A[i] mit j : A[j] < A[i] = k Spezialfälle k = : Minimum: Algorithmus mit n Vergleichsoperationen trivial. k = n: Maximum: Algorithmus mit n Vergleichsoperationen trivial. k = n/ : Median. Wiederholt das Minimum entfernen / auslesen: O(k n). Median: O(n ) Sortieren (kommt bald): O(n log n) Pivotieren O(n)! 0 Pivotieren Algorithmus Partition(A[l..r], p) Wähle ein Element p als Pivotelement Teile A in zwei Teile auf, den Rang von p bestimmend. Rekursion auf dem relevanten Teil. Falls k = r, dann gefunden. p > > > p > > > > r n Input : Array A, welches den Sentinel p im Intervall [l, r] mindestens einmal enthält. Output : Array A partitioniert in [l..r] um p. Rückgabe der Position von p. while l < r do while A[l] < p do l l + while A[r] > p do r r swap(a[l], A[r]) if A[l] = A[r] then l l + return l-
Korrektheit: Invariante Korrektheit: Fortschritt Invariante I: A i p i [0, l), A i > p i (r, n], k [l, r] : A k = p. while l < r do while A[l] < p do l l + while A[r] > p do r r swap(a[l], A[r]) if A[l] = A[r] then l l + return l- I I und A[l] p I und A[r] p I und A[l] p A[r] I while l < r do while A[l] < p do l l + while A[r] > p do r r swap(a[l], A[r]) if A[l] = A[r] then l l + return l- Fortschritt wenn A[l] < p Fortschritt wenn A[r] > p Fortschritt wenn A[l] > p oder A[r] < p Fortschritt wenn A[l] = A[r] = p Wahl des Pivots Das Minimum ist ein schlechter Pivot: worst Case Θ(n ) p p p p p Ein guter Pivot hat linear viele Elemente auf beiden Seiten. p ɛ n ɛ n Analyse Unterteilung mit Faktor q (0 < q < ): zwei Gruppen mit q n und ( q) n Elementen (ohne Einschränkung q q). T (n) T (q n) + c n = c n + q c n + T (q n) =... = c n c n i=0 q i geom. Reihe = c n q = O(n) log q (n) i=0 q i + T ()
Wie bekommen wir das hin? Der Zufall hilft uns (Tony Hoare, ). Wähle in jedem Schritt einen zufälligen Pivot. schlecht gute Pivots schlecht Wahrscheinlichkeit für guten Pivot nach einem Versuch: =: ρ. Wahrscheinlichkeit für guten Pivot nach k Versuchen: ( ρ) k ρ. Erwartungswert der geometrischen Verteilung: /ρ = Algorithmus Quickselect (A[l..r], i) Input : Array A der Länge n. Indizes l i r n, so dass für alle x A[l..r] gilt, dass j A[j] x l und j A[j] x r. Output : Partitioniertes Array A, so dass j A[j] A[i] = i if l=r then return; repeat wähle zufälligen Pivot x A[l..r] p l for j = l to r do if A[j] x then p p + l+r until p (l+r) m Partition(A[l..r], x) if i < m then quickselect(a[l..m], i) else quickselect(a[m..r], i)