Zum Inhalt springen
Zurück zur Themenübersicht

Praktische Informatik

Rekursion, Teile und herrsche, Rücksetzverfahren

Ein Verfahren, das sich selbst aufruft, und warum das kein Zirkelschluss ist, sondern ein Werkzeug.

Benötigte Grundlagen

Dieses Vorwissen brauchst du für das Kapitel. Schau kurz nach, wenn dir etwas davon nicht mehr präsent ist, sonst leg direkt los.

Einführung

„Um zu verstehen, was Rekursion ist, muss man zuerst verstehen, was Rekursion ist."

Der Witz trifft die Sache und verfehlt sie zugleich. Er trifft, weil sich eine rekursive Definition tatsächlich auf sich selbst beruft. Er verfehlt, weil ihm der entscheidende Teil fehlt: der Abbruch.

Ohne Abbruch wäre Rekursion ein Zirkelschluss. Mit Abbruch ist sie eines der mächtigsten Werkzeuge der Informatik, und sie löst Aufgaben, die mit kaum formulierbar wären.

Das kannst du nach diesem Kapitel

  • Basisfall und Rekursionsschritt unterscheiden und beide selbst aufstellen.

  • einen rekursiven Aufruf über den nachvollziehen.

  • erkennen, wann Rekursion einer überlegen ist und wann nicht.

  • das Prinzip Teile und herrsche auf ein neues Problem anwenden.

  • das Rücksetzverfahren an einem Beispiel erläutern (Vertiefung).

Kurz aufgefrischt

Vorausgesetzt werden mit Parametern und Rückgabewerten aus Programmieren: Variablen, Kontrollstrukturen, Unterprogramme, der aus Datentypen und Datenstrukturen und die aus Aufwand von Algorithmen.

Die zwei notwendigen Teile

Ein heißt rekursiv, wenn es sich selbst aufruft. Damit das kein endloser Rückgriff wird, braucht es zwingend zwei Teile:

Basisfall. Ein Fall, der ohne weiteren Aufruf beantwortet wird. Er ist der Boden. Rekursionsschritt. Der Aufruf auf ein kleineres Teilproblem, das dem Basisfall näher liegt.

Beide sind unverzichtbar. Fehlt der Basisfall, läuft die Rekursion endlos. Führt der Rekursionsschritt nicht näher an den Basisfall heran, ebenfalls.

Beispiel Fakultät. Die mathematische Definition ist bereits rekursiv:

0!=1n!=n⋅(n−1)!0! = 1 \qquad n! = n \cdot (n-1)!
fakultaet(n):
    wenn n = 0:
        gib 1 zurück              // Basisfall
    sonst:
        gib n * fakultaet(n-1)    // Rekursionsschritt

Die Umsetzung ist eine wörtliche Übersetzung der Definition, und genau das ist die Stärke der Rekursion: Wo ein Problem rekursiv definiert ist, ist die rekursive Lösung die natürliche.

Beide Teile, und warum keiner fehlen darf

fakultaet(n)n > 0?janeingib 1zurückgib n ·fakultaet(n−1)zurückErgebnis an denAufrufer

Im Bild stecken genau die zwei Teile, ohne die keine Rekursion funktioniert. Der Seitenzweig ist der Basisfall: Er beantwortet die Frage ohne weiteren Aufruf und ist damit der Boden, auf dem alles aufsetzt. Der gerade Weg ist der Rekursionsschritt, und entscheidend ist an ihm nicht, dass er sich selbst aufruft, sondern dass er es mit n−1n-1 tut, also mit einem Wert, der dem Basisfall näher liegt. Nimm einen der beiden weg, und das Bild zeigt sofort, was passiert: Ohne Seitenzweig gibt es keinen Ausgang, und ohne die Verkleinerung wird der Ausgang nie erreicht. Beides endet in derselben Endlosschleife. (Die Bedingung steht hier als n>0n > 0, im Quelltext daneben als n=0n = 0, dieselbe , nur von der anderen Seite gelesen.)

Wie der Rechner das abarbeitet

Bei jedem Aufruf legt der Rechner einen Eintrag auf den mit den lokalen Werten und der Stelle, an der es nach der Rückkehr weitergeht. Für fakultaet(4)\texttt{fakultaet(4)}:

fakultaet(4)  →  4 * fakultaet(3)         ↓ hinein
fakultaet(3)  →  3 * fakultaet(2)         ↓
fakultaet(2)  →  2 * fakultaet(1)         ↓
fakultaet(1)  →  1 * fakultaet(0)         ↓
fakultaet(0)  →  1                        ← Boden erreicht

fakultaet(1)  =  1 * 1  = 1               ↑ heraus
fakultaet(2)  =  2 * 1  = 2               ↑
fakultaet(3)  =  3 * 2  = 6               ↑
fakultaet(4)  =  4 * 6  = 24              ↑

