🏠 Startseite

Rekursion

Wenn Funktionen sich selbst aufrufen

Vorlesung 10 | Fortgeschrittene Algorithmen

"Um Rekursion zu verstehen, muss man zuerst Rekursion verstehen."

Lernziele

  • Verstehen, was Rekursion ist und wie sie funktioniert
  • Basisfall und Rekursionsfall unterscheiden
  • Rekursive Funktionen in C implementieren
  • Den Call Stack bei Rekursion verstehen
  • Rekursion vs. Iteration vergleichen
  • Klassische rekursive Algorithmen kennenlernen

Was ist Rekursion?

Definition

Rekursion ist eine Programmiertechnik, bei der eine Funktion sich selbst aufruft.

Beispiel aus dem Alltag:

Russische Matroschka-Puppen - jede Puppe enthält eine kleinere Version von sich selbst.

Zwei wichtige Teile

  1. Basisfall:
    Die Bedingung, die die Rekursion beendet
  2. Rekursionsfall:
    Der Selbstaufruf mit verkleinertem Problem

Rekursion begegnet uns überall!

🪆

Matroschka

Puppe in Puppe in Puppe...

🪞

Zwei Spiegel

Unendliche Reflexion

📁

Ordnerstruktur

Ordner in Ordner in Ordner...

🌿

Farn-Blatt

Jeder Teil sieht aus wie das Ganze

Gemeinsam: Ein großes Problem besteht aus kleineren Versionen von sich selbst!

Erstes Beispiel: Countdown

void countdown(int n) {
    if (n == 0) {           // Basisfall
        printf("Start!\n");
        return;
    }
    printf("%d...\n", n);
    countdown(n - 1);      // Rekursionsfall
}

// Aufruf:
countdown(3);

Schritt fur Schritt:

1 countdown(3) → n=3, nicht 0 → print "3..."
2 countdown(2) → n=2, nicht 0 → print "2..."
3 countdown(1) → n=1, nicht 0 → print "1..."
4 countdown(0) → n=0! → print "Start!" → STOP

Ausgabe: 3... 2... 1... Start!

Der Call Stack - Funktionen "stapeln" sich

Was ist der Stack?

Der Call Stack ist wie ein Stapel Teller:

  • Jeder Funktionsaufruf legt einen "Teller" oben drauf
  • Die Funktion muss warten, bis der Aufruf daruber fertig ist
  • Erst wenn eine Funktion return macht, wird ihr "Teller" entfernt

Stack wachst = Mehr Funktionen warten aufeinander = Mehr Speicher wird benutzt!

countdown(3) → Stack-Aufbau

countdown(0) → "Start!" + return ← TOP (zuletzt hinzu)
↑ wartet
countdown(1) → "1..." + ruft countdown(0)
↑ wartet
countdown(2) → "2..." + ruft countdown(1)
↑ wartet
countdown(3) → "3..." + ruft countdown(2) ← BOTTOM (zuerst hinzu)

Ruckgabe (Stack wird abgebaut): countdown(0) endet zuerst → dann 1 → dann 2 → dann 3

Wie beim Tellerstapel: Der oberste Teller wird zuerst weggenommen!

Warum "wartet" die Funktion? - Telefonanruf-Analogie

Stell dir Telefonanrufe vor:

📞 Du rufst Mama an...
📞 Mama ruft Oma an...
📞 Oma ruft Onkel an...

Wer legt zuerst auf?

Onkel legt auf → ✓ fertig

Oma kann jetzt auflegen → ✓ fertig

Mama kann jetzt auflegen → ✓ fertig

Du legst zuletzt auf → ✓ fertig

Du hast zuerst angerufen, legst aber zuletzt auf!

So ist es bei countdown():

void countdown(int n) {
    if (n == 0) {
        printf("Start!\n");
        return;
    }
    printf("%d...\n", n);
    countdown(n - 1);  // ⏳ WARTET HIER!
    // Erst wenn countdown(n-1)
    // fertig ist, geht es weiter
}

