MINT lernen

Rekursive Aufrufe verfolgen

Wer merkt sich eigentlich, wo jeder der vielen offenen Aufrufe gerade stehen geblieben ist?

1

Der Aufrufstapel

Die Fakultät \(n! = n \cdot (n-1) \cdot \ldots \cdot 1\) lässt sich rekursiv berechnen: \(n! = n \cdot (n-1)!\) und \(1! = 1\). Doch während fakultaet(4) auf fakultaet(3) wartet, muss sich das Programm merken, dass danach noch mit 4 zu multiplizieren ist.

static long fakultaet(int n) {
    if (n <= 1) {
        return 1;
    }
    return n * fakultaet(n - 1);
}
  • Rahmen:jeder Aufruf bekommt einen eigenen Speicherbereich mit seinen Parametern, lokalen Variablen und der Stelle, an der es nach der Rückkehr weitergeht.
  • Aufrufstapel:die Rahmen liegen übereinander; nur der oberste arbeitet, alle darunter warten.
  • Abstieg:jeder Selbstaufruf legt einen neuen Rahmen oben auf — bis die Abbruchbedingung greift.
  • Aufstieg:return entfernt den obersten Rahmen und reicht den Wert an den wartenden Aufruf darunter.
  • Rekursionstiefe:größte Zahl gleichzeitig offener Aufrufe; bei fakultaet(4) sind es 4.
SchrittAufrufStapel (oben rechts)Aktion
1fakultaet(4)f(4)wartet auf 4 · fakultaet(3)
2fakultaet(3)f(4) f(3)wartet auf 3 · fakultaet(2)
3fakultaet(2)f(4) f(3) f(2)wartet auf 2 · fakultaet(1)
4fakultaet(1)f(4) f(3) f(2) f(1)Abbruch: gibt 1 zurück
5← fakultaet(2)f(4) f(3) f(2)2 · 1 = 2 zurück
6← fakultaet(3)f(4) f(3)3 · 2 = 6 zurück
7← fakultaet(4)f(4)4 · 6 = 24 zurück

Drei andere Methoden, derselbe Mechanismus: Wähle oben eine Methode und gehe mit „Schritt vor“ (oder → auf der Bühne) durch die Aufrufe. Beobachte, wie der Stapel beim Abstieg wächst und beim Aufstieg Rahmen für Rahmen abgebaut wird. ▶ spielt alles ab.

Aufrufstapel auf- und abbauen

Halte fest: Der zuletzt begonnene Aufruf wird als erster beendet. Beim Abstieg wird nur gewartet, gerechnet wird erst beim Aufstieg — außer bei ggT, wo der Wert unverändert nach unten durchgereicht wird.

2

Aufrufbaum bei zwei Selbstaufrufen

static int fib(int n) {
    if (n <= 1) {
        return n;
    }
    return fib(n - 1) + fib(n - 2);
}
  • Lineare Rekursion:ein Selbstaufruf je Aufruf — die Aufrufe bilden eine Kette.
  • Baumrekursion:zwei oder mehr Selbstaufrufe — die Aufrufe bilden einen Aufrufbaum.
  • Reihenfolge:Java wertet fib(n - 1) vollständig aus, bevor fib(n - 2) beginnt: Der Baum wird von links nach rechts in die Tiefe durchlaufen.
  • Tiefe ≠ Anzahl:der Stapel ist höchstens so hoch wie der längste Ast, der Baum hat aber viel mehr Knoten.
fib(4)1→ 3fib(3)2→ 2fib(2)3→ 1fib(1)4→ 1fib(0)5→ 0fib(1)6→ 1fib(2)7→ 1fib(1)8→ 1fib(0)9→ 0
Aufrufbaum von fib(4): orange die Reihenfolge der Aufrufe, grün die Rückgabewerte, grün hinterlegt die Abbruchfälle.
Herleitung:
\(A(n) = 1 + A(n-1) + A(n-2)\)
Ansatz

\(A(n)\) zählt die Aufrufe bei fib(n): der Aufruf selbst plus alle Aufrufe der beiden Teilbäume. Für \(n \le 1\) gilt \(A(n) = 1\).

\(A(2) = 1 + 1 + 1 = 3\)
einsetzen

Die beiden Teilbäume von fib(2) sind Abbruchfälle mit je einem Aufruf.

\(A(3) = 1 + 3 + 1 = 5\)
einsetzen

Linker Teilbaum fib(2) mit 3 Aufrufen, rechter Teilbaum fib(1) mit einem.

\(A(4) = 1 + 5 + 3 = 9\)
Ergebnis

Neun Aufrufe — genau die neun Knoten im Baum. Die Rekursionstiefe ist dagegen nur 4.

Merke

Abstieg legt Rahmen auf den Aufrufstapel, Aufstieg nimmt sie in umgekehrter Reihenfolge ab · Rekursionstiefe = größte Stapelhöhe · bei Baumrekursion: Anzahl der Aufrufe = Knoten im Aufrufbaum

3

Allgemeine Hinweise

Einrücken beim Mitschreiben

Rücke jeden neuen Aufruf eine Stufe weiter ein und jede Rückgabe wieder aus. So siehst du sofort, welcher Aufruf auf welchen wartet — die Einrückung ist die Stapelhöhe.

Jeder Rahmen hat sein eigenes n

Die Parameter gehören zum jeweiligen Aufruf. Wird im inneren Aufruf n kleiner, bleibt das n der wartenden Aufrufe unverändert.

Gerechnet wird beim Rückweg

In return n * fakultaet(n - 1); wird erst multipliziert, wenn der innere Aufruf zurückkehrt. Wer beim Abstieg schon Zwischenergebnisse notiert, verrechnet sich leicht.

Videos