🔴 Zwei Beobachtungen, die den größten Teil aller Missverständnisse ausräumen:

Erst geht es hinein, dann heraus. Die Multiplikationen finden nach dem tiefsten Aufruf statt, auf dem Rückweg. Wer glaubt, die Rechnung geschehe beim Hineingehen, kommt bei jeder Nachverfolgung durcheinander.

Der Stapel wächst mit der Tiefe. Jeder offene Aufruf belegt Speicher. Bei fakultaet(100000)\texttt{fakultaet(100000)} läuft der Aufrufstapel über, und das Programm bricht ab. Rekursionstiefe ist also nicht kostenlos, und das ist der wichtigste praktische Nachteil.

Wann Rekursion sinnvoll ist

Die Fakultät ist ein schlechtes Beispiel für den Nutzen der Rekursion, auch wenn sie ein gutes für ihr Prinzip ist. Eine leistet dasselbe, ist schneller und braucht keinen Stapelplatz.

Die Frage lautet deshalb: Wann lohnt sich Rekursion?

Wenn sich das Problem natürlich in gleichartige kleinere Teilprobleme zerlegt.

Typische Fälle:

  • Bäume und Verzeichnisse. Ein Ordner enthält Ordner, die Ordner enthalten. Eine Schleife müsste die Schachtelungstiefe kennen, die Rekursion nicht.
  • Teile und herrsche. Sortieren durch Mischen, binäre Suche.
  • Alle Möglichkeiten durchprobieren. Labyrinth, Damenproblem, Sudoku.

In all diesen Fällen ist die rekursive Fassung kürzer und verständlicher. Bei einfachen Wiederholungen dagegen ist die Schleife die bessere Wahl.

Ein Beispiel, das ohne Rekursion mühsam wäre

Die Türme von Hanoi: Ein von nn Scheiben soll von Stab A nach C, es darf nur eine Scheibe auf einmal bewegt werden, und nie eine größere auf eine kleinere.

Die rekursive Lösung besteht aus drei Zeilen:

bewege(n, von, nach, hilf):
    wenn n = 0: fertig                        // Basisfall
    bewege(n-1, von, hilf, nach)              // oberste n-1 auf den Hilfsstab
    verschiebe die unterste Scheibe von → nach
    bewege(n-1, hilf, nach, von)              // die n-1 wieder obendrauf

Der Gedanke dahinter ist bemerkenswert: Man löst das Problem, ohne es ganz zu durchdenken. Man nimmt an, dass der Fall n−1n-1 bereits gelöst ist, und beschreibt nur, wie man daraus den Fall nn gewinnt. Genau das ist die Denkweise der vollständigen Induktion.

Die Zahl der Züge ist 2n−12^n - 1. Bei 64 Scheiben, wie in der Legende, wären das über 1,8⋅10191{,}8 \cdot 10^{19} Züge; bei einem Zug pro Sekunde dauerte das länger als das bisherige Alter des Universums. Die Lösung ist also kurz und trotzdem exponentiell teuer.

Türme von Hanoi mit drei Scheiben

3: A→C2: A→B2: B→C1: A→C1: C→B1: B→A1: A→C

Jeder ist ein Aufruf, und jeder Aufruf bewegt genau eine Scheibe, die unterste seines . Die Beschriftung nennt, wie viele Scheiben er übernimmt und von welchem Stab zu welchem. Jetzt zähle: Der Baum hat sieben Knoten, also braucht die Lösung sieben Züge, und 23−1=72^3 - 1 = 7. Die Formel steht damit nicht als Behauptung daneben, sondern ist abzählbar. 🔴 Bemerkenswert ist, wie diese Lösung zustande kommt: Niemand hat sich sieben Züge ausgedacht. Man nimmt an, der Fall mit zwei Scheiben sei schon gelöst, und beschreibt nur, wie man daraus den Fall mit dreien gewinnt, obere zwei beiseite, unterste ans Ziel, obere zwei wieder darauf. Genau das ist die Denkweise der vollständigen Induktion. Bei 64 Scheiben ergäbe dieselbe Rechnung über 1,8⋅10191{,}8 \cdot 10^{19} Züge: Die Lösung ist kurz und trotzdem exponentiell teuer.

Wenn Rekursion teuer wird

Die Fibonacci-Folge lädt zur direkten Umsetzung ein:

f(0)=0,f(1)=1,f(n)=f(n−1)+f(n−2)f(0) = 0, \quad f(1) = 1, \quad f(n) = f(n-1) + f(n-2)
fib(n):
    wenn n ≤ 1: gib n zurück
    gib fib(n-1) + fib(n-2)

Kurz, richtig und katastrophal langsam. Der Grund ist die Mehrfachberechnung:

                fib(5)
              /        \
         fib(4)        fib(3)
         /    \        /    \
    fib(3)  fib(2) fib(2)  fib(1)
    /    \