countdown(3): "Ich habe 3 gedruckt, jetzt rufe ich countdown(2)... Ich bin BLOCKIERT bis countdown(2) zuruckkommt!"

Zwei Phasen der Rekursion

Phase 1: Abtauchen (Stack fullt sich)

Jeder rekursive Aufruf wird im Speicher gestapelt. Die Funktionen warten alle!

countdown(3) ← wartet im Speicher
  countdown(2) ← wartet im Speicher
    countdown(1) ← wartet im Speicher
      countdown(0) ← Basis erreicht!

Phase 2: Auftauchen (Stack leert sich)

Jetzt werden die wartenden Funktionen ruckwarts abgearbeitet:

      countdown(0) → return
    countdown(1) → jetzt fertig!
  countdown(2) → jetzt fertig!
countdown(3) → jetzt fertig!

void demo(int n) {
    if (n == 0) {
        printf("=== BASIS ===\n");
        return;
    }
    // Phase 1: VOR dem Abtauchen
    printf("Runter: %d\n", n);

    demo(n - 1);  // Abtauchen!

    // Phase 2: NACH dem Auftauchen
    printf("Hoch: %d\n", n);
}

Ausgabe von demo(3):

Runter: 3

Runter: 2

Runter: 1

=== BASIS ===

Hoch: 1

Hoch: 2

Hoch: 3

← Speicher fullt sich

← Speicher fullt sich

← Speicher fullt sich

← Wendepunkt!

← Speicher leert sich

← Speicher leert sich

← Speicher leert sich

Was passiert bei countdown(1000)?

Der Stack wächst und wächst...

n=3 3 Frames
n=10 10 Frames
n=100 100 Frames
💥
n=10000 Stack Overflow!

Stack Overflow: Der Speicher für den Stack ist begrenzt (meist 1-8 MB). Bei zu vielen rekursiven Aufrufen stürzt das Programm ab!

Wie viel ist zu viel?

Tiefe Status
1 - 100 ✓ Kein Problem
100 - 1.000 ✓ Meistens OK
1.000 - 10.000 ⚠️ Riskant
> 10.000 ❌ Stack Overflow

Lösung: Für sehr tiefe Rekursion → Iteration verwenden!

Klassiker: Fakultät (n!)

Mathematische Definition

n! = n × (n-1) × (n-2) × ... × 1

5! = 5 × 4 × 3 × 2 × 1 = 120

Rekursive Definition:

n! = n × (n-1)!

0! = 1 (Basisfall)

int fakultaet(int n) {
    // Basisfall
    if (n == 0) {
        return 1;
    }

    // Rekursionsfall
    return n * fakultaet(n - 1);
}

fakultaet(4) - Schritt für Schritt

Abwärts (Aufrufe):

fakultaet(4) = 4 × fakultaet(3)

fakultaet(3) = 3 × fakultaet(2)

fakultaet(2) = 2 × fakultaet(1)

fakultaet(1) = 1 × fakultaet(0)

fakultaet(0) = 1 ← Basisfall!

Aufwärts (Rückgaben):

fakultaet(1) = 1 × 1 = 1

fakultaet(2) = 2 × 1 = 2

fakultaet(3) = 3 × 2 = 6

fakultaet(4) = 4 × 6 = 24

Übung zusammen: Was gibt fakultaet(5) aus?

Der Code:

int fakultaet(int n) {
    if (n == 0) {
        return 1;
    }
    return n * fakultaet(n - 1);
}

printf("%d", fakultaet(5));

Frage: Berechnet gemeinsam - was ist 5! ?

Löst es Schritt für Schritt:

5! = 5 × 4! = 5 × ___
4! = 4 × 3! = 4 × ___
3! = 3 × 2! = 3 × ___
2! = 2 × 1! = 2 × ___
1! = 1 × 0! = 1 × ___
0! = 1 ✓

Ergebnis: 120

Der Call Stack bei Rekursion

Was ist der Call Stack?

Ein Stapel (Stack) im Speicher, der für jeden Funktionsaufruf speichert:

  • Lokale Variablen
  • Parameter
  • Rücksprungadresse

