MINT lernen

Rekursion und Iteration

Jede Schleife lässt sich als Rekursion schreiben — aber lohnt sich das auch?

1

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: i wird im Selbstaufruf als i + 1 weitergegeben.
  • Schleifenbedingung:wird verneint zur Abbruchbedingung: Die Schleife läuft, solange i < a.length; die Rekursion endet bei i == a.length.
  • Schleifenkörper:wird zum Rekursionsschritt: a[i] wird mit dem Ergebnis für den Rest verknüpft.
  • Hilfsvariable:s entfällt — das Zwischenergebnis steckt im Rückgabewert.
  • Endrekursion:ist der Selbstaufruf die letzte Aktion (return f(…); wie bei ggT), 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.
2

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:fib aus 3.1.2 berechnet dieselben Teilprobleme immer wieder — die Zahl der Aufrufe wächst exponentiell; fibIter braucht \(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.

Aufrufe zählen

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.

Herleitung:
\(A(n) = 1 + A(n-1) + A(n-2)\)
Ansatz

Aufrufe von fib(n) wie in 3.1.2, mit \(A(0) = A(1) = 1\).

\(A(n) > 2 \cdot A(n-2)\)
abschätzen

Weil \(A(n-1) \ge A(n-2)\) ist, sind die beiden Teilbäume zusammen mindestens doppelt so groß wie der kleinere.

\(A(n) > 2^2 \cdot A(n-4)\)
wiederholen

Dieselbe Abschätzung für \(A(n-2)\) eingesetzt.

\(A(n) > 2^k \cdot A(n-2k)\)
\(k\)-mal

Nach \(k\) Wiederholungen, solange \(n - 2k \ge 0\) ist.

\(A(n) > 2^{\lfloor n/2 \rfloor}\)
Ergebnis

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.

Merke

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

3

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.

Videos