fib(2)  fib(1)

fib(3)\texttt{fib(3)} wird zweimal berechnet, fib(2)\texttt{fib(2)} dreimal, und bei größerem nn wächst das exponentiell. Für fib(50)\texttt{fib(50)} wären es über 101010^{10} Aufrufe.

Abhilfe: Zwischenergebnisse merken statt neu berechnen. Man nennt das Speichern bereits berechneter Werte, und damit sinkt der Aufwand von O(2n)O(2^n) auf O(n)O(n). Alternativ genügt eine einfache , die von unten hochrechnet.

🔴 Die Lehre daraus ist wichtig: Rekursiv formulierbar heißt nicht rekursiv sinnvoll. Prüfe immer, ob dieselben Teilprobleme mehrfach auftreten.

fib(5): jede Zahl ist ein Aufruf

543322121

Die Zahl im ist das nn des Aufrufs fib(n)\texttt{fib}(n). Markiert ist alles, was mehr als einmal vorkommt, und das ist die Pointe: fib(3)\texttt{fib}(3) wird zweimal berechnet, fib(2)\texttt{fib}(2) dreimal und fib(1)\texttt{fib}(1) ebenfalls zweimal, sieben der neun Knoten sind reine Wiederholung. Jedes Mal entsteht derselbe Wert, und jedes Mal rechnet ihn das Programm von Grund auf neu aus. Bei n=5n = 5 ist das ärgerlich, bei n=50n = 50 vernichtend: Der Baum wächst exponentiell, und es wären über 101010^{10} Aufrufe. 🔴 Die Abhilfe ist so einfach wie der Fehler: Zwischenergebnisse merken statt neu berechnen, dann fällt der Aufwand von O(2n)O(2^n) auf O(n)O(n). Die Lehre daraus gilt über dieses Beispiel hinaus: Rekursiv formulierbar heißt nicht rekursiv sinnvoll. Prüfe immer, ob dieselben Teilprobleme mehrfach auftreten, der Baum zeigt es sofort.

Vertiefung: Rücksetzverfahren

Dieser Abschnitt gehört zum erhöhten Anforderungsniveau.

Manche Aufgaben lassen sich nur durch Ausprobieren lösen, aber nicht durch blindes: Ein Labyrinth, ein Sudoku, die Frage, ob acht Damen auf ein Schachbrett passen, ohne sich zu bedrohen.

Das Rücksetzverfahren (Backtracking) geht so vor:

Wähle eine mögliche Fortsetzung. Prüfe, ob sie die Bedingungen noch erfüllen kann. Vertiefe rekursiv, wenn ja. Setze zurück und probiere die nächste Möglichkeit, wenn nein.

loese(teilloesung):
    wenn teilloesung vollständig:
        gib sie aus, fertig
    für jede mögliche Fortsetzung:
        wenn Fortsetzung zulässig:
            füge sie hinzu
            loese(teilloesung)          // vertiefen
            entferne sie wieder         // zurücksetzen

Die Zeile „entferne sie wieder" ist der Kern und der Namensgeber. Sie stellt den vor dem Versuch wieder her, damit die nächste Möglichkeit auf sauberem Grund beginnt.

Beim Damenproblem setzt man Zeile für Zeile eine Dame. Steht sie schlecht, geht man in die vorige Zeile zurück und versucht dort die nächste Spalte.

Der entscheidende Gewinn liegt im frühen Abschneiden. Erkennt man in Zeile 3, dass keine Fortsetzung mehr möglich ist, spart man sich alle Anordnungen der übrigen Zeilen auf einen Schlag. Beim Acht-Damen-Problem gäbe es 88≈16,88^8 \approx 16{,}8 Millionen Stellungen; das Rücksetzverfahren prüft nur einen kleinen Bruchteil davon.

Wichtig ist aber, was das nicht bedeutet: Der Aufwand bleibt im schlechtesten Fall exponentiell. Das Rücksetzverfahren macht ein schweres Problem nicht leicht, es macht es nur oft handhabbar. Damit steht es genau an der Grenze, die im Kapitel über PP und NPNP beschrieben wurde.

Rücksetzverfahren: der Gewinn steckt im Abbruch

Versuch 1Versuch 2SackgasseStartSp. 1Sp. 2keineSp. 4Lösung