Achtung: Jeder Aufruf braucht Speicher! Zu tiefe Rekursion führt zu Stack Overflow.

Stack bei fakultaet(4)

fakultaet(0) → return 1
fakultaet(1) → 1 × ?
fakultaet(2) → 2 × ?
fakultaet(3) → 3 × ?
fakultaet(4) → 4 × ?

↑ Stapel wächst nach oben

Fibonacci-Zahlen

Die Folge

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...

Regel: Jede Zahl ist die Summe der zwei vorherigen!

F(n) = F(n-1) + F(n-2)

F(0) = 0, F(1) = 1

int fibonacci(int n) {
    // Basisfälle
    if (n == 0) return 0;
    if (n == 1) return 1;

    // Rekursionsfall
    return fibonacci(n-1)
         + fibonacci(n-2);
}

Problem: Warum ist naive Fibonacci-Rekursion so langsam?

Aufrufbaum fur fib(5)

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

Das Problem:

fib(3) wird 2x berechnet

fib(2) wird 3x berechnet

Wir berechnen dieselben Werte immer wieder!

Warum passiert das?

Fibonacci ruft sich zweimal pro Aufruf auf:

fib(n) = fib(n-1) + fib(n-2)

Das erzeugt einen Baum, keinen Stapel!

Wie schlimm ist es?

fib(10) 177 Aufrufe
fib(20) 21.891 Aufrufe
fib(40) 331 Millionen Aufrufe!

fib(40) dauert Minuten - obwohl das Ergebnis nur 102.334.155 ist!

Losung: Wie berechnet man Fibonacci richtig?

Losung 1: Iteration (Schleife)

int fib_iter(int n) {
    if (n <= 1) return n;

    int prev = 0, curr = 1;
    for (int i = 2; i <= n; i++) {
        int next = prev + curr;
        prev = curr;
        curr = next;
    }
    return curr;
}

n Schritte statt Millionen

Kein Stack-Overflow-Risiko

fib(40) in Mikrosekunden!

Losung 2: Memoization (Caching)

int cache[100] = {0};  // Speicher

int fib_memo(int n) {
    if (n <= 1) return n;
    if (cache[n] != 0)
        return cache[n]; // Schon berechnet!

    cache[n] = fib_memo(n-1) + fib_memo(n-2);
    return cache[n];
}

Bleibt rekursiv, aber speichert Ergebnisse

Jeder Wert wird nur einmal berechnet

Auch fib(40) in Mikrosekunden!

Wann welche Lösung?

Iteration wählen:

Wenn du nur das Ergebnis brauchst → einfachste & schnellste Lösung

Memoization wählen:

Wenn du mehrere Fibonacci-Werte brauchst (Cache wiederverwendbar) oder rekursive Struktur beibehalten willst

Übung zusammen: Berechne fib(6)

Die Fibonacci-Folge:

0
1
1
2
3
5
?
fib(0) fib(1) fib(2) fib(3) fib(4) fib(5) fib(6)

Regel: fib(n) = fib(n-1) + fib(n-2)

Lösung Schritt für Schritt:

fib(6) = fib(5) + fib(4)
= 5 + 3
= 8

fib(6) = 8

Die Folge: 0, 1, 1, 2, 3, 5, 8, 13, 21...

Quiz: Was gibt dieser Code aus?

void mystery(int n) {
    if (n <= 0) {
        return;
    }
    printf("%d ", n);
    mystery(n - 2);
}

mystery(7);

Was wird ausgegeben?

A) 7 5 3 1
B) 1 3 5 7
C) 7 6 5 4 3 2 1
D) Stack Overflow

Lösung: A) 7 5 3 1

void mystery(int n) {
    if (n <= 0) {
        return;
    }
    printf("%d ", n);
    mystery(n - 2);  // ← n-2, nicht n-1!
}

Schritt für Schritt:

mystery(7) → print 7, call mystery(5)
mystery(5) → print 5, call mystery(3)
mystery(3) → print 3, call mystery(1)
mystery(1) → print 1, call mystery(-1)
mystery(-1) → return (n <= 0) ✓

