Zwei Wege zum selben Ergebnis
Aus Kapitel 2 kennst du die Summe einer Reihung mit einer Schleife. Dieselbe Aufgabe lässt sich rekursiv lösen: Die Summe ab Index i ist a[i] plus die Summe ab i + 1. Aufruf: summe(a) bzw. summe(a, 0).
static int summe(int[] a) {
int s = 0;
for (int i = 0; i < a.length; i++) {
s = s + a[i];
}
return s;
}
iterativ — mit Schleife
static int summe(int[] a, int i) {
if (i == a.length) {
return 0;
}
return a[i] + summe(a, i + 1);
}
rekursiv — mit Selbstaufruf
- Schleifenvariable:wird zum Parameter:
iwird im Selbstaufruf alsi + 1weitergegeben. - Schleifenbedingung:wird verneint zur Abbruchbedingung: Die Schleife läuft, solange
i < a.length; die Rekursion endet beii == a.length. - Schleifenkörper:wird zum Rekursionsschritt:
a[i]wird mit dem Ergebnis für den Rest verknüpft. - Hilfsvariable:
sentfällt — das Zwischenergebnis steckt im Rückgabewert. - Endrekursion:ist der Selbstaufruf die letzte Aktion (
return f(…);wie beiggT), lässt sich die Methode direkt als Schleife schreiben. - Gleich mächtig:jede Schleife lässt sich als Rekursion schreiben und jede Rekursion als Schleife — notfalls mit einem selbst verwalteten Stapel.
Aufwand vergleichen
static long fibIter(int n) {
long a = 0; // fib(0)
long b = 1; // fib(1)
for (int i = 0; i < n; i++) {
long neu = a + b;
a = b;
b = neu;
}
return a;
}
Fibonacci iterativ: zwei Variablen wandern die Folge entlang, fibIter(10) liefert 55.
- Laufzeit:
fibaus 3.1.2 berechnet dieselben Teilprobleme immer wieder — die Zahl der Aufrufe wächst exponentiell;fibIterbraucht \(n\) Durchläufe. - Speicher:die Rekursion braucht einen Rahmen je offener Ebene (Tiefe \(n\)), die Schleife nur den einen Rahmen der Methode.
- Lesbarkeit:die rekursive Fassung steht näher an der Definition und ist oft kürzer und leichter zu prüfen.
Stelle mit dem Regler \(n\) ein und vergleiche, wie viele Aufrufe fib(n) braucht und wie viele Durchläufe fibIter(n). Der zweite Regler legt fest, wie viele Aufrufe der Rechner pro Sekunde schafft. Schalte zwischen logarithmischer und linearer Skala um; ▶ lässt \(n\) wachsen.
Halte fest: Jedes zusätzliche \(n\) vervielfacht die Aufrufe von fib etwa um den Faktor 1,6 — bei fibIter kommt nur ein Durchlauf dazu. Ein schnellerer Rechner verschiebt die Grenze kaum.
Aufrufe von fib(n) wie in 3.1.2, mit \(A(0) = A(1) = 1\).
Weil \(A(n-1) \ge A(n-2)\) ist, sind die beiden Teilbäume zusammen mindestens doppelt so groß wie der kleinere.
Dieselbe Abschätzung für \(A(n-2)\) eingesetzt.
Nach \(k\) Wiederholungen, solange \(n - 2k \ge 0\) ist.
Mit \(k = \lfloor n/2 \rfloor\) und \(A(n-2k) \ge 1\): mindestens exponentielles Wachstum. Für \(n = 30\) mehr als 32 768 Aufrufe — tatsächlich sind es 2 692 537.
Rekursion und Iteration sind gleich mächtig · Rekursion: nah an der Definition, braucht Stapelspeicher je offener Ebene · Baumrekursion mit sich wiederholenden Teilproblemen wächst exponentiell — dann iterativ lösen
Allgemeine Hinweise
Endrekursion erkennen
Steht return f(…); als letzte Aktion, ist nach der Rückkehr nichts mehr zu tun. Dann werden die Parameter einfach zu Variablen, die in einer Schleife neu belegt werden.
Tiefe begrenzt
Java verkraftet einige tausend bis zehntausend offene Aufrufe. summe(a, 0) für eine Reihung mit einer Million Einträgen endet mit StackOverflowError — die Schleife nicht.
Merken statt neu rechnen
Speichert man schon berechnete Werte in einer Reihung und schlägt sie vor jedem Aufruf nach, braucht auch das rekursive fib nur noch etwa \(2n\) Aufrufe.
