U NIVERSIT ÄT B ERN I NFORMATIK VORLESUNG EI T YP UL B LATT 2 (1/5) AUSGABE HS 08 Lösungsvorschlag Serie 2 – Rekursion 1. Algorithmen-Paradigmen Es gibt verschiedene Algorithmen-Paradigmen, also grundsätzliche Arten, wie man einen Algorithmus formulieren kann. Im funktionalen Paradigma werden Berechnungen durch Funktionen formuliert. Damit lassen sich natürlich v.a. mathematische Funktionen sehr einfach ausdrücken. Es gibt Programmiersprachen, in welchen sich Algorithmen dieses Paradigmas unverändert implementieren lassen (z.B. Haskell). Die Fibonacci-Funktion wird funktional wie folgt definiert: f ib(0) = f ib(1) = 1 f ib(n) = f ib(n − 1) + f ib(n − 2) Algorithmen gemäss dem imperativen Paradigma bilden die verbreitetste Art, Algorithmen für Computer zu formulieren, da sie auf einem abstrakten Modell eines üblichen Rechners basieren. Imperative Algorithmen basieren auf den Konzepten Anweisung und Variable. Dabei werden die Befehle in einer genau definierten Reihenfolge abgearbeitet. Das imperative Paradigma wird durch viele Programmiersprachen wie C, C++, COBOL und auch Java realisiert. Da Maschinenprogramme für übliche Rechner schlussendlich auch imperativ sind, folgt daraus, dass z.B. in funktionalen Programmiersprachen implementierte Programme letztlich auch in imperative übersetzt werden. Es ist deshalb üblich, Algorithmen gemäss dem imperativen Paradigma zu formulieren. Man sollte insbesondere darauf achten, die verschiedenen Konzepte nicht zu vermischen. (a) fib(n) als imperativer Algorithmus (rekursiv) Der Pseudocode für den imperativen Algorithmus könnte folgendermassen aussehen: int Algorithm fib(n): // Input : n (nicht negativer Integer-Wert) // Output: Fibonacci Zahl von n // Zweck : Rekursive Berechnung der Fibonacci-Zahl if n < 2 then return 1 else return (fib(n-1) + fib(n-2)) (b) Wie sich die Rekursion zur Laufzeit entfaltet, lässt sich Abbildung 1 entnehmen. Kommentar: Dieser rekursive Algorithmus zur Berechnung von f ib(n) stellt sich zur Laufzeit als sehr aufwändig und ineffizient (exponentielle Komplexität) heraus. Immer wieder werden Ausdrücke erneut berechnet (z. B. fib(3)), deren Resultat eigentlich bereits ausgerechnet wurde. Es werden also sehr viele redundante Berechnungen vorgenommen. 1 1 1 fib(0) fib(2) fib(3) 1 1 fib(1) fib(4) 1 fib(1) 1 2 fib(2) 1 fib(0) 1 fib(5) 1 fib(1) 1 fib(2) 3 1 fib(3) 1 fib(0) 2 1 fib(1) 1 1 fib(1) 1 fib(2) 1 fib(3) 1 fib(0) 2 5 1 fib(1) 1 3 fib(4) 1 fib(1) 1 2 fib(2) 1 fib(0) 1 VORLESUNG EI fib(1) 1 2 3 5 8 fib(6) 13 U NIVERSIT ÄT B ERN I NFORMATIK T YP UL B LATT 2 (2/5) Abbildung 1: Dynamische Entfaltung der Rekursion fib(6). AUSGABE HS 08 U NIVERSIT ÄT B ERN I NFORMATIK VORLESUNG EI T YP UL B LATT 2 (3/5) AUSGABE HS 08 (c) Es ist möglich, den Algorithmus so in einen rekursiven Algorithmus umzuformulieren, dass er eine lineare Zeitkomplexität aufweist: fib(n) als imperativer Algorithmus (endrekursiv) Der rekursive Algorithmus mit linearer Zeitkomplexität ist endrekursiv. int Algorithm linrecfib(n, a1, a2): // Input : n (nicht negativer Integer-Wert) // Output : Fibonacci Zahl von n // Zweck : Iterative Berechnung der Fibonacci-Zahl if n==0 return a1 else linrecfib(n-1, a2, a1+a2) Dieser Algorithmus ist gleich effizient wie der nachfolgend vorgestellte iterative Algorithmus. fib(n) als imperativer Algorithmus (iterativ) int Algorithm iterativfib(n): // Input : n (nicht negativer Integer-Wert) // Output : Fibonacci Zahl von n // Zweck : Iterative Berechnung der Fibonacci-Zahl a = b = result = 1 for (i=2; i<=n; i++) result = a + b b = a a = result return result fib(n) explizit berechnet Bisher mussten wir zur Berechnung einer Fibonacci-Zahl alle vorhergehenden Zahlen berechnen. Der französische Mathematiker Jacques-Philippe-Marie Binet hat bereits 1843 eine Funktion gefunden, mit welcher sich eine beliebige Fibonacci-Zahl explizit berechnen lässt: Ã Ã √ !n+1 √ !n+1 1 1+ 5 1 1− 5 f (n) = √ −√ 2 2 5 5 2. (a) Der rekursive Algorithmus zur Berechnung von sum(n) kann wie folgt aussehen: Algorithm sum(n): // Input : ganze Zahl n>=1 // Output: Summe von 1 bis n if n==1 then return 1 else return n + sum(n-1) (b) Die rekursive Implementation von sum(n) unter a) ist eine lineare, direkte Rekursion, die nicht endrekursiv ist (als letzte Operation wird die Addition ausgeführt). U NIVERSIT ÄT B ERN I NFORMATIK VORLESUNG EI T YP UL B LATT 2 (4/5) AUSGABE HS 08 (c) Der iterative Algorithmus zur Berechnung von sum(n) kann wie folgt aussehen: int Algorithm sum(n): // Input : ganze Zahl n>=1 // Output: Summe von 1 bis n sum = 0 for i=1 to n do sum = sum + i return sum 3. (a) Die Lösung ist in Abbildung 2 dargestellt. y 4 3 5 2 6 1 7 8 1 2 0 1 2 3 4 0 0 3 4 5 x Abbildung 2: Die gefüllte Figur Nachstehend ist die Reihenfolge der rekursiven Aufrufe aufgeführt. Dabei gibt die Zahl rechts die momentane Rekursions- bzw. Stacktiefe an. Flood_Fill(2,3) Flood_Fill(3,3) Flood_Fill(4,3) Flood_Fill(5,3) Flood_Fill(3,3) Flood_Fill(4,4) Flood_Fill(4,2) Flood_Fill(5,2) Flood_Fill(3,2) Flood_Fill(4,3) Flood_Fill(4,1) Flood_Fill(5,1) Flood_Fill(3,1) Flood_Fill(4,2) Flood_Fill(4,0) Flood_Fill(2,3) Flood_Fill(3,4) Flood_Fill(3,2) Flood_Fill(1,3) Flood_Fill(2,3) Flood_Fill(0,3) Flood_Fill(1,4) Flood_Fill(1,2) Flood_Fill(2,2) Flood_Fill(0,2) Flood_Fill(1,3) Flood_Fill(1,1) 1 2 3 4 4 4 4 5 5 5 5 6 6 6 6 3 3 3 2 3 3 3 3 4 4 4 4 U NIVERSIT ÄT B ERN I NFORMATIK VORLESUNG EI Flood_Fill(2,1) Flood_Fill(3,1) Flood_Fill(1,1) Flood_Fill(2,2) Flood_Fill(2,0) Flood_Fill(0,1) Flood_Fill(1,2) Flood_Fill(1,0) Flood_Fill(2,4) Flood_Fill(2,2) T YP UL B LATT 2 (5/5) AUSGABE HS 08 5 6 6 6 6 5 5 5 2 2 (b) Die maximale Rekursions- bzw. Stacktiefe beträgt 6. (c) Beurteilung des Algorithmus: Es handelt sich um einen höchst ineffizienten Algorithmus. Für nur 9 Punkte braucht man schon einen Stack der Tiefe 6 und es kommt zu 37 Funktionsaufrufe. Der Algorithmus füllt i. Allg. nur dann die ganze Figur, wenn die Stacktiefe nicht begrenzt wird. Wird diese aber nicht begrenzt, so kann es zu einem Speicherüberlauf kommen. Es gibt auch Varianten des Flood-Fill-Algorithmus, die die Stacktiefe begrenzen und im Fall von Löchern den Algorithmus in diesen Löchern wieder starten. Das Auffinden dieser Löcher ist aber schwierig.