Trick: n wird um 2 verringert → nur ungerade Zahlen!

Quiz: Was berechnet diese Funktion?

int geheimnis(int n) {
    if (n == 1) {
        return 1;
    }
    return n + geheimnis(n - 1);
}

printf("%d", geheimnis(4));

Was wird ausgegeben?

A) 4
B) 10
C) 24
D) 1

Lösung: B) 10

Diese Funktion berechnet die Summe von 1 bis n!

geheimnis(4) = 4 + geheimnis(3)
= 4 + 3 + geheimnis(2)
= 4 + 3 + 2 + geheimnis(1)
= 4 + 3 + 2 + 1
= 10

Mathematisch:

1 + 2 + 3 + 4 = 10

Formel: n + (n-1) + ... + 1 = n×(n+1)/2

4 × 5 / 2 = 20 / 2 = 10

Rekursion vs. Iteration: Fakultät visualisiert

Berechne 4! = 4 × 3 × 2 × 1 = 24

Rekursiv: Von oben nach unten

int fak_rek(int n) {
    if (n == 0) return 1;
    return n * fak_rek(n-1);
}

Phase 1: Abtauchen (Stack füllt sich)

fak(4) = 4 × fak(3) = 4 × ?
fak(3) = 3 × fak(2) = 3 × ?
fak(2) = 2 × fak(1) = 2 × ?
fak(1) = 1 × fak(0) = 1 × ?
fak(0) = 1 ← Basisfall!

Phase 2: Auftauchen (Ergebnisse zurück)

fak(1) = 1 × 1 = 1
fak(2) = 2 × 1 = 2
fak(3) = 3 × 2 = 6
fak(4) = 4 × 6 = 24

Iterativ: Schritt für Schritt

int fak_iter(int n) {
    int erg = 1;
    for(int i=1; i<=n; i++)
        erg *= i;
    return erg;
}

Direkte Berechnung (kein Stack nötig)

Schritt i erg Rechnung
Start-1Initialisierung
1111 × 1 = 1
2221 × 2 = 2
3362 × 3 = 6
44246 × 4 = 24
Rekursiv: Eleganter Code, aber braucht Stack-Speicher
Iterativ: Effizienter, nur eine Variable nötig

Warum ist Iteration manchmal komplexer?

Beispiel: Alle Dateien in Ordnern zählen (inkl. Unterordner)

Rekursiv: Natürlich!

📁 Dokumente/
├── 📄 brief.txt
├── 📁 Fotos/
│ └── 📄 foto.jpg
└── 📁 Arbeit/
└── 📄 projekt.pdf

Denkweise:

"Zähle Dateien hier + frag jeden Unterordner rekursiv"

Einfach wie im echten Leben!

Iterativ: Kompliziert!

Du musst dir selbst merken, wo du warst:

1. Starte bei Dokumente/
2. Finde brief.txt ✓
3. Finde Fotos/ → merke dir: "Arbeit/ noch offen!"
4. Gehe in Fotos/, finde foto.jpg ✓
5. Erinnere dich: "Arbeit/ war noch offen"
6. Gehe in Arbeit/, finde projekt.pdf ✓

Problem: Du brauchst eine "To-Do-Liste" für noch nicht besuchte Ordner!

Fazit: Bei Rekursion merkt sich der Computer automatisch, wo er war. Bei Iteration musst du das selbst organisieren!

Wann Rekursion? Wann Iteration?

Rekursion ist ideal für:

🌳
Baumstrukturen
Dateisysteme, HTML/DOM, Menüs
🔀
Divide & Conquer
Quicksort, Mergesort, Binärsuche
🧩
Mathematische Definitionen
Fibonacci, Fakultät, Potenz
📖
Wenn Code lesbar sein soll
Prototypen, Lehrzwecke

Iteration ist besser für:

🔢
Einfache Schleifen
Summen, Durchläufe, Zähler
Performance-kritisch
Games, Echtzeit, große Daten
📚
Sehr tiefe Verschachtelung
Stack-Overflow vermeiden
💾
Speicher sparen
Embedded, Mobile

