Algorithmen & Programmierung Rekursive Funktionen (1) Berechnung der Fakultät Fakultät Die Fakultät N! einer nichtnegativen ganzen Zahl N kann folgendermaßen definiert werden: d.h. zur Berechnung werden alle Zahlen von 1 bis N miteinander multipliziert: 1·2·3·...·N Fakultätsberechnung in C int fak_iterativ(int N) { int fk = 1; // = Fakultät von 0 und Fakultät von 1 for (int i = 2; i <= N; i++) fk = fk * i; return fk; } Dr. Frank Seifert Vorlesung Algorithmen & Programmierung WS 2015/2016 461 Berechnung der Fakultät Alternative Die Fakultät kann auch wie folgt definiert werden: d.h. zur Bestimmung der Fakultät wird wiederum die Fakultät benötigt. Rekursion Der Begriff Rekursion bezeichnet die Definition einer Funktion durch sich selbst. Konsequenz für rekursive Algorithmen Damit ein rekursiver Algorithmus terminieren kann, muss es eine Abbruchbedingung für die Rekursion geben und damit diese eintreten kann, müssen der bzw. die Funktionsparameter bei jedem rekursiven Funktionsaufruf geändert werden. Eine unendliche Wiederholung eines rekursiven Algorithmus ist praktisch unmöglich. Dr. Frank Seifert Vorlesung Algorithmen & Programmierung WS 2015/2016 462 Abarbeitung rekursiver Funktionen Funktionswert Jeder (rekursive) Funktionsaufruf berechnet (irgendwann) einen individuellen Funktionswert, der von den jeweils gültigen Parametern bzw. Variablenwerten zum Aufrufzeitpunkt abhängt. Rekursiver Abstieg Zunächst bewirkt ein rekursiver Funktionsaufruf allerdings solange weitere rekursive Funktionsaufrufe (mit jeweils neuen Parametern) bis die Abbruchbedingung erreicht ist. Dr. Frank Seifert Vorlesung Algorithmen & Programmierung WS 2015/2016 463 Abarbeitung rekursiver Funktionen Rekursiver Aufstieg Ein konkretes Funktionsergebnis wird zum ersten Mal bei Erreichen des Abbruchkriteriums berechnet und zurückgegeben. Mit Hilfe dieses Funktionswerts wird nun im Rahmen des vorletzten rekursiven Funktionsaufrufs ein konkreter Funktionswert berechnet und an den drittletzten Funktionsaufruf weitergereicht usw. … Konsequenz Für jeden Funktionsaufruf müssen alle Funktionsvariablen und Parameter gespeichert werden! Dr. Frank Seifert Vorlesung Algorithmen & Programmierung WS 2015/2016 464 Abarbeitung rekursiver Funktionen Stack Bei jedem Funktionsaufruf (egal ob rekursiv oder nicht) werden alle Variablen und Parameter einer Funktion sowie die Rücksprungadresse und weitere Verwaltungsinformationen in einer Datenstruktur namens Stack (Stapel) abgelegt, die durch das Laufzeitsystem verwaltet wird. ➔ Details in Vorlesung Datenstrukturen Nach der vollständigen Durchführung einer Funktion wird der zu dieser Funktion korrespondierende Teil des Stacks wieder gelöscht und ggf. nur das Funktionsergebnis auf dem Stack abgelegt, um es einer aufrufenden Funktion zur Verfügung stellen zu können. Dr. Frank Seifert Vorlesung Algorithmen & Programmierung WS 2015/2016 465 Abarbeitung rekursiver Funktionen Rekursive Funktionen Ruft sich eine Funktion selbst auf, werden für jeden Aufruf die entsprechenden Funktionsparameter auf dem Stack abgelegt, der deshalb proportional zur Rekursionstiefe wächst. (Deshalb sind unendliche Rekursionstiefen unmöglich) Nach dem Erreichen der Abbruchbedingung werden die Berechnungen der Funktionsrümpfe in der umgekehrten Reihenfolge ihres Aufrufs zu Ende gebracht und die entsprechenden Teile des Stacks gelöscht, der dadurch wieder schrumpft. Dr. Frank Seifert Vorlesung Algorithmen & Programmierung WS 2015/2016 466 Abarbeitung rekursiver Funktionen int f(int N) { if (N == 0) return 1; return N * f(N - 1); } int main() { int v = 0; v = f(5); printf("5! ist %d", v); return 0; } Dr. Frank Seifert Vorlesung Algorithmen & Programmierung WS 2015/2016 467 4 3 2 1 Abstieg der Rekursion - Stack wächst 5 0 Dr. Frank Seifert Vorlesung Algorithmen & Programmierung WS 2015/2016 468 24 6 2 1 1 Dr. Frank Seifert Vorlesung Algorithmen & Programmierung WS 2015/2016 Aufstieg der Rekursion - Stack schrumpft 120 469 Formulierung rekursiver Algorithmen Rekursion als Problemlösungsstrategie Ein Algorithmus zur Lösung eines Problems muss so formuliert werden, dass die (mehrfache) Selbstanwendung des Algorithmus auf seine Ergebnisse die Lösung des Problems liefern kann. Dazu formuliert man in der Regel eine Menge elementarer Operationen, die in jedem Rekursionsschritt mit genau einem Element der Problemdatenmenge ausgeführt werden können. Anschließend wird die Problemdatenmenge verkleinert und der Algorithmus auf der verkleinerten Problemdatenmenge wieder angewendet, bis die Lösung des Problems gefunden wurde. Definition eines Abbruchkriteriums Meist wird bei der Verkleinerung der Problemdatenmenge irgendwann ein besonderer Zustand (Abbruchkriterium) erreicht, z.B. beim Erreichen einer leeren Problemdatenmenge. Jeder Rekursionsschritt muss Bestandteile des Abbruchkriteriums modifizieren, so dass dieses erreicht werden kann. Dr. Frank Seifert Vorlesung Algorithmen & Programmierung WS 2015/2016 470 Formulierung rekursiver Algorithmen Beispiel 1 Addition aller Elemente eines Feldes Iterative Lösung Die Summe aller Feldelemente lässt sich durch Iteration über alle Feldelemente bei gleichzeitiger Addition des jeweils aktuellen Feldelements zu einer Summenvariablen bestimmen. Rekursive Lösung Die Summe aller Elemente eines Feldes lässt sich durch Addition des ersten Feldelements mit der Summe des Restfeldes bestimmen. Abbruchkriterium • Das Restfeld enthält nur noch ein Element. Dr. Frank Seifert Vorlesung Algorithmen & Programmierung WS 2015/2016 471 Formulierung rekursiver Algorithmen Beispiel 2 Einlesen einer Zeichenfolge von der Tastatur und Ausgabe in umgekehrter Reihenfolge Iterative Lösung • Bereitstellen eines ausreichend großen Feldes • Solange gelesenes Zeichen c ungleich '\n' lege c im Feld ab • Iteriere vom letzten zum ersten Feldelement mit Ausgabe des jeweiligen Feldelements Rekursive Lösung • Lies Zeichen c ein und rufe Funktion wieder auf • Gib c nach dem rekursiven Funktionaufruf aus • Abbruchkriterium: c == '\n' Dr. Frank Seifert Vorlesung Algorithmen & Programmierung WS 2015/2016 472 Formulierung rekursiver Algorithmen Beispiel 2 Einlesen einer Zeichenfolge von der Tastatur und Ausgabe in umgekehrter Reihenfolge Vergleich der Varianten Offene Frage bei der iterativen Variante • Wie stellen wir ein ausreichend großes Feld bereit? Rekursion bietet eine implizite dynamische Speichermöglichkeit für nahezu beliebig viele Werte. Felder stellen eine explizite Speichermöglichkeit mit im Vorhinein bekannter Elementanzahl dar. Theoretisch bräuchten wir zur (iterativen) Lösung der Aufgabe ein dynamisches Feld, dessen Elementanzahl in Abhängigkeit von der gelesenen Zeichenanzahl schrumpfen und wachsen kann ➔ Vorlesung Datenstrukturen • Rekursion realisiert dynamische Größe • Rekursiver Algorithmus ist zudem kürzer Dr. Frank Seifert Vorlesung Algorithmen & Programmierung WS 2015/2016 473 Formulierung rekursiver Algorithmen Beispiel 3 Ausgabe einer Dezimalzahl als Dualzahl Iterative Lösung • benötigt Feld zum Ablegen der Binärziffern, da die Berechnung „umgekehrt“ erfolgt • Größe des benötigten Feldes lässt im Vorhinein bestimmen • Algorithmus siehe Übung Rekursive Lösung • Rufe Funktion rekursiv mit halbiertem Argument auf • Gib Rest der Division des Arguments durch 2 aus • Abbruchkriterium: Argument ≤ 1 Dr. Frank Seifert Vorlesung Algorithmen & Programmierung WS 2015/2016 474 Ende der Vorlesung