Hier sind die 12 häufigsten Fehler in Klausuren zu Rekursion und Teile und herrsche. Lies sie durch — wer einen Fehler kennt, macht ihn seltener. Die meisten passieren an der Abbruchbedingung und an den Grenzen der Teilbereiche.
Die 12 häufigsten Fehler
1Abbruchbedingung zu spät geprüft
Selbstaufruf vor der Abbruchbedingung
So wird oft programmiert: Die Summe der Zahlen von 1 bis n:
static int summe(int n) {
int rest = summe(n - 1); // ruft sich immer auf
if (n == 0) {
return 0;
}
return n + rest;
}Richtig ist: Die Abbruchbedingung muss vor dem Selbstaufruf stehen, sonst wird sie nie erreicht — jeder Aufruf ruft sofort den nächsten auf, bis zum StackOverflowError.
static int summe(int n) {
if (n == 0) {
return 0;
}
return n + summe(n - 1);
}Erst den einfachsten Fall ohne Selbstaufruf beantworten, dann rekursiv weiterrechnen.
2Abbruchbedingung wird übersprungen
n == 0 bei Zweierschritten
So wird oft programmiert:
static int haelfte(int n) {
if (n == 0) {
return 0;
}
return 1 + haelfte(n - 2);
}haelfte(7) läuft über 1, −1, −3, … und endet nie.
Richtig ist: Die Abbruchbedingung muss jeden möglichen Weg abfangen — hier auch ungerade und negative Werte.
static int haelfte(int n) {
if (n <= 0) {
return 0;
}
return 1 + haelfte(n - 2);
}Terminierung prüfen: Kommt jeder Selbstaufruf der Abbruchbedingung näher, ohne sie zu überspringen?
3Ergebnis des Selbstaufrufs verworfen
zaehle(…); ohne return
So wird oft programmiert:
static int zaehle(String s, char c) {
if (s.length() == 0) {
return 0;
}
if (s.charAt(0) == c) {
return 1 + zaehle(s.substring(1), c);
}
zaehle(s.substring(1), c); // Ergebnis geht verloren
return 0;
}Richtig ist: Der Rückgabewert des Selbstaufrufs ist das Teilergebnis — er muss weitergegeben werden. Sonst liefert die Methode schon beim ersten Zeichen, das nicht passt, 0.
return zaehle(s.substring(1), c);
Jeder Rekursionsschritt muss das Teilergebnis verwenden — meist steht der Selbstaufruf in einem return.
4Reihenfolge beim Aufstieg übersehen
Ausgabe nach dem Selbstaufruf
So wird oft gedacht: p(3) gibt „321321“ aus:
static void p(int n) {
if (n > 0) {
System.out.print(n);
p(n - 1);
System.out.print(n);
}
}Richtig ist: Die zweite Ausgabe läuft erst beim Aufstieg, und dort endet der zuletzt begonnene Aufruf zuerst: Ausgabe 321123. Der Aufrufstapel arbeitet nach dem Prinzip „zuletzt hinein, zuerst hinaus“.
Was vor dem Selbstaufruf steht, läuft beim Abstieg; was danach steht, beim Aufstieg in umgekehrter Reihenfolge.
5Aufrufe und Tiefe verwechselt
Tiefe von fib(4) = 9?
So wird oft geantwortet: „fib(4) braucht 9 Aufrufe, also liegen 9 Rahmen auf dem Stapel.“
Richtig ist: Der Aufrufbaum hat 9 Knoten, gleichzeitig offen sind aber nur die Aufrufe auf einem Weg von der Wurzel zu einem Blatt: fib(4), fib(3), fib(2), fib(1) — Tiefe 4. Ebenso Mergesort: \(2n - 1\) Aufrufe, aber nur \(\log_2 n + 1\) gleichzeitig.
Rekursionstiefe = längster Weg im Aufrufbaum; Anzahl der Aufrufe = alle Knoten.
6Index hinter dem letzten Zeichen
s.charAt(s.length())
So wird oft programmiert: Im Palindrom-Test:
if (s.charAt(0) != s.charAt(s.length())) {
return false;
}Richtig ist: Wie bei Reihungen läuft der Index einer Zeichenkette von 0 bis s.length() - 1; s.charAt(s.length()) wirft eine StringIndexOutOfBoundsException. Der Rest ohne beide Randzeichen ist s.substring(1, s.length() - 1).
if (s.charAt(0) != s.charAt(s.length() - 1)) {
return false;
}Letztes Zeichen: s.charAt(s.length() - 1); substring(a, b) endet vor Index b.
7Suchbereich wird nicht kleiner
binSuche(a, x, mitte, rechts)
So wird oft programmiert: In der rekursiven binären Suche:
if (a[mitte] < x) {
return binSuche(a, x, mitte, rechts);
}Richtig ist: Bei zwei Elementen gilt mitte = links — der Bereich bleibt gleich, die Rekursion endet nie. a[mitte] ist nach dem Vergleich erledigt und gehört nicht mehr dazu.
if (a[mitte] < x) {
return binSuche(a, x, mitte + 1, rechts);
}
return binSuche(a, x, links, mitte - 1);Jeder Selbstaufruf muss einen echt kleineren Bereich bekommen: mitte ± 1.
8Teilbereiche mit Lücke
links … mitte − 1 und mitte + 1 … rechts
So wird oft programmiert: Teile und herrsche für das Maximum:
int maxL = maximum(a, links, mitte - 1); int maxR = maximum(a, mitte + 1, rechts);
Richtig ist: Die Teilbereiche müssen lückenlos aneinanderstoßen und dürfen sich nicht überlappen. Hier fehlt a[mitte]; bei zwei Elementen entsteht außerdem ein leerer Bereich, den der Basisfall links == rechts nicht abfängt.
int maxL = maximum(a, links, mitte); int maxR = maximum(a, mitte + 1, rechts);
Bei Teile und herrsche: links … mitte und mitte + 1 … rechts — jedes Element genau einmal.
9Rest beim Mischen vergessen
nur eine while-Schleife
So wird oft programmiert:
while (i <= mitte && j <= rechts) {
if (a[i] <= a[j]) { hilf[k] = a[i]; i++; }
else { hilf[k] = a[j]; j++; }
k++;
}
// zurückkopieren …Richtig ist: Die Schleife endet, sobald eine Hälfte leer ist. Die übrigen Elemente der anderen Hälfte müssen ohne Vergleich übernommen werden — sonst bleiben in hilf Nullen stehen.
while (i <= mitte) { hilf[k] = a[i]; i++; k++; }
while (j <= rechts) { hilf[k] = a[j]; j++; k++; }Nach der Hauptschleife des Mischens immer beide Reste übernehmen.
10Stabilität verspielt
a[i] < a[j] beim Mischen
So wird oft programmiert:
if (a[i] < a[j]) {
hilf[k] = a[i]; i++;
} else {
hilf[k] = a[j]; j++;
}Richtig ist: Die Sortierung stimmt zwar, aber bei gleichen Werten wird das rechte Element zuerst übernommen — gleiche Schlüssel tauschen ihre Reihenfolge. Mit <= gewinnt das linke, früher stehende Element: Mergesort bleibt stabil.
if (a[i] <= a[j]) {Stabiles Mischen: bei Gleichheit links zuerst — <=.
11Pivot erneut mitsortiert
quicksort(a, links, p)
So wird oft programmiert:
int p = zerlege(a, links, rechts); quicksort(a, links, p); quicksort(a, p + 1, rechts);
Richtig ist: Nach zerlege steht das Pivot an Index p endgültig. Wird es mitsortiert und liegt es ganz rechts, bleibt der Bereich gleich groß — endlose Rekursion.
quicksort(a, links, p - 1); quicksort(a, p + 1, rechts);
Das Pivot gehört zu keinem der beiden Teile.
12Laufzeit pauschal beurteilt
„Quicksort braucht immer n · log₂ n.“
So wird oft geantwortet: „Quicksort ist das schnellste Verfahren und braucht immer \(n\log_2 n\) Vergleiche; Mergesort sortiert ebenfalls in-place.“
Richtig ist: Quicksort braucht im Mittel etwa \(1{,}39\,n\log_2 n\), im ungünstigsten Fall (z. B. sortierte Eingabe, Pivot = letztes Element) \(\frac{n(n-1)}{2}\) Vergleiche bei Tiefe \(n\). Mergesort garantiert höchstens \(n\log_2 n\), braucht aber eine Hilfsreihung der Länge \(n\).
Beim Beurteilen immer günstigen, mittleren und ungünstigsten Fall sowie Speicher und Tiefe nennen.