Goldene Regel: Jede Rekursion kann in Iteration umgewandelt werden - aber manchmal ist Rekursion viel einfacher zu verstehen!

Häufige Fehler bei Rekursion

1. Fehlender Basisfall

// FALSCH!
int falsch(int n) {
    return n * falsch(n-1);
}

Was passiert bei falsch(3)?

falsch(3) → falsch(2)
→ falsch(1)
→ falsch(0)
→ falsch(-1)
→ falsch(-2)
→ ... ∞ 💥

Stack Overflow! Kein Stopp!

2. Kein Fortschritt zum Ziel

// FALSCH!
int falsch2(int n) {
    if (n == 0) return 1;
    return n * falsch2(n); // n-1 vergessen!
}

Was passiert bei falsch2(3)?

falsch2(3) → falsch2(3)
→ falsch2(3)
→ falsch2(3)
→ falsch2(3)
→ ... ∞ 💥

Stack Overflow! n bleibt 3!

Checkliste vor dem Coden:

✓ Gibt es einen Basisfall?
✓ Wird n bei jedem Aufruf kleiner?
✓ Erreicht man garantiert den Basisfall?

So denkst du wie der Computer

Schritt-für-Schritt-Anleitung zum "Rekursion tracen"

1️⃣

Aufschreiben

Schreibe den ersten Aufruf auf ein Blatt

fak(4) = ?
2️⃣

Basisfall prüfen

Ist n == 0? Wenn ja, return 1

4 ≠ 0 → weiter
3️⃣

Einrücken

Schreibe den nächsten Aufruf eingerückt

  fak(3) = ?
4️⃣

Auflösen

Von unten nach oben die ? ersetzen

? → 6 → 24

Tipp: Nimm Papier und Stift! Rekursion "im Kopf" zu tracen ist schwer - aufschreiben macht es einfach.

Debugging-Tipps für Rekursion

Tipp 1: printf am Anfang

int fak(int n) {
    printf("fak(%d) aufgerufen\n", n);

    if (n == 0) return 1;
    return n * fak(n-1);
}
fak(3) aufgerufen
fak(2) aufgerufen
fak(1) aufgerufen
fak(0) aufgerufen

Tipp 2: printf mit Ergebnis

int fak(int n) {
    if (n == 0) return 1;
    int erg = n * fak(n-1);
    printf("fak(%d) = %d\n", n, erg);
    return erg;
}
fak(1) = 1
fak(2) = 2
fak(3) = 6

Wenn dein Programm einfriert: Es gibt wahrscheinlich keinen Basisfall oder keinen Fortschritt!

Mini-Quiz: Was passiert hier?

Code A: printf VOR Rekursion

void test(int n) {
    if (n > 0) {
        printf("%d ", n);  // ← ZUERST drucken
        test(n - 1);       // ← DANN abtauchen
    }
}
test(4);

Ausgabe: 4 3 2 1

Druckt beim Abtauchen (groß → klein)

Code B: printf NACH Rekursion

void test(int n) {
    if (n > 0) {
        test(n - 1);       // ← ZUERST abtauchen
        printf("%d ", n);  // ← DANN drucken
    }
}
test(4);

Ausgabe: 1 2 3 4

Druckt beim Auftauchen (klein → groß)

Merksatz: Code vor dem rekursiven Aufruf → läuft beim "Abtauchen" | Code nach dem Aufruf → läuft beim "Auftauchen"!

Quiz: Wo ist der Fehler?

int summe(int n) {
    return n + summe(n - 1);
}

printf("%d", summe(5));

Was passiert?

A) Gibt 15 aus
B) Gibt 0 aus
C) Stack Overflow
D) Compile Error

Lösung: C) Stack Overflow!

Problem: Es fehlt der Basisfall!

summe(5) → 5 + summe(4)
summe(4) → 4 + summe(3)
...
summe(0) → 0 + summe(-1)
summe(-1) → -1 + summe(-2) ...
→ Läuft ewig! 💥

So wäre es richtig:

int summe(int n) {
    if (n == 0) return 0;  // ← Basisfall!
    return n + summe(n - 1);
}