Zwei Versuche, die erste Dame zu setzen. Der linke führt in eine Sackgasse: In der nächsten Zeile ist keine Spalte mehr frei, die Teillösung kann also nicht mehr vollständig werden. Statt trotzdem weiterzuprobieren, nimmt das Verfahren die Dame zurück und geht zum nächsten Versuch, die Zeile „entferne sie wieder“ im Ablauf ist genau das und gibt dem Verfahren seinen Namen. 🔴 Und hier steckt der ganze Gewinn, aber er steckt in dem, was das Bild nicht zeigt: Unter der Sackgasse werden gar keine mehr erzeugt. Beim Acht-Damen-Problem gäbe es 88≈16,88^8 \approx 16{,}8 Millionen Stellungen; wer in Zeile 3 abbricht, erspart sich mit einem Schlag alle Anordnungen der fünf Zeilen darunter. Was das nicht heißt: Der schlechteste Fall bleibt exponentiell. Das Rücksetzverfahren macht ein schweres Problem nicht leicht, es macht es oft handhabbar, und steht damit genau an der Grenze, die im Kapitel über PP und NPNP beschrieben ist.

Einen rekursiven Aufruf verfolgen

Verfolge summe(4)\texttt{summe(4)} für das Verfahren: summe(n)\texttt{summe(n)} gibt 0 zurück, wenn n=0n = 0, sonst n+summe(n-1)n + \texttt{summe(n-1)}.

  1. 1

    Hineingehen, jeder Aufruf kommt auf den und wartet:

    summe(4) = 4 + summe(3)      ← wartet
    summe(3) = 3 + summe(2)      ← wartet
    summe(2) = 2 + summe(1)      ← wartet
    summe(1) = 1 + summe(0)      ← wartet
    summe(0) = 0                 ← Basisfall, gibt sofort zurück
    
  2. 2

    An dieser Stelle liegen fünf Aufrufe auf dem Stapel. Genau das ist der Speicherbedarf der Rekursion.

  3. 3

    Herauskommen, nun werden die wartenden Additionen von unten nach oben ausgeführt:

    summe(1) = 1 + 0 = 1
    summe(2) = 2 + 1 = 3
    summe(3) = 3 + 3 = 6
    summe(4) = 4 + 6 = 10
    
  4. 4

    Ergebnis: 10, und tatsächlich ist 1+2+3+4=101+2+3+4 = 10.

  5. 5

    Die häufigste Fehlvorstellung: dass summe(4)\texttt{summe(4)} zuerst 4+34 + 3 rechnet. Das kann nicht sein, denn bei 4+summe(3)4 + \texttt{summe(3)} steht der zweite Summand noch gar nicht fest. Der Aufruf muss erst vollständig zurückkehren.

  6. 6

    Beobachtung zur Tiefe: Für summe(n)\texttt{summe(n)} liegen n+1n+1 Aufrufe gleichzeitig auf dem Stapel. Bei n=100 000n = 100\,000 reicht der Stapel nicht mehr, und das Programm bricht ab. Eine hätte diese Grenze nicht.

summe(4)=10\texttt{summe(4)} = 10. Fünf Aufrufe auf dem Stapel, die Additionen erfolgen alle auf dem Rückweg.

Warum Fibonacci naiv scheitert

Zeige, warum die direkte rekursive Berechnung von fib(n)\texttt{fib(n)} exponentiell viele Aufrufe braucht, und wie man das behebt.

  1. 1

    Aufrufbaum für fib(5)\texttt{fib(5)} zeichnen:

                    fib(5)
                  /        \
             fib(4)        fib(3)
             /    \        /    \
        fib(3)  fib(2) fib(2)  fib(1)
        /    \
    fib(2)  fib(1)
    
  2. 2

    Zählen, was mehrfach vorkommt: fib(3)\texttt{fib(3)} zweimal, fib(2)\texttt{fib(2)} dreimal. Und jeder dieser Zweige berechnet seinerseits alles darunter erneut.

  3. 3

    Warum das exponentiell wird: Jeder Aufruf erzeugt zwei weitere. Der Aufrufbaum verdoppelt sich also näherungsweise mit jeder Ebene, und bei nn Ebenen stehen etwa 2n2^n da.

  4. 4

    Für fib(50)\texttt{fib(50)} sind das über 101010^{10} Aufrufe, also Stunden Rechenzeit für eine Zahl mit elf Stellen.

  5. 5

    Abhilfe 1: Zwischenergebnisse merken. Man legt eine Tabelle an und prüft vor jeder Berechnung, ob der Wert schon darinsteht. Dann wird jedes fib(k)\texttt{fib(k)} genau einmal berechnet, und der Aufwand sinkt auf O(n)O(n).

  6. 6

    Abhilfe 2: von unten. Man rechnet f(0),f(1),f(2),…f(0), f(1), f(2), \ldots der Reihe nach hoch und merkt sich nur die letzten beiden Werte. Ebenfalls O(n)O(n), dazu ohne Stapelbedarf.

  7. 7

    Die allgemeine Lehre: Rekursion ist dann teuer, wenn dieselben Teilprobleme mehrfach auftreten. Beim Sortieren durch Mischen passiert das nicht, denn dort sind die Hälften verschieden; deshalb bleibt es dort bei O(nlog⁡n)O(n \log n).

