In der Serie "Named Problems" stelle ich dir klassische, namentlich bekannte Algorithmus-Probleme vor, die seit Jahrzehnten in Ausbildung, Studium und Interviews verwendet werden. Anhand verständlicher Problemstellungen wie FizzBuzz, Türme von Hanoi oder dem Sieb des Eratosthenes lernst du hier, algorithmisch zu denken, Lösungsstrategien zu entwickeln und diese sauber in Java umzusetzen.
Die Türme von Hanoi sind ein bekanntes Denkspiel: Scheiben müssen nach festen Regeln von einem Stab auf einen anderen umgezogen werden.
Das Problem ist berühmt, weil es sich fast wie von selbst in kleinere Teilprobleme zerlegen lässt. Genau das ist der Kern von Rekursion.
In diesem Artikel baust du dir ein klares Modell im Kopf auf und setzt es in Java um: erst straightforward, dann als Alternative mit einem iterativen Ansatz.
Wenn du danach Rekursion bisher eher „magisch“ fandest: hier wird sie nachvollziehbar und praktisch.
Problemstellung
Du hast drei Stäbe (z.B. A, B, C) und n Scheiben. Alle Scheiben liegen zu Beginn sortiert auf A (die größte unten, die kleinste oben). Ziel ist es, alle Scheiben nach C zu bewegen.
- Du darfst immer nur eine Scheibe pro Zug bewegen.
- Du darfst nur die oberste Scheibe eines Stapels nehmen.
- Eine größere Scheibe darf nie auf einer kleineren liegen.
Mini-Beispiel für n = 3 (Start: alle auf A, Ziel: C):
- Bewege Scheibe 1 von
AnachC - Bewege Scheibe 2 von
AnachB - Bewege Scheibe 1 von
CnachB - Bewege Scheibe 3 von
AnachC - Bewege Scheibe 1 von
BnachA - Bewege Scheibe 2 von
BnachC - Bewege Scheibe 1 von
AnachC
Die Idee dahinter
Kernkonzept: Rekursion und Problemzerlegung. Statt „n Scheiben auf einmal“ zu lösen, zerlegst du es so:
- Um
nScheiben vonfromnachtozu bewegen, musst du zuerstn-1Scheiben vonfromnachauxbewegen. - Dann bewegst du die größte Scheibe (Scheibe
n) vonfromnachto. - Zum Schluss bewegst du die
n-1Scheiben vonauxnachto.
Die Visualisierung als kurze Liste:
move(n, from, to, aux)- 1)
move(n-1, from, aux, to) - 2)
move disk n: from -> to - 3)
move(n-1, aux, to, from)
Schritt für Schritt zur Lösung
- Definiere das Ziel: Alle Scheiben sollen von
AnachC. - Formuliere die Funktion:
move(n, from, to, aux)beschreibt genau diese Aufgabe. - Setze die Abbruchbedingung: Wenn
n == 0, gibt es nichts zu tun. - Zerlege das Problem: Bewege erst
n-1Scheiben zur Hilfsstangeaux. - Führe den Hauptzug aus: Bewege Scheibe
nvonfromnachto. - Räume auf: Bewege die
n-1Scheiben vonauxnachto. - Teste klein: Starte mit
n = 1, dannn = 2, dannn = 3.
Java-Implementierung (Variante A: straightforward)
public class TowersOfHanoiStraightforward {
public static void main(String[] args) {
int n = 3;
solve(n, 'A', 'C', 'B');
}
public static void solve(int n, char from, char to, char aux) {
if (n < 0) {
throw new IllegalArgumentException("n must be >= 0");
}
move(n, from, to, aux);
}
private static void move(int n, char from, char to, char aux) {
if (n == 0) {
return; // Abbruchbedingung: nichts zu bewegen
}
move(n - 1, from, aux, to);
printMove(n, from, to);
move(n - 1, aux, to, from);
}
private static void printMove(int disk, char from, char to) {
System.out.println("Move disk " + disk + " from " + from + " to " + to);
}
}
Diese Variante zeigt die Rekursion am klarsten: move beschreibt die Problemzerlegung direkt. Wichtig ist die Abbruchbedingung n == 0, damit die Rekursion endet. Die Methode printMove hält die Ausgabe getrennt, damit move leicht zu lesen bleibt.
Java-Implementierung (Variante B: Alternative oder Verbesserung)
import java.util.ArrayDeque;
import java.util.Deque;
public class TowersOfHanoiIterative {
private record Frame(int n, char from, char to, char aux, int stage) {
Frame nextStage() {
return new Frame(n, from, to, aux, stage + 1);
}
}
public static void main(String[] args) {
int n = 3;
solveIterative(n, 'A', 'C', 'B');
}
public static void solveIterative(int n, char from, char to, char aux) {
if (n < 0) {
throw new IllegalArgumentException("n must be >= 0");
}
if (n == 0) {
return;
}
Deque<Frame> stack = new ArrayDeque<>();
stack.push(new Frame(n, from, to, aux, 0));
while (!stack.isEmpty()) {
Frame f = stack.pop();
if (f.n() == 0) {
continue;
}
if (f.stage() == 0) {
stack.push(f.nextStage());
stack.push(new Frame(f.n() - 1, f.from(), f.aux(), f.to(), 0));
} else if (f.stage() == 1) {
printMove(f.n(), f.from(), f.to());
stack.push(f.nextStage());
} else {
stack.push(new Frame(f.n() - 1, f.aux(), f.to(), f.from(), 0));
}
}
}
private static void printMove(int disk, char from, char to) {
System.out.println("Move disk " + disk + " from " + from + " to " + to);
}
}
Hier simulierst du den Aufruf-Stack der Rekursion mit einer eigenen Stack-Datenstruktur (ArrayDeque). Das ist eine gute Übung, um zu verstehen, was bei Rekursion „unter der Haube“ passiert. Die Frame-Datenstruktur merkt sich Parameter und eine kleine stage-Zustandsvariable, damit die Reihenfolge exakt wie bei der rekursiven Variante bleibt.
Beispiel: Eingabe und Ausgabe
Beispiel 1: Eingabe n=1 (Start A, Ziel C, Hilfe B)
Output: Move disk 1 from A to C
Beispiel 2: Eingabe n=2 (Start A, Ziel C, Hilfe B)
Output (3 Zeilen): Move disk 1 from A to B, Move disk 2 from A to C, Move disk 1 from B to C
Beispiel 3: Eingabe n=3 (Start A, Ziel C, Hilfe B)
Output beginnt mit: Move disk 1 from A to C, Move disk 2 from A to B, Move disk 1 from C to B ... und endet mit Move disk 1 from A to C
Typische Fehler
- Falsche Abbruchbedingung:
if (n == 1) return;ist falsch, weil du dann den letzten Zug nie ausgibst. - Off-by-one bei der Scheibennummer: Manche geben bei
printMoveversehentlichn-1aus stattn. - Parameter vertauscht: Besonders häufig:
auxundtoin einem der rekursiven Aufrufe falsch gesetzt. - Negatives n nicht behandelt: Ohne Guard kann die Rekursion unendlich weiterlaufen oder zu unsinnigen Ausgaben führen.
- Zu viel Logik in einer Methode: Wenn Ausgabe, Validierung und Rekursion vermischt werden, wird der Code schnell unleserlich.
- Iterative Simulation ohne Zustand: In der iterativen Variante braucht man eine Art „stage“, sonst verliert man die Reihenfolge (erst links, dann Mitte, dann rechts).
Fazit
Bei den Türmen von Hanoi siehst du sehr klar, wie Rekursion durch Problemzerlegung funktioniert: ein großes Problem wird zu zwei kleineren plus einem einfachen Schritt.
Du hast eine einfache rekursive Lösung umgesetzt und gelernt, wie man dasselbe Verhalten iterativ mit einem eigenen Stack nachbaut. Wichtig ist vor allem: Abbruchbedingung sauber definieren, Parameter konsistent halten, und erst mit kleinen n testen.
