Lektion 2.2

Rekursion

20 min Lesezeit

Du suchst eine Datei und weißt nur, dass sie irgendwo in einem Ordner liegt. Du öffnest den Ordner und siehst Dateien und Unterordner. In jedem Unterordner stellt sich dieselbe Aufgabe noch einmal, nur kleiner. Bei einer russischen Matrjoschka ist es ähnlich: Jede Puppe enthält eine kleinere, bis die kleinste keine mehr enthält.

Beim Programmieren heißt dieses Vorgehen Rekursion: Eine Funktion löst ein Problem, indem sie sich für ein kleineres Problem selbst aufruft. Du lernst, wie man sie aufbaut, wie der Rechner sie ausführt und wann man sie einsetzt.

Definition

Rekursionsschritt und Abbruchbedingung

Eine Funktion ist rekursiv, wenn sie sich selbst aufruft. Sie braucht zwei Teile. Die Abbruchbedingung (Basisfall) löst den kleinsten Fall direkt, ohne weiteren Aufruf. Der Rekursionsschritt führt das Problem auf ein kleineres zurück und ruft dafür die Funktion selbst auf. Jeder Aufruf muss dem Basisfall näherkommen, sonst endet die Rekursion nie. Das Standardbeispiel ist die Fakultät:

ZeileEinrückungAnweisung
10def fak(n):\texttt{def fak(n):}
21if n == 0:\texttt{if n == 0:}
32return 1\texttt{return 1}
41return n * fak(n - 1)\texttt{return n * fak(n - 1)}
n!={1n=0n⋅(n−1)!n≥1n! = \begin{cases} 1 & n = 0 \\ n \cdot (n-1)! & n \geq 1 \end{cases}
Definition

Aufrufstapel

Bei jedem Aufruf legt der Rechner einen Block auf den Aufrufstapel: Er enthält die Parameter, die lokalen Variablen und die Stelle, an der es nach dem Aufruf weitergeht. Der Stapel arbeitet wie ein Tellerstapel, es wird oben abgelegt und oben entfernt. Endet ein Aufruf, wird sein Block entfernt, und der Aufrufer macht weiter. Der Stapel hat begrenzte Größe: Zu viele verschachtelte Aufrufe führen zum Stapelüberlauf, in Python zum Fehler RecursionError (Grenze standardmäßig etwa 1000 Aufrufe).

Beispiel

Fakultät und Aufrufstapel

Wie wird fak(3)\texttt{fak(3)} ausgeführt? Zeige in jedem Schritt den Aufrufstapel, der älteste Aufruf steht zuerst.

SchrittEreignisAufrufstapel
1fak(3) beginntfak(3)
23 ist nicht 0, Aufruf fak(2)fak(3), fak(2)
3Aufruf fak(1)fak(3), fak(2), fak(1)
4Aufruf fak(0)fak(3), fak(2), fak(1), fak(0)
5fak(0) gibt 1 zurückfak(3), fak(2), fak(1)
6fak(1) gibt 1 * 1 = 1 zurückfak(3), fak(2)
7fak(2) gibt 2 * 1 = 2 zurückfak(3)
8fak(3) gibt 3 * 2 = 6 zurückleer

Die Multiplikationen geschehen erst beim Zurückkehren, in umgekehrter Reihenfolge der Aufrufe. Es gilt 3!=63! = 6.

Definition

Lineare und verzweigte Rekursion

Bei linearer Rekursion ruft sich die Funktion pro Durchlauf höchstens einmal selbst auf, wie fak\texttt{fak}. Bei verzweigter Rekursion ruft sie sich mehrfach auf, und die Aufrufe bilden einen Baum. Beispiel sind die Fibonacci-Zahlen 0, 1, 1, 2, 3, 5, 8, ...: Jede ist die Summe der beiden vorigen.

ZeileEinrückungAnweisung
10def fib(n):\texttt{def fib(n):}
21if n < 2:\texttt{if n < 2:}
32return n\texttt{return n}
41return fib(n - 1) + fib(n - 2)\texttt{return fib(n - 1) + fib(n - 2)}

Die Funktion hat zwei Abbruchfälle (n=0n = 0 und n=1n = 1) und zwei rekursive Aufrufe. Die Zahl der Aufrufe wächst schnell: fib(5)\texttt{fib(5)} braucht 15 Aufrufe, fib(30)\texttt{fib(30)} etwa 2,7 Millionen, weil dieselben Teilwerte immer wieder berechnet werden.

Beispiel

Der Aufrufbaum von fib(4)

Wie oft wird jeder Funktionsaufruf ausgeführt, wenn fib(4)\texttt{fib(4)} berechnet wird?

fib(4)\texttt{fib(4)} ruft fib(3)\texttt{fib(3)} und fib(2)\texttt{fib(2)} auf. fib(3)\texttt{fib(3)} ruft fib(2)\texttt{fib(2)} und fib(1)\texttt{fib(1)} auf. Jedes fib(2)\texttt{fib(2)} ruft fib(1)\texttt{fib(1)} und fib(0)\texttt{fib(0)} auf.

AufrufAnzahl
fib(4)1
fib(3)1
fib(2)2
fib(1)3
fib(0)2

Das sind 1 + 1 + 2 + 3 + 2 = 9 Aufrufe. Das Ergebnis ist fib(4)\texttt{fib(4)} = 3. Der Wert fib(2) wird zweimal berechnet, obwohl einmal genügt.

Definition

Divide and Conquer

Divide and Conquer (teile und herrsche) zerlegt ein Problem in kleinere, gleichartige Teilprobleme, löst sie rekursiv und setzt die Teillösungen zur Gesamtlösung zusammen. Die Phasen heißen Teilen, Lösen und Zusammenführen. Mergesort, Quicksort und die binäre Suche arbeiten so. Die Funktion maxi\texttt{maxi} liefert das größte Element von l[lo]\texttt{l[lo]} bis l[hi]\texttt{l[hi]}:

ZeileEinrückungAnweisung
10def maxi(l, lo, hi):\texttt{def maxi(l, lo, hi):}
21if lo == hi:\texttt{if lo == hi:}
32return l[lo]\texttt{return l[lo]}
41mitte = (lo + hi) // 2\texttt{mitte = (lo + hi) // 2}
51a = maxi(l, lo, mitte)\texttt{a = maxi(l, lo, mitte)}
61b = maxi(l, mitte + 1, hi)\texttt{b = maxi(l, mitte + 1, hi)}
71if a > b:\texttt{if a > b:}
82return a\texttt{return a}
91return b\texttt{return b}
Beispiel

Maximum durch Halbieren

Was liefert maxi([4, 9, 2, 7], 0, 3)\texttt{maxi([4, 9, 2, 7], 0, 3)}, und wie viele Aufrufe gibt es?

Zeile 4 teilt den Bereich 0 bis 3 bei mitte = 1. Der linke Teil 0 bis 1 teilt sich bei mitte = 0 in die Einzelfälle 4 und 9, der Vergleich 4 > 9 ist falsch, und er liefert 9. Der rechte Teil 2 bis 3 teilt sich in 2 und 7 und liefert 7. Zusammengeführt: 9 > 7, das Ergebnis ist 9.

Aufrufe gibt es 1 für den ganzen Bereich, 2 für die Hälften und 4 für die Einzelfälle, zusammen 7. Die Rekursionstiefe, also die Zahl der übereinander liegenden Aufrufe, ist 3.

Definition

Rekursion und Iteration im Vergleich

Jede rekursive Funktion lässt sich durch eine Schleife ersetzen, notfalls mit einem selbst verwalteten Stapel, und umgekehrt. Die Quersumme aus der Lektion Algorithmik und Programmierung geht rekursiv so: def quersumme(n):\texttt{def quersumme(n):}, dann bei n < 10\texttt{n < 10} den Wert n\texttt{n} zurückgeben, sonst return n % 10 + quersumme(n // 10)\texttt{return n \% 10 + quersumme(n // 10)}. Für 4729 ergibt das 9 + 2 + 7 + 4 = 22.

Kriteriumrekursiviterativ
Aufbauoft kürzer und näher an der Definition, besonders bei Teilen und Herrschen und Baumstrukturenoft länger
Speicherein Block auf dem Aufrufstapel je Aufruf, Tiefe begrenztmeist konstant
LaufzeitAufrufe kosten Zeit, naive verzweigte Rekursion wiederholt Teilarbeit (fib)oft schneller

Die iterative Fibonacci-Funktion merkt sich die letzten beiden Werte und braucht nur nn Durchläufe.

Definition

Backtracking

Backtracking (Zurückverfolgen) löst Suchprobleme, indem es eine Teillösung Schritt für Schritt aufbaut. Lässt sie sich nicht mehr zu einer Lösung erweitern (Sackgasse), nimmt es den letzten Schritt zurück und probiert die nächste Möglichkeit. Das Zurücknehmen übernimmt der Aufrufstapel: Kehrt ein Aufruf ohne Erfolg zurück, macht der Aufrufer mit seiner nächsten Wahl weiter. Beispiel ist das n-Damen-Problem: n Damen sollen so auf ein Brett mit n mal n Feldern, dass keine eine andere schlägt. Eine Dame schlägt in ihrer Zeile, Spalte und beiden Diagonalen. Man setzt Zeile für Zeile eine Dame:

ZeileEinrückungAnweisung
10versuche(zeile):\texttt{versuche(zeile):}
21wenn zeile > n: gib wahr zuru¨ck\texttt{wenn zeile > n: gib wahr zurück}
31fu¨r jede spalte von 1 bis n:\texttt{für jede spalte von 1 bis n:}
42wenn die Dame auf (zeile, spalte) erlaubt ist:\texttt{wenn die Dame auf (zeile, spalte) erlaubt ist:}
53setze die Dame\texttt{setze die Dame}
63wenn versuche(zeile + 1): gib wahr zuru¨ck\texttt{wenn versuche(zeile + 1): gib wahr zurück}
73nimm die Dame zuru¨ck\texttt{nimm die Dame zurück}
81gib falsch zuru¨ck\texttt{gib falsch zurück}
Beispiel

Vier Damen

Wie findet Backtracking eine Lösung für n = 4? Ein Feld heißt (Zeile, Spalte). Die Dame der Zeile 1 beginnt in Spalte 1.

  • Dame auf (1,1). In Zeile 2 sind Spalte 1 (gleiche Spalte) und Spalte 2 (Diagonale) bedroht, Spalte 3 ist frei: Dame auf (2,3).
  • Zeile 3 hat kein freies Feld: Spalte 1 und 3 liegen in bedrohten Spalten, Spalte 2 und 4 auf Diagonalen der Dame (2,3). Sackgasse, zurück zu Zeile 2: nächste freie Spalte ist 4, Dame auf (2,4).
  • Zeile 3: Spalte 2 ist frei, Dame auf (3,2). In Zeile 4 ist nichts frei: Spalte 1, 2 und 4 sind bedroht, Spalte 3 liegt auf einer Diagonalen der Dame (3,2). Zurück zu Zeile 3: Spalte 3 liegt auf einer Diagonalen der Dame (2,4), Spalte 4 in ihrer Spalte. Zurück zu Zeile 2: keine weitere Spalte. Zurück zu Zeile 1.
  • Dame auf (1,2). Zeile 2: Spalte 4 ist die erste freie. Zeile 3: Spalte 1. Zeile 4: Spalte 3. Alle Zeilen sind besetzt.

Lösung: (1,2), (2,4), (3,1), (4,3). Keine zwei Damen stehen in derselben Spalte oder auf derselben Diagonalen.

Häufige Fehler

Ohne Abbruchbedingung läuft es ewig

„Fehlt die Abbruchbedingung, läuft die Rekursion endlos weiter.“

Richtig ist

Jeder Aufruf belegt einen Block auf dem Aufrufstapel, und der ist begrenzt. In Python bricht das Programm nach etwa 1000 verschachtelten Aufrufen mit RecursionError ab. Auch mit Abbruchbedingung gilt: Der Rekursionsschritt muss das Problem verkleinern, sonst wird sie nie erreicht.

Häufige Fehler

Rekursion ist immer die bessere Lösung

„Rekursive Lösungen sind kürzer, also auch besser und schneller.“

Richtig ist

Kürzer sind sie oft, schneller nicht immer. fib(30)\texttt{fib(30)} braucht rekursiv etwa 2,7 Millionen Aufrufe, eine Schleife nur 30 Durchläufe. Rekursion lohnt sich, wenn das Problem selbst rekursiv gebaut ist, etwa bei Bäumen oder Teilen und Herrschen.

Zusammenfassung

  • Eine rekursive Funktion ruft sich selbst auf. Die Abbruchbedingung löst den kleinsten Fall, der Rekursionsschritt führt auf ein kleineres Problem zurück.

  • Der Aufrufstapel enthält für jeden offenen Aufruf einen Block. Zu tiefe Rekursion führt zum Stapelüberlauf.

  • Lineare Rekursion hat höchstens einen Selbstaufruf je Durchlauf, verzweigte hat mehrere und kann Teilarbeit wiederholen.

  • Divide and Conquer teilt, löst die Teile rekursiv und führt die Teillösungen zusammen.

  • Rekursion und Iteration sind gleich mächtig. Rekursion ist oft kürzer, Iteration spart Speicher und Aufrufe.

  • Backtracking baut Teillösungen auf und nimmt bei Sackgassen Schritte zurück.

Übungen

Aufgabe 1Numerisch

Welche Zahl liefert fak(6)\texttt{fak(6)}?

Aufgabe 2Numerisch

Eine Folge ist rekursiv definiert: h(0)=0h(0) = 0 und h(n)=2⋅h(n−1)+1h(n) = 2 \cdot h(n-1) + 1. So viele Züge braucht man in einem Knobelspiel mit n Scheiben. Berechne h(5)h(5).

Aufgabe 3Multiple Choice

Eine Funktion g ist nur durch g(n)=g(n−1)+1g(n) = g(n-1) + 1 definiert, ohne Abbruchbedingung. Was geschieht bei g(3)g(3)?

Aufgabe 4Numerisch

Wie viele Aufrufe von fib entstehen bei fib(5)\texttt{fib(5)}, den ersten Aufruf mitgezählt?

Aufgabe 5Freitext

Schreibe in Pseudocode oder Python eine rekursive Funktion potenz(b, n)\texttt{potenz(b, n)} für bnb^n mit ganzem n≥0n \geq 0. Nenne Abbruchbedingung und Rekursionsschritt und werte potenz(2, 3)\texttt{potenz(2, 3)} aus.