Der naive Aufruf ist O(2n)O(2^n) wegen Mehrfachberechnung; mit gemerkten Zwischenwerten oder einer Schleife wird daraus O(n)O(n).

Typischer Fehler

„Der Basisfall ist Formsache; Hauptsache, der rekursive Aufruf ist richtig."

Ohne Basisfall gibt es überhaupt kein Ergebnis, nur einen Absturz.

Sieh dir das an:

fakultaet(n):
    gib n * fakultaet(n-1) zurück

Der Rekursionsschritt ist mathematisch völlig korrekt. Trotzdem läuft das Programm bis fakultaet(0)\texttt{fakultaet(0)}, fakultaet(-1)\texttt{fakultaet(-1)}, fakultaet(-2)\texttt{fakultaet(-2)} weiter und bricht mit einem Stapelüberlauf ab.

Ebenso wichtig ist die zweite Bedingung: Der Rekursionsschritt muss dem Basisfall näher kommen. Dieser hat einen Basisfall und läuft trotzdem endlos:

fakultaet(n):
    wenn n = 0: gib 1 zurück
    gib n * fakultaet(n) zurück      // n statt n-1

Der Basisfall wird nie erreicht, weil sich das Argument nicht verändert.

Daher die Prüfliste, die man bei jeder Rekursion durchgehen sollte:

Gibt es einen Basisfall? Wird er von jedem Startwert aus erreicht? Wird das Argument bei jedem Schritt echt kleiner?

Bei fakultaet\texttt{fakultaet} gehört übrigens auch der negative Bereich dazu. Ein Basisfall n = 0\texttt{n = 0} hilft nicht, wenn jemand fakultaet(-3)\texttt{fakultaet(-3)} aufruft; sauber wäre n≤0\texttt{n} \le 0 oder eine ausdrückliche Prüfung der Eingabe.

Übung 1

leicht

a) Nenne die zwei notwendigen Teile jeder Rekursion. b) Was geschieht, wenn der Basisfall fehlt? c) Verfolge fakultaet(3)\texttt{fakultaet(3)} und gib alle Zwischenergebnisse an.

Tipp anzeigen

Zu c): Erst hinein bis zum Basisfall, dann heraus.

Lösung anzeigen

a) Ein Basisfall, der ohne weiteren Aufruf beantwortet wird, und ein Rekursionsschritt, der auf ein kleineres, dem Basisfall näher liegendes Teilproblem führt.

b) Die Aufrufe hören nicht auf. Jeder belegt Platz auf dem , und wenn dieser voll ist, bricht das Programm mit einem Stapelüberlauf ab.

c) Hineingehen: fakultaet(3)=3⋅fakultaet(2)\texttt{fakultaet(3)} = 3 \cdot \texttt{fakultaet(2)} fakultaet(2)=2⋅fakultaet(1)\texttt{fakultaet(2)} = 2 \cdot \texttt{fakultaet(1)} fakultaet(1)=1⋅fakultaet(0)\texttt{fakultaet(1)} = 1 \cdot \texttt{fakultaet(0)} fakultaet(0)=1\texttt{fakultaet(0)} = 1 (Basisfall)

Herauskommen: 1⋅1=11 \cdot 1 = 1, dann 2⋅1=22 \cdot 1 = 2, dann 3⋅2=63 \cdot 2 = 6.

Ergebnis: 6.

Detaillierte Schritterklärung anzeigen

Hier wird jeder Schritt einzeln erklärt, vor allem, warum er gemacht wird.

Erklärungstiefe

✦ Empfohlen: Standard – Die normale Erklärungstiefe passt zum Einstieg.

  1. 1

    a) Basisfall und Rekursionsschritt, und warum beide nötig sind

    Ein Basisfall, der ohne weiteren Aufruf beantwortet wird, und ein Rekursionsschritt, der auf ein kleineres, dem Basisfall näher liegendes Teilproblem führt. Beide Bedingungen zusammen garantieren, dass die Kette endet.

  2. 2

    b) Was ohne Basisfall geschieht

    Die Aufrufe hören nicht auf. Jeder belegt Platz auf dem Aufrufstapel, und wenn dieser voll ist, bricht das Programm mit einem Stapelüberlauf ab.

  3. 3

    c) fakultaet(3) verfolgen: erst hinein, dann heraus

    Hinein bis zum Basisfall: fakultaet(3)=3⋅fakultaet(2)\texttt{fakultaet(3)} = 3 \cdot \texttt{fakultaet(2)}, fakultaet(2)=2⋅fakultaet(1)\texttt{fakultaet(2)} = 2 \cdot \texttt{fakultaet(1)}, fakultaet(1)=1⋅fakultaet(0)\texttt{fakultaet(1)} = 1 \cdot \texttt{fakultaet(0)}, fakultaet(0)=1\texttt{fakultaet(0)} = 1 (Basisfall). Heraus: 1⋅1=11 \cdot 1 = 1, dann 2⋅1=22 \cdot 1 = 2, dann 3⋅2=63 \cdot 2 = 6. Ergebnis 6.

    3⋅(2⋅(1⋅1))=63 \cdot (2 \cdot (1 \cdot 1)) = 6

    Zwischenergebnis

    fakultaet(3)=6\texttt{fakultaet(3)} = 6