Merke: IMMER zuerst den Basisfall schreiben!

Quiz: Was berechnet power(2, 4)?

int power(int basis, int exp) {
    if (exp == 0) {
        return 1;
    }
    return basis * power(basis, exp - 1);
}

printf("%d", power(2, 4));

Was wird ausgegeben?

A) 8
B) 16
C) 6
D) 4

Lösung: B) 16

Diese Funktion berechnet basisexp = 24!

power(2, 4) = 2 × power(2, 3)
= 2 × 2 × power(2, 2)
= 2 × 2 × 2 × power(2, 1)
= 2 × 2 × 2 × 2 × power(2, 0)
= 2 × 2 × 2 × 2 × 1
= 16

Mathematisch:

24 = 2 × 2 × 2 × 2 = 16

Basisfall: exp == 0 → return 1

Rekursion: basis × power(basis, exp-1)

Schema: So schreibst du rekursive Funktionen

1

Frage: Was ist der einfachste Fall?

Das ist dein Basisfall. Bei Fakultät: n=0 → return 1

2

Frage: Wie kann ich das Problem verkleinern?

Bei Fakultät: Um n! zu berechnen, brauche ich nur (n-1)!

3

Frage: Wie kombiniere ich die Ergebnisse?

Bei Fakultät: n! = n × (n-1)! → also return n * fak(n-1)

int rekursiv(int n) {
    if (/* Basisfall erreicht? */)
        return /* einfache Lösung */;

    return /* kombiniere n mit rekursiv(kleiner) */;
}

Schema: So verstehst du den Call Stack

1

Jeder Aufruf = neuer "Teller" auf dem Stapel

Die Funktion wird "pausiert" und wartet auf das Ergebnis des Unteraufrufs

2

Der oberste "Teller" wird zuerst bearbeitet

LIFO = Last In, First Out → Der letzte Aufruf endet zuerst

3

Wenn return passiert, wird der "Teller" weggenommen

Das Ergebnis geht zurück an den wartenden Aufruf darunter

Abtauchen

⬇️

Stack wächst

Basisfall

🛑

Wendepunkt!

Auftauchen

⬆️

Stack schrumpft

Schema: So vermeidest du Fehler

Schreibe ZUERST den Basisfall!

Bevor du den rekursiven Teil schreibst, definiere: "Wann hört die Rekursion auf?"

if (n == 0) return 1; // ← IMMER ZUERST!

Prüfe: Wird n IMMER kleiner?

Der rekursive Aufruf muss das Problem verkleinern, sonst: Endlosschleife!

fak(n - 1); // ← n wird kleiner → gut!

Teste mit kleinen Zahlen zuerst!

Probiere fak(0), fak(1), fak(2) aus - nicht gleich fak(100)!

printf("%d", fak(2)); // ← Erst klein testen!

Zusammenfassung: Was haben wir gelernt?

Jede Rekursion braucht:

🛑
Basisfall
Stoppt die Rekursion (z.B. n == 0)
🔄
Rekursionsfall
Ruft sich selbst mit kleinerem Problem auf
📉
Fortschritt
Problem wird bei jedem Aufruf kleiner

Das Wichtigste:

📚
Call Stack
Funktionen "warten" aufeinander (wie Teller)
⬇️⬆️
Zwei Phasen
Abtauchen (fragen) → Auftauchen (antworten)
⚖️
Rekursion vs. Iteration
Beides möglich, manchmal ist eines einfacher

Merksatz: "Löse das Problem, indem du es auf ein kleineres Problem zurückführst!"

Die klassischen Beispiele

Countdown

3 → 2 → 1 → 0

Das einfachste Beispiel

Fakultät

5! = 5×4×3×2×1

n! = n × (n-1)!

Fibonacci

1, 1, 2, 3, 5, 8...

Vorsicht: Doppelte Berechnung!

Tipp für die Klausur: Immer zuerst Basisfall und Rekursionsfall identifizieren!

Vielen Dank!

Fragen zur Rekursion?

Nächste Vorlesung: Sortieralgorithmen

1 / 30