Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions src/main/java/com/thealgorithms/recursion/TowerOfHanoi.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.thealgorithms.recursion;

import java.util.ArrayList;
import java.util.List;

/**
* A utility class to solve the Tower of Hanoi problem using recursion.
*
* The Tower of Hanoi is a classic recursive algorithm where the objective is to move all disks
* from the source peg to the destination peg following these rules:
* 1. Only one disk can be moved at a time.
* 2. A disk can only be moved if it is the topmost disk on a peg.
* 3. No larger disk may be placed on top of a smaller disk.
*
* The recursive solution divides the problem into three steps:
* Step 1: Move (n-1) disks from source to auxiliary peg.
* Step 2: Move the largest disk from source to destination peg.
* Step 3: Move (n-1) disks from auxiliary to destination peg.
*
* Time Complexity: O(2^n - 1)
* Space Complexity: O(n) - recursion call stack depth
*/
public final class TowerOfHanoi {

private TowerOfHanoi() {
}

/**
* Solves the Tower of Hanoi problem for n disks.
*
* @param n number of disks (must be at least 1)
* @param source source peg (typically 'A')
* @param destination destination peg (typically 'C')
* @param auxiliary auxiliary peg (typically 'B')
* @return a list of moves required to solve the problem
* @throws IllegalArgumentException if n is less than 1
*/
public static List<String> solve(int n, char source, char destination, char auxiliary) {
if (n < 1) {
throw new IllegalArgumentException("Number of disks must be at least 1");
}

List<String> moves = new ArrayList<>();
moveDisks(n, source, destination, auxiliary, moves);
return moves;
}

/**
* Recursive helper to move disks from source to destination using auxiliary peg.
* Implements the three-step Tower of Hanoi algorithm.
*
* @param n number of disks to move
* @param source source peg
* @param destination destination peg
* @param auxiliary auxiliary peg
* @param moves list to accumulate moves
*/
private static void moveDisks(int n, char source, char destination, char auxiliary, List<String> moves) {
if (n == 1) {
moves.add("Move disk 1 from " + source + " to " + destination);
return;
}

moveDisks(n - 1, source, auxiliary, destination, moves);
moves.add("Move disk " + n + " from " + source + " to " + destination);
moveDisks(n - 1, auxiliary, destination, source, moves);
}

/**
* Calculates the number of moves required to solve Tower of Hanoi for n disks.
* Formula: 2^n - 1
*
* @param n number of disks (must be at least 1)
* @return the number of moves required
* @throws IllegalArgumentException if n is less than 1
*/
public static long getMoveCount(int n) {
if (n < 1) {
throw new IllegalArgumentException("Number of disks must be at least 1");
}
return (1L << n) - 1;
}
}
105 changes: 105 additions & 0 deletions src/test/java/com/thealgorithms/recursion/TowerOfHanoiTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.thealgorithms.recursion;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

public class TowerOfHanoiTest {

@ParameterizedTest
@MethodSource("diskCountAndMoveCount")
void testMoveCount(int disks, long expectedMoves) {
assertEquals(expectedMoves, TowerOfHanoi.getMoveCount(disks));
}

private static Stream<Arguments> diskCountAndMoveCount() {
return Stream.of(Arguments.of(1, 1L), Arguments.of(2, 3L), Arguments.of(3, 7L), Arguments.of(4, 15L), Arguments.of(5, 31L), Arguments.of(10, 1023L));
}

@ParameterizedTest
@MethodSource("diskCountAndExpectedMoves")
void testSolveReturnsCorrectNumberOfMoves(int disks, long expectedMoveCount) {
List<String> moves = TowerOfHanoi.solve(disks, 'A', 'C', 'B');
assertEquals(expectedMoveCount, moves.size());
}

private static Stream<Arguments> diskCountAndExpectedMoves() {
return Stream.of(Arguments.of(1, 1L), Arguments.of(2, 3L), Arguments.of(3, 7L), Arguments.of(4, 15L), Arguments.of(5, 31L));
}

@Test
void testSolveWithOneDisks() {
List<String> moves = TowerOfHanoi.solve(1, 'A', 'C', 'B');
assertEquals(1, moves.size());
assertEquals("Move disk 1 from A to C", moves.get(0));
}

@Test
void testSolveWithTwoDisks() {
List<String> moves = TowerOfHanoi.solve(2, 'A', 'C', 'B');
assertEquals(3, moves.size());
assertEquals("Move disk 1 from A to B", moves.get(0));
assertEquals("Move disk 2 from A to C", moves.get(1));
assertEquals("Move disk 1 from B to C", moves.get(2));
}

@Test
void testSolveWithThreeDisks() {
List<String> moves = TowerOfHanoi.solve(3, 'A', 'C', 'B');
assertEquals(7, moves.size());
assertEquals("Move disk 1 from A to C", moves.get(0));
assertEquals("Move disk 3 from A to C", moves.get(3));
assertEquals("Move disk 1 from A to C", moves.get(6));
}

@Test
void testSolveWithDifferentPegs() {
List<String> moves = TowerOfHanoi.solve(2, 'X', 'Z', 'Y');
assertEquals(3, moves.size());
assertEquals("Move disk 1 from X to Y", moves.get(0));
assertEquals("Move disk 2 from X to Z", moves.get(1));
assertEquals("Move disk 1 from Y to Z", moves.get(2));
}

@Test
void testThrowsForNegativeDisks() {
assertThrows(IllegalArgumentException.class, () -> TowerOfHanoi.solve(-1, 'A', 'C', 'B'));
}

@Test
void testThrowsForZeroDisks() {
assertThrows(IllegalArgumentException.class, () -> TowerOfHanoi.solve(0, 'A', 'C', 'B'));
}

@Test
void testGetMoveCountThrowsForNegativeDisks() {
assertThrows(IllegalArgumentException.class, () -> TowerOfHanoi.getMoveCount(-1));
}

@Test
void testGetMoveCountThrowsForZeroDisks() {
assertThrows(IllegalArgumentException.class, () -> TowerOfHanoi.getMoveCount(0));
}

@ParameterizedTest
@MethodSource("diskCountAndExpectedMoves")
void testSolveMoveCountMatchesGetMoveCount(int disks, long expectedMoveCount) {
List<String> moves = TowerOfHanoi.solve(disks, 'A', 'C', 'B');
long moveCount = TowerOfHanoi.getMoveCount(disks);
assertEquals(moveCount, moves.size());
assertEquals(expectedMoveCount, moveCount);
}

@Test
void testSolveIsNotEmpty() {
List<String> moves = TowerOfHanoi.solve(1, 'A', 'C', 'B');
assertFalse(moves.isEmpty());
}
}
Loading