Übung 2

mittel

a) Schreibe ein rekursives Verfahren, das die Quersumme einer natürlichen Zahl berechnet. b) Verfolge es für die Zahl 472. c) Erkläre, warum die naive rekursive Berechnung von Fibonacci so teuer ist. d) Nenne zwei Möglichkeiten, das zu beheben, und gib jeweils den Aufwand an.

Tipp anzeigen

Zu a): Wie bekommt man die letzte Ziffer und wie den Rest?

Lösung anzeigen

a) Die letzte Ziffer ist der Rest bei Division durch 10, der Rest der Zahl ist die ganzzahlige Division durch 10.

quersumme(n):
    wenn n = 0:
        gib 0 zurück                                 // Basisfall
    gib (n mod 10) + quersumme(n ganzzahlig / 10)    // Rekursionsschritt

Das Argument wird bei jedem Schritt um eine Stelle kürzer und erreicht damit sicher die 0.

b) Hineingehen: quersumme(472)=2+quersumme(47)\texttt{quersumme(472)} = 2 + \texttt{quersumme(47)} quersumme(47)=7+quersumme(4)\texttt{quersumme(47)} = 7 + \texttt{quersumme(4)} quersumme(4)=4+quersumme(0)\texttt{quersumme(4)} = 4 + \texttt{quersumme(0)} quersumme(0)=0\texttt{quersumme(0)} = 0

Herauskommen: 4+0=44 + 0 = 4, dann 7+4=117 + 4 = 11, dann 2+11=132 + 11 = 13.

Ergebnis: 13, und 4+7+2=134 + 7 + 2 = 13 stimmt.

c) Weil dieselben Teilprobleme mehrfach berechnet werden. fib(n)\texttt{fib(n)} ruft fib(n-1)\texttt{fib(n-1)} und fib(n-2)\texttt{fib(n-2)} auf, und beide berechnen ihrerseits überlappende Teilbäume neu. Jeder Aufruf erzeugt zwei weitere, der Aufrufbaum verdoppelt sich also näherungsweise je Ebene, was auf etwa 2n2^n Aufrufe führt. Für fib(50)\texttt{fib(50)} sind das über 101010^{10}.

d) Zwischenergebnisse merken: Vor jeder Berechnung in einer Tabelle nachsehen, ob der Wert schon vorliegt. Jedes fib(k)\texttt{fib(k)} wird dann genau einmal berechnet → O(n)O(n).

von unten: f(0),f(1),f(2),…f(0), f(1), f(2), \ldots der Reihe nach hochrechnen und nur die letzten beiden Werte merken → O(n)O(n), zusätzlich ohne Stapelbedarf und mit O(1)O(1) Speicher.

Detaillierte Schritterklärung anzeigen

Hier wird jeder Schritt einzeln erklärt, vor allem, warum er gemacht wird.

Erklärungstiefe

✦ Empfohlen: Standard – Die normale Erklärungstiefe passt zum Einstieg.

  1. 1

    Teil a): Die Zerlegung finden

    Man sucht, wie sich das Problem in ein kleineres desselben Typs zerlegt. Bei der Quersumme ist das die Trennung in letzte Ziffer und Rest.

    472 = 2 + \text{Quersumme von } 47

    Zwischenergebnis

    n mod 10n \bmod 10 liefert die Ziffer, n:10n : 10 den Rest.

    Der Basisfall n=0n = 0 ist hier zwingend, denn nur er beendet den Abstieg. Und er wird sicher erreicht, weil die Zahl bei jedem Schritt eine Stelle verliert.

  2. 2

    Teil b): Zwei Spalten führen

    Man notiert erst alle Aufrufe beim Hineingehen und rechnet dann beim Herauskommen von unten nach oben zusammen.

    Zwischenergebnis

    0→4→11→130 \to 4 \to 11 \to 13.

  3. 3

    Teil c): Den Aufrufbaum zeichnen

    Statt zu behaupten, dass es langsam ist, zeichnet man den Baum und zählt, was mehrfach vorkommt. Bei fib(5)\texttt{fib(5)} tritt fib(2)\texttt{fib(2)} bereits dreimal auf.

    \text{etwa } 2^n \text{ Aufrufe}

    Zwischenergebnis

    Jeder Aufruf erzeugt zwei weitere → exponentiell.

  4. 4

    Teil d): Zwei Wege, ein Ergebnis

    Beide Wege beseitigen dieselbe Ursache, nämlich die Mehrfachberechnung. Der eine merkt sich Ergebnisse, der andere rechnet gleich in der richtigen Reihenfolge.

    Zwischenergebnis

    Beide O(n)O(n); die Schleife zusätzlich ohne Stapelbedarf.

    Wenn ein Problem sich so leicht von unten aufbauen lässt, ist die Schleife meist die bessere Wahl. Die Rekursion lohnt dort, wo die Reihenfolge der Teilprobleme nicht von vornherein feststeht, etwa bei Bäumen.

