MINT lernen

Typische Fehler — was oft schiefgeht

Zwölf Fehler, die in Klausuren zu Rekursion, Mergesort und Quicksort am meisten Punkte kosten — jeweils mit dem falschen Code, dem richtigen Weg und einem Merksatz.

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.