Tower of Hanoi Explained: Recursion, Logic & Real Code
Most recursion tutorials give you factorial or Fibonacci and call it a day. Tower of Hanoi is different — it's the puzzle that makes recursion click. It's used in computer science courses worldwide not because it's academically cute, but because it perfectly mirrors how recursive thinking actually works: break a big problem into a smaller version of itself, trust the process, and let the call stack do the heavy lifting. It shows up in coding interviews at companies like Google, Amazon, and Meta — not because they want you to memorise it, but because solving it live reveals whether you can think recursively under pressure.
The problem has a deceptively simple rule set: three pegs, N discs stacked from largest to smallest on the first peg, and you need to move the whole stack to the last peg. You can only move one disc at a time, and a larger disc can never sit on top of a smaller one. An iterative solution to this problem is nightmarishly complex. A recursive solution is about eight lines of code. That contrast is the entire point — recursion isn't just a technique, it's the right tool for problems with self-similar structure.
By the end of this article you'll understand not just how to write the Tower of Hanoi recursive function, but why the algorithm works, how to trace through a call stack manually, how to calculate the minimum number of moves, and how to answer the curveball interview questions that trip people up. We'll build the solution step by step so nothing feels like magic.
Why Recursion Is the Only Sane Approach Here
Before writing a single line of code, let's understand why recursion is the natural fit and not just a clever trick.
The key insight is this: to move N discs from peg A to peg C, you must first move the top N-1 discs out of the way (to peg B), then slide the biggest disc to peg C, then move the N-1 discs from peg B onto peg C. That's it. That's the whole algorithm.
Notice that moving N-1 discs is exactly the same problem — just smaller. And moving N-2 discs is the same problem, even smaller. This self-similar structure is the definition of a recursively solvable problem. Every recursive call is trusting a slightly smaller version of itself to just work.
The base case is when N equals 1. You don't need to move anything out of the way — you just pick up the single disc and place it directly on the destination peg. That's your stopping condition. Without it, the function calls itself forever and your stack overflows.
An iterative approach would require you to simulate the entire call stack manually, tracking state across every move. It exists, but it's harder to write, harder to read, and harder to verify. Recursion here isn't a shortcut — it's the problem speaking its own language.
public class TowerOfHanoi { /** * Moves 'discCount' discs from 'sourcePeg' to 'destinationPeg' * using 'auxiliaryPeg' as a temporary holding peg. * * @param discCount number of discs to move * @param sourcePeg the peg where the discs currently sit * @param destinationPeg the peg where the discs need to end up * @param auxiliaryPeg the spare peg used for temporary storage */ public static void moveDiscs( int discCount, char sourcePeg, char destinationPeg, char auxiliaryPeg) { // BASE CASE: only one disc left — move it directly, no setup needed if (discCount == 1) { System.out.println( "Move disc 1 from peg " + sourcePeg + " to peg " + destinationPeg ); return; // stop recursing — the problem is solved at this level } // STEP 1: Move the top (discCount - 1) discs out of the way // Destination becomes auxiliary so the big disc has room to land moveDiscs(discCount - 1, sourcePeg, auxiliaryPeg, destinationPeg); // STEP 2: Move the largest remaining disc to the destination // This is always a single, unobstructed move System.out.println( "Move disc " + discCount + " from peg " + sourcePeg + " to peg " + destinationPeg ); // STEP 3: Move the (discCount - 1) discs from auxiliary onto destination // Source is now the auxiliary peg where we parked them in Step 1 moveDiscs(discCount - 1, auxiliaryPeg, destinationPeg, sourcePeg); } public static void main(String[] args) { int numberOfDiscs = 3; System.out.println( "Solving Tower of Hanoi for " + numberOfDiscs + " discs:\n" ); // Standard setup: Source = A, Destination = C, Auxiliary = B moveDiscs(numberOfDiscs, 'A', 'C', 'B'); // Minimum moves formula: (2^n) - 1 = (2^3) - 1 = 7 int minimumMoves = (int) Math.pow(2, numberOfDiscs) - 1; System.out.println("\nTotal moves taken: " + minimumMoves); } }
Move disc 1 from peg A to peg C
Move disc 2 from peg A to peg B
Move disc 1 from peg C to peg B
Move disc 3 from peg A to peg C
Move disc 1 from peg B to peg A
Move disc 2 from peg B to peg C
Move disc 1 from peg A to peg C
Total moves taken: 7
Tracing the Call Stack: What Actually Happens Step by Step
Understanding the code is one thing. Understanding what the call stack looks like is what separates someone who memorised the solution from someone who truly gets recursion.
Let's trace N=3. The first call is moveDiscs(3, A, C, B). Before printing anything, it immediately calls moveDiscs(2, A, B, C). Before that prints anything, it calls moveDiscs(1, A, C, B) — which hits the base case and prints 'Move disc 1 from A to C'. Then control returns to the N=2 frame, which prints 'Move disc 2 from A to B', then calls moveDiscs(1, C, B, A), printing 'Move disc 1 from C to B'. The N=2 call is now complete. Control returns to the N=3 frame, which prints 'Move disc 3 from A to C', then kicks off the right-hand branch.
This is the critical mental model: recursive calls don't all run at once. Each frame is paused — frozen mid-execution — while it waits for a deeper call to return. The call stack is literally a stack of paused function frames. When a base case fires, the stack starts unwinding.
If you draw this as a tree, each node has a left child (move N-1 out of the way), a middle action (move the big disc), and a right child (move N-1 back on top). The output is an in-order traversal of that tree — which is a beautiful insight into how recursion and tree traversal are the same concept wearing different clothes.
public class TowerOfHanoiWithDepth { /** * Same algorithm as before, but with depth tracking added * so you can SEE the call stack building up and unwinding. */ public static void moveDiscs( int discCount, char sourcePeg, char destinationPeg, char auxiliaryPeg, int callDepth) { // Indent output proportional to call depth — visualises stack depth String indent = " ".repeat(callDepth); System.out.println( indent + "[Depth " + callDepth + "] moveDiscs(" + discCount + ", " + sourcePeg + " -> " + destinationPeg + ")" ); if (discCount == 1) { // Base case: deepest point — stack starts unwinding from here System.out.println( indent + " *** MOVE: disc 1 from " + sourcePeg + " to " + destinationPeg ); return; } // Left branch: clear the way for the big disc moveDiscs(discCount - 1, sourcePeg, auxiliaryPeg, destinationPeg, callDepth + 1); // Mid-point action: place the big disc on the destination System.out.println( indent + " *** MOVE: disc " + discCount + " from " + sourcePeg + " to " + destinationPeg ); // Right branch: place the smaller discs on top of the big disc moveDiscs(discCount - 1, auxiliaryPeg, destinationPeg, sourcePeg, callDepth + 1); } public static void main(String[] args) { System.out.println("--- Call Stack Visualisation for N=3 ---\n"); moveDiscs(3, 'A', 'C', 'B', 0); } }
[Depth 0] moveDiscs(3, A -> C)
[Depth 1] moveDiscs(2, A -> B)
[Depth 2] moveDiscs(1, A -> C)
*** MOVE: disc 1 from A to C
*** MOVE: disc 2 from A to B
[Depth 2] moveDiscs(1, C -> B)
*** MOVE: disc 1 from C to B
*** MOVE: disc 3 from A to C
[Depth 1] moveDiscs(2, B -> C)
[Depth 2] moveDiscs(1, B -> A)
*** MOVE: disc 1 from B to A
*** MOVE: disc 2 from B to C
[Depth 2] moveDiscs(1, A -> C)
*** MOVE: disc 1 from A to C
The Math Behind the Minimum Moves: Why 2ⁿ − 1?
Every correct solution to Tower of Hanoi with N discs takes exactly 2ⁿ − 1 moves. You can't do it in fewer. This isn't a fun fact — it's a provable consequence of the algorithm's structure, and understanding it will make you a better engineer.
Let's define T(n) as the minimum number of moves needed for N discs. For N=1, that's 1 move — the base case. For any N > 1, the algorithm does T(n-1) moves to shift the top stack to auxiliary, then 1 move for the big disc, then T(n-1) moves again to shift the small stack onto the destination.
So: T(n) = 2 × T(n-1) + 1. Unrolling this recurrence gives T(n) = 2ⁿ − 1. This is an exponential time complexity: O(2ⁿ). For 10 discs that's 1,023 moves. For 64 discs — the original legend says monks are solving the puzzle with 64 golden discs — it's over 18 quintillion moves. At one move per second, that's 585 billion years. The universe is about 14 billion years old. So no, the monks won't finish.
This exponential growth matters in real engineering. Any algorithm with O(2ⁿ) complexity is unusable at scale. Recognising this pattern — a recurrence that doubles at each step — is a core skill for analysing recursive algorithms in technical interviews.
public class HanoiMoveCounter { // Counts moves recursively — matches the recurrence T(n) = 2*T(n-1) + 1 public static long countMoves(int discCount) { if (discCount == 1) { return 1; // base case: one disc needs exactly one move } // Left branch moves + big disc move + right branch moves return 2 * countMoves(discCount - 1) + 1; } // Closed-form version: 2^n - 1 (much faster, same result) public static long countMovesFormula(int discCount) { return (long) Math.pow(2, discCount) - 1; } public static void main(String[] args) { System.out.printf("%-8s %-20s %-20s%n", "Discs", "Recursive Count", "Formula (2^n - 1)"); System.out.println("-".repeat(50)); // Show the exponential growth up to 20 discs for (int discCount = 1; discCount <= 20; discCount++) { long recursive = countMoves(discCount); long formula = countMovesFormula(discCount); System.out.printf("%-8d %-20d %-20d%n", discCount, recursive, formula); } // Dramatic illustration of exponential scale System.out.println("\nFor 64 discs: " + countMovesFormula(64) + " moves"); System.out.println("That's " + (countMovesFormula(64) / 31_557_600L) + " years at one move per second."); } }
--------------------------------------------------
1 1 1
2 3 3
3 7 7
4 15 15
5 31 31
6 63 63
7 127 127
8 255 255
9 511 511
10 1023 1023
11 2047 2047
12 4095 4095
13 8191 8191
14 16383 16383
15 32767 32767
16 65535 65535
17 131071 131071
18 262143 262143
19 524287 524287
20 1048575 1048575
For 64 discs: 18446744073709551615 moves
That's 584542046090 years at one move per second.
Storing Move History: A Practical Real-World Extension
Printing to the console is fine for learning, but in a real application — say, building a game, an animation engine, or a puzzle validator — you need to capture each move as structured data you can process, store, or replay.
This is where we extend the pure recursive solution into something production-ready. Instead of printing, we collect each move as an object and return a list. The algorithm is identical — the only change is how we record each step.
This pattern comes up constantly in real engineering. You might be implementing an undo/redo system, recording an audit trail, or building a step-by-step hint feature in an educational app. The recursion stays clean; you just thread a data structure through it to capture the side effects as pure data instead of console output.
It also makes testing dramatically easier. You can assert on the returned list rather than capturing stdout, which is fragile. Structured output is always preferable to print-based output in production code — this example shows exactly how to make that transition without complicating the recursive logic at all.
import java.util.ArrayList; import java.util.List; public class HanoiMoveRecorder { // Represents a single disc move — clean, testable, serialisable static class DiscMove { final int discNumber; final char fromPeg; final char toPeg; DiscMove(int discNumber, char fromPeg, char toPeg) { this.discNumber = discNumber; this.fromPeg = fromPeg; this.toPeg = toPeg; } @Override public String toString() { return "Disc " + discNumber + ": " + fromPeg + " -> " + toPeg; } } /** * Identical logic to the original — but instead of printing, * we append each move to a shared list that the caller owns. */ public static void recordMoves( int discCount, char sourcePeg, char destinationPeg, char auxiliaryPeg, List<DiscMove> moveHistory) { if (discCount == 1) { // Base case: record the single move and return moveHistory.add(new DiscMove(discCount, sourcePeg, destinationPeg)); return; } // Move smaller discs to auxiliary — record each step recordMoves(discCount - 1, sourcePeg, auxiliaryPeg, destinationPeg, moveHistory); // Record the big disc move moveHistory.add(new DiscMove(discCount, sourcePeg, destinationPeg)); // Move smaller discs from auxiliary to destination — record each step recordMoves(discCount - 1, auxiliaryPeg, destinationPeg, sourcePeg, moveHistory); } public static void main(String[] args) { int numberOfDiscs = 3; List<DiscMove> moveHistory = new ArrayList<>(); recordMoves(numberOfDiscs, 'A', 'C', 'B', moveHistory); System.out.println("Complete move history for " + numberOfDiscs + " discs:"); System.out.println("Total moves: " + moveHistory.size() + "\n"); // Now you can iterate, store to DB, send to a frontend, replay in a game, etc. for (int step = 0; step < moveHistory.size(); step++) { System.out.println("Step " + (step + 1) + ": " + moveHistory.get(step)); } // Verify against the formula — they must always match long expectedMoves = (long) Math.pow(2, numberOfDiscs) - 1; boolean isOptimal = moveHistory.size() == expectedMoves; System.out.println("\nSolution is optimal: " + isOptimal); } }
Total moves: 7
Step 1: Disc 1: A -> C
Step 2: Disc 2: A -> B
Step 3: Disc 1: C -> B
Step 4: Disc 3: A -> C
Step 5: Disc 1: B -> A
Step 6: Disc 2: B -> C
Step 7: Disc 1: A -> C
Solution is optimal: true
| Aspect | Recursive Solution | Iterative Solution |
|---|---|---|
| Code length | ~10 lines of logic | 40-60 lines with manual stack simulation |
| Readability | Maps directly to the problem's logic | Hard to follow — simulates what the OS does for you |
| Call stack usage | O(n) stack frames at any one time | O(n) explicit stack objects on the heap |
| Performance | Same number of moves: 2ⁿ − 1 | Same number of moves: 2ⁿ − 1 |
| Risk of stack overflow | Yes, for very large N (N > ~5000 in Java) | No — uses heap instead of call stack |
| Interview preference | Almost always expected recursively first | Sometimes asked as a follow-up challenge |
| Testability | Easy — pure function with predictable output | Harder — more state to track and verify |
| Best used when | N is small to moderate (educational, games, puzzles) | N is very large and stack depth is a concern |
🎯 Key Takeaways
- The recursive solution works because the problem has self-similar structure — moving N discs always reduces to moving N-1 discs twice with one single move in between.
- The base case is discCount == 1, not 0 — getting this wrong either causes StackOverflowError or silently skips single-disc moves, giving an incorrect move count.
- The minimum number of moves is always exactly 2ⁿ − 1 — this is provable from the recurrence T(n) = 2T(n-1) + 1, and no algorithm can solve it in fewer moves.
- Replacing print statements with a move list (the accumulator pattern) is the real-world version of this algorithm — it makes the output testable, storable, and replayable without touching the recursive logic.
⚠ Common Mistakes to Avoid
- ✕Mistake 1: Swapping auxiliaryPeg and destinationPeg in the recursive calls — Symptom: the output looks almost right but some discs end up on the wrong peg, often a violation of the 'no large on small' rule — Fix: remember the role rotation pattern: (source → destination), then the next call swaps destination and auxiliary. Write it out in English first before coding: 'move N-1 to the spare, move the big one to the goal, move N-1 from the spare to the goal'.
- ✕Mistake 2: Missing or wrong base case — Symptom: StackOverflowError, or the function terminates early and prints nothing for N=1 — Fix: the base case must be discCount == 1 (not 0). If you use 0, you'll move a phantom disc. If you use discCount <= 0 without a print statement, the single-disc case silently does nothing and your move count will be wrong.
- ✕Mistake 3: Using int instead of long for move counts at large N — Symptom: for N >= 32, countMoves() returns a negative number or overflows silently — Fix: the return type and any accumulator variables must be long. For N=32, 2^32 - 1 is 4,294,967,295 — well beyond Integer.MAX_VALUE of 2,147,483,647. Switch to long or BigInteger immediately when scaling up.
Interview Questions on This Topic
- QWhat is the minimum number of moves to solve Tower of Hanoi for N discs, and can you derive that formula from the recurrence relation rather than just stating it?
- QHow would you modify the Tower of Hanoi solution to return all the moves as a list instead of printing them, and why might that be more useful in a production system?
- QTower of Hanoi has O(2ⁿ) time complexity — if an interviewer asks you to make it faster, how do you respond? (The answer: you can't. 2ⁿ − 1 moves is provably optimal. The right answer is to explain WHY, not scramble for an optimisation that doesn't exist.)
Frequently Asked Questions
What is the time complexity of Tower of Hanoi and why?
The time complexity is O(2ⁿ) where n is the number of discs. This comes directly from the recurrence T(n) = 2T(n-1) + 1, which solves to 2ⁿ − 1. Every additional disc doubles the number of required moves, making this an exponential algorithm that becomes impractical for large N.
Can Tower of Hanoi be solved without recursion?
Yes, but it's much more complex. The iterative solution either uses a loop with a manual stack structure to simulate the call stack, or relies on a mathematical pattern (odd-numbered discs always move in one direction, even-numbered in the other). It produces the same 2ⁿ − 1 moves — there's no performance gain, only a trade-off between call stack depth and code complexity.
Why does swapping the peg parameters in the recursive calls break everything?
Each recursive call assigns a different role — source, destination, and auxiliary — to each peg. The roles rotate with each call level. If you swap auxiliary and destination in either recursive call, you're telling discs to travel to the wrong peg, which either violates the 'no large on small' constraint or leaves discs on the wrong final peg. The easiest fix is to think of each call in plain English before writing code: 'I want to move N-1 discs to the spare peg, then the big disc to the goal, then N-1 discs from the spare to the goal.'
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.