Übung 3

schwer

a) Erkläre die rekursive Lösung der Türme von Hanoi und begründe, warum sie 2n−12^n - 1 Züge braucht. b) (Vertiefung) Beschreibe das Rücksetzverfahren am Damenproblem. c) (Vertiefung) Warum spart das Rücksetzverfahren gegenüber dem Durchprobieren aller Stellungen, und warum bleibt es trotzdem im schlechtesten Fall exponentiell? d) Nenne einen Fall, in dem eine der Rekursion vorzuziehen ist, und begründe.

Tipp anzeigen

Zu a): Wie oft wird das Verfahren für n−1n-1 aufgerufen?

Lösung anzeigen

a) Lösung: Um nn Scheiben von A nach C zu bringen, verlegt man zuerst die obersten n−1n-1 Scheiben von A nach B (rekursiv), schiebt dann die unterste Scheibe von A nach C und verlegt schließlich die n−1n-1 Scheiben von B nach C (wieder rekursiv). Der Basisfall ist n=0n = 0, dort ist nichts zu tun.

Zügezahl: Sei z(n)z(n) die Anzahl der Züge. Aus dem Ablauf folgt unmittelbar

z(0)=0z(n)=2⋅z(n−1)+1z(0) = 0 \qquad z(n) = 2 \cdot z(n-1) + 1

Daraus ergibt sich z(1)=1z(1) = 1, z(2)=3z(2) = 3, z(3)=7z(3) = 7, z(4)=15z(4) = 15, allgemein z(n)=2n−1z(n) = 2^n - 1. Man sieht es auch direkt: Jede zusätzliche Scheibe verdoppelt die Arbeit und fügt einen Zug hinzu.

b) Man setzt Zeile für Zeile eine Dame:

  1. In Zeile 1 die erste zulässige Spalte wählen.
  2. In Zeile 2 die erste Spalte suchen, die von keiner bereits gesetzten Dame bedroht wird (weder gleiche Spalte noch gleiche Diagonale).
  3. So weiter, bis alle acht Damen stehen. Dann ist eine Lösung gefunden.
  4. Gibt es in einer Zeile keine zulässige Spalte, geht man in die vorige Zeile zurück, entfernt die dortige Dame und versucht die nächste Spalte.

Das Zurückgehen ist der namensgebende Schritt. Es stellt den vor dem Versuch wieder her, damit die nächste Möglichkeit unverfälscht beginnt.

c) Ersparnis durch frühes Abschneiden: Wird schon in Zeile 3 erkannt, dass keine zulässige Fortsetzung existiert, entfallen alle Anordnungen der Zeilen 4 bis 8 auf einen Schlag. Beim blinden Durchprobieren wären das 85=32 7688^5 = 32\,768 Stellungen allein an dieser einen Stelle. Statt der 88≈16,88^8 \approx 16{,}8 Millionen Gesamtstellungen prüft das Verfahren deshalb nur einen kleinen Bruchteil.

Warum trotzdem exponentiell: Das Abschneiden hängt davon ab, wie früh Widersprüche auftreten, und das ist eine Eigenschaft der Aufgabe, keine Garantie des Verfahrens. Im ungünstigsten Fall werden Widersprüche erst ganz unten erkannt, und dann bleibt fast der gesamte Suchbaum zu durchlaufen. Das Rücksetzverfahren macht ein schweres Problem also nicht leicht, sondern oft handhabbar. Damit steht es genau an der Grenze zwischen PP und NPNP: Eine gefundene Lösung ist schnell prüfbar, das Finden bleibt teuer.

d) Beispiel: die Summe aller Zahlen von 1 bis nn oder allgemein jede einfache Wiederholung fester Anzahl.

Begründung: Die Rekursion legt für jeden Schritt einen Eintrag auf den , verbraucht also O(n)O(n) Speicher und scheitert bei großem nn an einem Stapelüberlauf. Eine Schleife kommt mit O(1)O(1) Speicher aus und ist zusätzlich schneller, weil kein Aufrufaufwand entsteht. Da sich das Problem nicht natürlich in gleichartige Teilprobleme zerlegt, sondern nur eine Wiederholung darstellt, bringt die Rekursion hier auch keinen Gewinn an Verständlichkeit.

Die Faustregel lautet: Rekursion bei geschachtelten Strukturen, Schleife bei einfachen Wiederholungen.

Detaillierte Schritterklärung anzeigen

Hier wird jeder Schritt einzeln erklärt, vor allem, warum er gemacht wird.

Erklärungstiefe

✦ Empfohlen: Standard – Die normale Erklärungstiefe passt zum Einstieg.

  1. 1

    a) Die rekursive Lösung: das Problem auf ein kleineres zurückführen

    Um nn Scheiben von A nach C zu bringen: erst die obersten n−1n-1 Scheiben von A nach B verlegen (rekursiv), dann die unterste Scheibe von A nach C schieben, dann die n−1n-1 Scheiben von B nach C verlegen (wieder rekursiv). Basisfall ist n=0n = 0. Dort ist nichts zu tun.

  2. 2

    a) Die Zügezahl aus der Rekursionsgleichung

    Sei z(n)z(n) die Anzahl der Züge. Aus dem Ablauf folgt unmittelbar z(0)=0z(0) = 0 und z(n)=2⋅z(n−1)+1z(n) = 2 \cdot z(n-1) + 1, zweimal die Teillösung plus ein Zug für die unterste Scheibe. Daraus: z(1)=1z(1) = 1, z(2)=3z(2) = 3, z(3)=7z(3) = 7, z(4)=15z(4) = 15, allgemein z(n)=2n−1z(n) = 2^n - 1.

    z(n)=2z(n−1)+1z(n) = 2 z(n-1) + 1, z(0)=0⇒z(n)=2n−1z(0) = 0 \Rightarrow z(n) = 2^n - 1

  3. 3

    b) (Vertiefung) Das Rücksetzverfahren am Damenproblem

    Man setzt Zeile für Zeile eine Dame: In Zeile 1 die erste zulässige Spalte wählen; in Zeile 2 die erste Spalte suchen, die von keiner bereits gesetzten Dame bedroht wird (weder gleiche Spalte noch gleiche Diagonale); so weiter bis alle acht stehen. Gibt es in einer Zeile keine zulässige Spalte, geht man in die vorige Zeile zurück, entfernt die dortige Dame und versucht die nächste Spalte.

  4. 4

    c) (Vertiefung) Warum Backtracking spart, und trotzdem exponentiell bleibt

    Ersparnis durch frühes Abschneiden: Wird schon in Zeile 3 erkannt, dass keine zulässige Fortsetzung existiert, entfallen alle Anordnungen der Zeilen 4 bis 8 auf einen Schlag, beim blinden Durchprobieren wären das 85=32 7688^5 = 32\,768 Stellungen allein an dieser Stelle. Statt der 88≈16,88^8 \approx 16{,}8 Millionen Gesamtstellungen prüft das Verfahren nur einen kleinen Bruchteil.

  5. 5

    d) Wann eine Schleife der Rekursion vorzuziehen ist

    Beispiel: die Summe aller Zahlen von 1 bis nn, oder allgemein jede einfache Wiederholung fester Anzahl. Begründung: Die Rekursion legt für jeden Schritt einen Eintrag auf den Aufrufstapel, verbraucht also O(n)O(n) Speicher und scheitert bei großem nn an einem Stapelüberlauf. Eine Schleife kommt mit O(1)O(1) Speicher aus und ist zusätzlich schneller, weil kein Aufrufaufwand entsteht.

Zusammenfassung

Ein rekursives ruft sich selbst auf und braucht dafür zwingend zwei Teile: einen Basisfall, der ohne weiteren Aufruf beantwortet wird, und einen Rekursionsschritt, der dem Basisfall näher kommt; fehlt einer von beiden, endet die Rekursion im Stapelüberlauf. Der Rechner legt jeden offenen Aufruf auf den , weshalb erst vollständig hineingegangen und dann auf dem Rückweg gerechnet wird und die Rekursionstiefe durch den Speicher begrenzt ist. Sinnvoll ist Rekursion dort, wo ein Problem natürlich in gleichartige kleinere Teile zerfällt, etwa bei Verzeichnissen, bei Teile und herrsche oder beim Durchprobieren aller Möglichkeiten; bei einfachen Wiederholungen ist die schneller und sparsamer. Dass sich etwas rekursiv formulieren lässt, macht es nicht sinnvoll: Die naive Fibonacci-Berechnung ist exponentiell, weil sich die Teilprobleme überlappen, und wird durch gemerkte Zwischenergebnisse oder eine Schleife auf O(n)O(n) gebracht. Das Rücksetzverfahren schließlich probiert Möglichkeiten schrittweise durch und setzt bei Widersprüchen zurück; sein früher Abbruch spart oft gewaltig, hebt aber den im schlechtesten Fall exponentiellen Aufwand nicht auf.