Skip to content

Tower of Hanoi in C#

The recursive solution in modern C#, stepped through beside the board — then an iterator whose moves LINQ can count and search without ever storing them.

On this page
  1. The recursive solution, running
  2. Run it yourself
  3. Line by line
  4. Counting the moves
  5. Moves on demand, queried with LINQ
  6. The same algorithm in other languages
  7. Frequently asked questions

The recursive solution, running

Step through the program. The lit line is the one executing, the towers are labelled for the call that is running, and the stack lists every call still waiting to finish.

Hanoi(3, 'A', 'C', 'B')

Move 3 disks from A to C.

Nothing is running yet. The first step makes this call.

Step 0 of 43

Call stack

Depth 0 of 4 max

  1. Empty. Nothing has been called yet.

Hanoi.cs

using System;

Hanoi(3, 'A', 'C', 'B');

static void Hanoi(int n, char source, char target, char spare)
{
    if (n == 0)
    {
        return;
    }
    Hanoi(n - 1, source, spare, target);
    Console.WriteLine($"Move disk {n} from {source} to {target}");
    Hanoi(n - 1, spare, target, source);
}

Moves

7 in all

Choose a row to jump the program to that move.

Every move the program makes for 3 disks
MoveDiskFromTo
11AC
22AB
31CB
43AC
51BA
62BC
71AC

Run it yourself

dotnet new console creates a project whose Program.cs already uses top-level statements, so paste the listing over that file's contents as it is. Top-level statements need C# 9, which means .NET 5 or later; the console template has produced them by default since .NET 6:

Run it

dotnet new console -o Hanoi
cd Hanoi
# replace Program.cs with the listing, then:
dotnet run

It prints the seven moves of the three-disk puzzle:

Output

Move disk 1 from A to C
Move disk 2 from A to B
Move disk 1 from C to B
Move disk 3 from A to C
Move disk 1 from B to A
Move disk 2 from B to C
Move disk 1 from A to C

Change the 3 in the call to solve a bigger puzzle. Every disk you add doubles the output, plus one line.

Line by line

Not what each line says — the listing says that — but why it has to be there:

LineWhy it is there
1Console lives in System. A project with implicit usings turned on supplies this line for you, but the listing carries it so that it compiles unchanged wherever it is pasted.
3Top-level statements, C# 9 and later: the program starts at the first statement in the file, so there is no Main and no class to write around a function that needs neither. Three disks, source A, target C, and B left over as the spare.
5With top-level statements, Hanoi is a local function inside a Main the compiler writes for you. Every value it needs arrives as an argument, so it captures nothing either way; static turns that from a fact into a rule the compiler enforces, so a later edit cannot quietly make it depend on a variable declared above it.
7–10Recursion needs a size it can answer without calling itself, or it never stops. Zero disks is that size: nothing to move, so return. The count is an int, and C# has no implicit conversion from double, so Hanoi(2.5, …) is a compile error rather than a recursion that steps over zero and runs out of stack.
11Disk n is heading for target, so target cannot also be where the n - 1 disks above it wait. They go to spare — which is why the last two arguments swap places on the way in.
12The only line that moves a disk. It is legal exactly here because line 11 has just cleared everything smaller than n off both the source and the target.
13Line 11 in reverse: the disks are on spare, so spare is the source now, and the tower they began on is free to be the working space.

Why those three lines are enough — the correctness proof, the recurrence and the complexity — is on the Tower of Hanoi algorithm page, and holds for every language here.

Counting the moves

CountMoves.cs

using System;
using System.Numerics;

// ulong is unsigned 64-bit: 2^64 - 1 is exactly its largest value.
ulong moves = ulong.MaxValue;
Console.WriteLine($"ulong:      {moves}");

// By default, arithmetic that overflows wraps round silently...
ulong wrapped = moves + 1;
Console.WriteLine($"wrapped:    {wrapped}");

// ...unless you ask for it to be checked.
try
{
    ulong next = checked(moves + 1);
    Console.WriteLine($"checked:    {next}");
}
catch (OverflowException)
{
    Console.WriteLine("checked:    OverflowException");
}

// UInt128, since .NET 7, is twice as wide: its maximum is the 128-disk count.
Console.WriteLine($"UInt128:    {UInt128.MaxValue}");

// BigInteger has no ceiling at all.
Console.WriteLine($"BigInteger: {BigInteger.Pow(2, 64) - 1}");

Run it

# in the same project, paste over Program.cs, then:
dotnet run

