Home DSA Tower of Hanoi Explained: Recursion, Logic & Real Code

Tower of Hanoi Explained: Recursion, Logic & Real Code

In Plain English 🔥
Imagine you're moving house, but your van only fits one box at a time, and you're not allowed to stack a big box on top of a small one. You have three spots to park boxes — your old place, your new place, and a friend's driveway as a temporary stop. Tower of Hanoi is exactly that puzzle: move a stack of discs from peg A to peg C, one disc at a time, never placing a bigger disc on a smaller one. The magic is that solving it for N discs always means first solving it for N-1 discs — which is exactly what recursion is built for.
⚡ Quick Answer
Imagine you're moving house, but your van only fits one box at a time, and you're not allowed to stack a big box on top of a small one. You have three spots to park boxes — your old place, your new place, and a friend's driveway as a temporary stop. Tower of Hanoi is exactly that puzzle: move a stack of discs from peg A to peg C, one disc at a time, never placing a bigger disc on a smaller one. The magic is that solving it for N discs always means first solving it for N-1 discs — which is exactly what recursion is built for.

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.

TowerOfHanoi.java · JAVA
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
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);
    }
}
▶ Output
Solving Tower of Hanoi for 3 discs:

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
⚠️
Pro Tip:Notice how the peg labels rotate between the three recursive roles — source, destination, auxiliary — in each call. If you get confused tracing it, label them by ROLE, not by letter. The letter is just a name; the role is what matters.

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.

TowerOfHanoiWithDepth.java · JAVA
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
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);
    }
}
▶ Output
--- Call Stack Visualisation for N=3 ---

[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
🔥
Interview Gold:When an interviewer says 'trace through this for N=3', use this indented format on the whiteboard. It shows you understand the call stack, not just the output. That distinction gets you the offer.

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.

HanoiMoveCounter.java · JAVA
123456789101112131415161718192021222324252627282930313233343536
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.");
    }
}
▶ Output
Discs Recursive Count Formula (2^n - 1)
--------------------------------------------------
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.
⚠️
Watch Out:Use long, not int, when computing 2ⁿ − 1 for large N. For N=32, (int) Math.pow(2, 32) - 1 overflows silently and gives you -1. Always cast to long or use BigInteger for anything above N=30.

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.

HanoiMoveRecorder.java · JAVA
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
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);
    }
}
▶ Output
Complete move history for 3 discs:
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
⚠️
Pro Tip:Passing a mutable list into a recursive function (the 'accumulator pattern') is cleaner than returning a list and merging results at every call frame. It avoids creating a new List per recursive call and keeps memory usage at O(2ⁿ) for the list rather than adding O(n) overhead per call frame for merging.
AspectRecursive SolutionIterative Solution
Code length~10 lines of logic40-60 lines with manual stack simulation
ReadabilityMaps directly to the problem's logicHard to follow — simulates what the OS does for you
Call stack usageO(n) stack frames at any one timeO(n) explicit stack objects on the heap
PerformanceSame number of moves: 2ⁿ − 1Same number of moves: 2ⁿ − 1
Risk of stack overflowYes, for very large N (N > ~5000 in Java)No — uses heap instead of call stack
Interview preferenceAlmost always expected recursively firstSometimes asked as a follow-up challenge
TestabilityEasy — pure function with predictable outputHarder — more state to track and verify
Best used whenN 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.'

🔥
TheCodeForge Editorial Team Verified Author

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.

← PreviousRecursion vs IterationNext →Divide and Conquer Technique
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged