(/) Lösungsvorschlag Serie Rekursion. 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: fib(0) = fib() = fib(n) = fib(n ) + fib(n ) 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): // Output: Fibonacci Zahl von n // Zweck : Rekursive Berechnung der Fibonacci-Zahl if n < then return return (fib(n-) + fib(n-)) (b) Wie sich die Rekursion zur Laufzeit entfaltet, lässt sich Abbildung 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()), deren Resultat eigentlich bereits ausgerechnet wurde. Es werden also sehr viele redundante Berechnungen vorgenommen.
(/) fib() fib() fib() fib(0) 8 fib() fib(4) fib() fib() fib() fib() fib() fib(0) fib() fib(0) fib() fib(6) fib() fib() fib(4) fib() fib() fib() fib() fib(0) fib(0) Abbildung : Dynamische Entfaltung der Rekursion fib(6).
(/) (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, a, a): // Output : Fibonacci Zahl von n // Zweck : Iterative Berechnung der Fibonacci-Zahl if n==0 return a linrecfib(n-, a, a+a) Dieser Algorithmus ist gleich effizient wie der nachfolgend vorgestellte iterative Algorithmus. fib(n) als imperativer Algorithmus (iterativ) int Algorithm iterativfib(n): // Output : Fibonacci Zahl von n // Zweck : Iterative Berechnung der Fibonacci-Zahl a = b = result = for (i=; 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 84 eine Funktion gefunden, mit welcher sich eine beliebige Fibonacci-Zahl explizit berechnen lässt: f(n) = ( + ) n+ ( ) n+. (a) Der rekursive Algorithmus zur Berechnung von sum(n) kann wie folgt aussehen: Algorithm sum(n): // Input : ganze Zahl n>= // Output: Summe von bis n if n== then return return n + sum(n-) (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).
(4/) (c) Der iterative Algorithmus zur Berechnung von sum(n) kann wie folgt aussehen: int Algorithm sum(n): // Input : ganze Zahl n>= // Output: Summe von bis n sum = 0 for i= to n do sum = sum + i return sum. (a) Die Lösung ist in Abbildung dargestellt. y 4 0 6 7 8 4 0 0 4 x Abbildung : 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(,) Flood_Fill(,) Flood_Fill(4,) Flood_Fill(,) 4 Flood_Fill(,) 4 Flood_Fill(4,4) 4 Flood_Fill(4,) 4 Flood_Fill(,) Flood_Fill(,) Flood_Fill(4,) Flood_Fill(4,) Flood_Fill(,) 6 Flood_Fill(,) 6 Flood_Fill(4,) 6 Flood_Fill(4,0) 6 Flood_Fill(,) Flood_Fill(,4) Flood_Fill(,) Flood_Fill(,) Flood_Fill(,) Flood_Fill(0,) Flood_Fill(,4) Flood_Fill(,) Flood_Fill(,) 4 Flood_Fill(0,) 4 Flood_Fill(,) 4 Flood_Fill(,) 4
(/) Flood_Fill(,) Flood_Fill(,) 6 Flood_Fill(,) 6 Flood_Fill(,) 6 Flood_Fill(,0) 6 Flood_Fill(0,) Flood_Fill(,) Flood_Fill(,0) Flood_Fill(,4) Flood_Fill(,) (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 7 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.