Output

ulong:      18446744073709551615
wrapped:    0
checked:    OverflowException
UInt128:    340282366920938463463374607431768211455
BigInteger: 18446744073709551615

ulong is an unsigned 64-bit integer, and its maximum, ulong.MaxValue, is exactly 2⁶⁴ − 1: the 64-disk move count fits with not one value to spare. Java has no such type, which is why the obvious way to write the count there quietly prints 0.

What happens one past it depends on context. C# arithmetic is unchecked by default, so moves + 1 silently wraps round to 0. Inside checked(...) the same addition throws an OverflowException instead. To make that the rule for a whole project, set <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow> in the .csproj.

Past 64 disks there are two ways on. UInt128, added in .NET 7, is an ordinary value type twice as wide, and its maximum is the 128-disk move count exactly. BigInteger, in System.Numerics, has no upper limit at all.

Moves on demand, queried with LINQ

HanoiIterator.cs

using System;
using System.Collections.Generic;

foreach (var (disk, fromPeg, toPeg) in Hanoi(3, 'A', 'C', 'B'))
{
    Console.WriteLine($"Move disk {disk} from {fromPeg} to {toPeg}");
}

// An iterator: each move is produced only when the loop above asks for the next one.
static IEnumerable<(int Disk, char From, char To)> Hanoi(int n, char source, char target, char spare)
{
    if (n == 0)
    {
        yield break;
    }
    foreach (var move in Hanoi(n - 1, source, spare, target))
    {
        yield return move;
    }
    yield return (n, source, target);
    foreach (var move in Hanoi(n - 1, spare, target, source))
    {
        yield return move;
    }
}

Run it

# in the same project, paste over Program.cs, then:
dotnet run

yield return turns Hanoi into an iterator. Calling it runs none of its body; each time the foreach asks for the next move, the method runs just far enough to produce one, and yield break ends a call with nothing to give. The moves are named tuples, (int Disk, char From, char To), and the foreach takes each one apart with deconstruction.

The two inner foreach loops are the part C# makes you write out. A JavaScript generator hands a whole nested generator on with yield*; C# has no equivalent, so every move a smaller call produces is yielded again by hand, once for each call it passes through on the way up. A move therefore costs work proportional to the depth of the call that made it, and every call builds an iterator object of its own — fifteen of them for three disks, 2ⁿ⁺¹ − 1 in general. At any size you could watch, that is too little to notice.

What C# gives back for that is LINQ. The moves are an IEnumerable, so every LINQ operator works on the solution without storing it, and each runs the iterator only as far as it needs to. For ten disks, Hanoi(10, 'A', 'C', 'B') answers:

  • .Count() is 1,023 — every move, 2¹⁰ − 1 of them.
  • .Count(m => m.Disk == 1) is 512: the smallest disk makes half of all the moves, one on every odd-numbered move.
  • .Count(m => m.Disk == 10) is 1: the largest disk crosses once.
  • .TakeWhile(m => m.Disk != 10).Count() + 1 is 512, so that crossing is move 2⁹ — the exact middle of the solution.

Those are facts the algorithm page proves, checked here by asking the moves rather than reading 1,023 lines of them. And .First(m => m.Disk == 10) returns (10, A, C) having produced only the 512 moves up to it: the iterator is simply never asked for the rest.

The same algorithm in other languages

Frequently asked questions

How do you write Tower of Hanoi in C#?

With a recursive method: if n is 0, return; otherwise call Hanoi(n - 1, source, spare, target), write the move of disk n with Console.WriteLine, and call Hanoi(n - 1, spare, target, source). With top-level statements the whole program is the call Hanoi(3, 'A', 'C', 'B') followed by that method.

What is the largest Tower of Hanoi move count a C# ulong can hold?

Exactly the 64-disk count. ulong.MaxValue is 2⁶⁴ − 1, which is 18,446,744,073,709,551,615. One more wraps round to 0 in the default unchecked context, or throws an OverflowException inside checked. For larger counts use UInt128, which holds every count up to 128 disks, or System.Numerics.BigInteger, which has no limit.

How do you count or search Tower of Hanoi moves in C# without storing them?

Write the solver as an iterator that returns IEnumerable<(int Disk, char From, char To)> and uses yield return instead of printing. LINQ then works on the moves lazily: Hanoi(10, 'A', 'C', 'B').Count(m => m.Disk == 1) returns 512 without ever holding the 1,023 moves in memory, and First stops the iterator as soon as it finds the move it was asked for.