MINT üben

Rekursion und Iteration

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

1

Zwei Wege zum selben Ergebnis

Du kennst 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.
Merke

Rekursion statt Schleife: Die Schleifenvariable wird zum Parameter (i → i + 1).

Abbruchbedingung: Die Schleifenbedingung wird verneint: Schleife läuft solange i < a.length, Rekursion endet bei i == a.length.

Rekursionsschritt: Der Schleifenkörper verknüpft a[i] mit dem Ergebnis für den Rest; die Hilfsvariable entfällt.

Gleich mächtig: Jede Schleife lässt sich als Rekursion schreiben und jede Rekursion als Schleife. Endrekursion (Selbstaufruf als letzte Aktion) lässt sich direkt als Schleife schreiben.

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 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), 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

Gleich mächtig: Rekursion und Iteration lösen dieselben Probleme.

Laufzeit: Baumrekursion mit sich wiederholenden Teilproblemen (fib) wächst exponentiell — dann iterativ lösen; die Schleife braucht nur \(n\) Durchläufe.

Speicher: Rekursion braucht einen Rahmen je offener Ebene (Tiefe \(n\)), die Schleife nur einen.

Lesbarkeit: Rekursion steht näher an der Definition und ist oft kürzer.

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