每日算法 — 使用java实现汉诺塔:迭代解法与栈模拟非递归实现

引言

汉诺塔问题是计算机科学中最经典的递归示例之一。传统的递归解法优雅简洁,但在实际工程中,递归可能面临栈溢出和性能问题。本文将用Java实现汉诺塔的完全非递归解法,包括基于栈显式模拟递归过程,以及利用二进制模式直接计算移动序列的高效迭代算法。

问题回顾

汉诺塔规则:有三根柱子A、B、C,A柱上有 $n$ 个大小不一的圆盘,大盘在下小盘在上。目标是将所有圆盘从A移动到C,每次只能移动一个圆盘,且大盘不能压在小盘上。

经典递归解法:
1. 将 $n-1$ 个盘从A经C移到B
2. 将第 $n$ 个盘从A移到C
3. 将 $n-1$ 个盘从B经A移到C

递归深度为 $n$,总移动次数为 $2^n – 1$。

二进制模式迭代法

汉诺塔的移动序列与二进制计数存在深刻联系:
– 第 $k$ 步(从1开始)移动的盘号等于 $k$ 的二进制末尾0的个数加1。
– 盘号的移动方向:奇数盘顺时针(A→B→C→A),偶数盘逆时针(A→C→B→A)。

利用这一性质,可在 $O(1)$ 空间内直接计算第 $k$ 步的移动,无需维护调用栈。

Java实现

圆盘与状态定义

public class Disk {
    int size;      // 盘的大小(编号)
    char from;     // 起始柱
    char to;       // 目标柱
    char aux;      // 辅助柱

    public Disk(int size, char from, char to, char aux) {
        this.size = size;
        this.from = from;
        this.to = to;
        this.aux = aux;
    }

    @Override
    public String toString() {
        return String.format("Disk[%d]: %c -> %c (aux=%c)", size, from, to, aux);
    }
}

public class TowerState {
    private Stack<Integer>[] pegs; // 三根柱子上的盘
    private int n;
    private int moveCount;

    @SuppressWarnings("unchecked")
    public TowerState(int n) {
        this.n = n;
        this.pegs = new Stack[3];
        for (int i = 0; i < 3; i++) pegs[i] = new Stack<>();
        // A柱(0)上放置n到1的盘
        for (int i = n; i >= 1; i--) pegs[0].push(i);
        this.moveCount = 0;
    }

    public boolean move(int from, int to) {
        if (pegs[from].isEmpty()) return false;
        if (!pegs[to].isEmpty() && pegs[from].peek() > pegs[to].peek()) return false;
        int disk = pegs[from].pop();
        pegs[to].push(disk);
        moveCount++;
        System.out.printf("Step %d: Move disk %d from %c to %c%n",
            moveCount, disk, (char)('A'+from), (char)('A'+to));
        return true;
    }

    public boolean isComplete() {
        return pegs[0].isEmpty() && pegs[1].isEmpty() && pegs[2].size() == n;
    }

    public void printState() {
        System.out.println("  A: " + pegs[0]);
        System.out.println("  B: " + pegs[1]);
        System.out.println("  C: " + pegs[2]);
    }
}

递归解法(基准)

public class HanoiRecursive {
    private int moveCount;

    public void solve(int n, char from, char to, char aux) {
        if (n <= 0) return;
        solve(n - 1, from, aux, to);
        System.out.printf("Move disk %d from %c to %c%n", n, from, to);
        moveCount++;
        solve(n - 1, aux, to, from);
    }

    public int getMoveCount() { return moveCount; }
}

栈模拟递归(显式栈非递归)

import java.util.*;

public class HanoiStackSimulation {
    /**
     * 用显式栈模拟递归调用
     * 每个栈帧记录:{n, from, to, aux, stage}
     * stage: 0=进入, 1=左递归返回后, 2=右递归返回后
     */
    public void solve(int n, char from, char to, char aux) {
        // 栈帧:int n, char from, to, aux, int stage
        Deque<int[]> stack = new ArrayDeque<>();
        // 用int编码: n(8位) | from(8位) | to(8位) | aux(8位) | stage(8位)
        stack.push(new int[]{n, from, to, aux, 0});

        while (!stack.isEmpty()) {
            int[] frame = stack.peek();
            int disk = frame[0];
            char f = (char) frame[1];
            char t = (char) frame[2];
            char a = (char) frame[3];
            int stage = frame[4];

            if (disk <= 0) {
                stack.pop();
                continue;
            }

            if (stage == 0) {
                // 模拟 solve(n-1, from, aux, to)
                frame[4] = 1; // 标记下次进入stage 1
                stack.push(new int[]{disk - 1, f, a, t, 0});
            } else if (stage == 1) {
                // 执行移动
                System.out.printf("Move disk %d from %c to %c%n", disk, f, t);
                frame[4] = 2; // 标记下次进入stage 2
                // 模拟 solve(n-1, aux, to, from)
                stack.push(new int[]{disk - 1, a, t, f, 0});
            } else {
                // stage == 2,当前帧完成
                stack.pop();
            }
        }
    }
}

二进制模式直接迭代

public class HanoiBinaryPattern {
    /**
     * 基于二进制模式的O(1)空间迭代算法
     * 第k步移动的盘号 = k末尾0的个数 + 1
     * 奇数盘顺时针(A->B->C->A),偶数盘逆时针(A->C->B->A)
     */
    public void solve(int n) {
        int totalMoves = (1 << n) - 1;
        // 柱编号:0=A, 1=B, 2=C
        int[] pegs = {0, 1, 2}; // 奇数盘使用这个顺序(顺时针)

        for (int k = 1; k <= totalMoves; k++) {
            int disk = Integer.numberOfTrailingZeros(k) + 1; // 移动的盘号

            // 计算该盘的移动方向
            // 对于n个盘的标准汉诺塔,奇数盘顺时针,偶数盘逆时针
            int from, to;
            if (disk % 2 == 1) {
                // 奇数盘:顺时针 A->B->C->A
                // 第k步中,盘disk的位置由 k / 2^disk 的奇偶性决定
                int cycle = (k >> disk) % 3;
                from = cycle;
                to = (cycle + 1) % 3;
            } else {
                // 偶数盘:逆时针 A->C->B->A
                int cycle = (k >> disk) % 3;
                from = cycle;
                to = (from + 2) % 3; // +2 等价于 -1 (mod 3)
            }

            System.out.printf("Move disk %d from %c to %c%n",
                disk, (char)('A'+from), (char)('A'+to));
        }
    }

    /**
     * 更简洁的二进制模式实现
     * 利用总步数为奇数的性质,每步只移动一个盘
     */
    public void solveOptimized(int n) {
        int totalMoves = (1 << n) - 1;
        // 记录每根柱子上最顶部盘的奇偶性(简化追踪)

        for (int step = 1; step <= totalMoves; step++) {
            int disk = Integer.numberOfTrailingZeros(step) + 1;

            // 盘disk的当前位置:通过分析step和disk的关系
            // 盘disk每 2^disk 步移动一次
            int moveIndex = step >> disk; // 盘disk的第几次移动

            char from, to;
            if (disk % 2 == 1) {
                // 奇数盘顺时针: A(0)->B(1)->C(2)->A(0)
                int pos = moveIndex % 3;
                from = (char)('A' + pos);
                to = (char)('A' + (pos + 1) % 3);
            } else {
                // 偶数盘逆时针: A(0)->C(2)->B(1)->A(0)
                int pos = moveIndex % 3;
                // 映射: 0->A, 1->C, 2->B
                char[] map = {'A', 'C', 'B'};
                from = map[pos];
                to = map[(pos + 1) % 3];
            }

            System.out.printf("Step %d: Move disk %d from %c to %c%n",
                step, disk, from, to);
        }
    }
}

状态追踪迭代法(带验证)

public class HanoiIterativeWithState {
    /**
     * 迭代法配合状态追踪,可验证每一步的合法性
     * 核心观察:每步只能移动最小的可移动盘,或唯一合法的移动
     */
    public void solveWithValidation(int n) {
        TowerState state = new TowerState(n);
        int totalMoves = (1 << n) - 1;

        // 记录每个盘当前所在的柱子
        int[] diskPos = new int[n + 1]; // 1-based, 初始都在A(0)
        Arrays.fill(diskPos, 0);

        // 方向:奇数盘顺时针(+1),偶数盘逆时针(-1, 即+2 mod 3)
        int[] direction = new int[n + 1];
        for (int i = 1; i <= n; i++) {
            direction[i] = (i % 2 == 1) ? 1 : 2; // 1=顺时针, 2=逆时针
        }

        System.out.println("Initial state:");
        state.printState();

        for (int step = 1; step <= totalMoves; step++) {
            int disk = Integer.numberOfTrailingZeros(step) + 1;
            int from = diskPos[disk];
            int to = (from + direction[disk]) % 3;

            state.move(from, to);
            diskPos[disk] = to;
        }

        System.out.println("\nFinal state:");
        state.printState();
        System.out.println("Complete: " + state.isComplete());
    }

    /**
     * 另一种简洁迭代:利用两个合法移动中必有一个移动最小盘的性质
     */
    public void solveFrameStewart(int n) {
        // 使用Stack数组显式维护三根柱子
        Stack<Integer>[] pegs = new Stack[3];
        for (int i = 0; i < 3; i++) pegs[i] = new Stack<>();
        for (int i = n; i >= 1; i--) pegs[0].push(i);

        int totalMoves = (1 << n) - 1;
        int moveCount = 0;

        // 对于奇数n,第一步将最小盘移到C;偶数n,移到B
        int source = 0, target = (n % 2 == 1) ? 2 : 1, auxiliary = 3 - source - target;

        while (moveCount < totalMoves) {
            // 移动最小盘
            int disk = pegs[source].pop();
            pegs[target].push(disk);
            moveCount++;
            System.out.printf("Step %d: Move disk %d from %c to %c%n",
                moveCount, disk, (char)('A'+source), (char)('A'+target));

            if (moveCount >= totalMoves) break;

            // 执行另一个合法移动(不涉及最小盘)
            int otherSrc = -1, otherDst = -1;
            // 找两根非空柱子之间唯一的合法移动
            for (int i = 0; i < 3; i++) {
                for (int j = 0; j < 3; j++) {
                    if (i == j) continue;
                    if (!pegs[i].isEmpty() && (pegs[j].isEmpty() || pegs[i].peek() < pegs[j].peek())) {
                        if (otherSrc == -1) {
                            otherSrc = i;
                            otherDst = j;
                        }
                    }
                }
            }

            if (otherSrc != -1) {
                disk = pegs[otherSrc].pop();
                pegs[otherDst].push(disk);
                moveCount++;
                System.out.printf("Step %d: Move disk %d from %c to %c%n",
                    moveCount, disk, (char)('A'+otherSrc), (char)('A'+otherDst));
            }

            // 更新最小盘的下一个目标
            int newTarget = 3 - source - target;
            source = target;
            target = newTarget;
        }
    }
}

主程序

public class HanoiMain {
    public static void main(String[] args) {
        int n = 4;

        System.out.println("=== 递归解法 ===");
        HanoiRecursive recursive = new HanoiRecursive();
        recursive.solve(n, 'A', 'C', 'B');
        System.out.println("总步数: " + recursive.getMoveCount());

        System.out.println("\n=== 栈模拟递归 ===");
        HanoiStackSimulation stackSim = new HanoiStackSimulation();
        stackSim.solve(n, 'A', 'C', 'B');

        System.out.println("\n=== 二进制模式迭代 ===");
        HanoiBinaryPattern binary = new HanoiBinaryPattern();
        binary.solveOptimized(n);

        System.out.println("\n=== 状态追踪迭代 ===");
        HanoiIterativeWithState iterative = new HanoiIterativeWithState();
        iterative.solveWithValidation(n);
    }
}

复杂度分析

  • 时间复杂度:所有解法均为 $O(2^n)$,必须输出 $2^n – 1$ 次移动。
  • 空间复杂度:
  • 递归解法:$O(n)$ 调用栈深度。
  • 栈模拟:$O(n)$ 显式栈空间。
  • 二进制模式:$O(1)$ 额外空间(不含输出),最优。
  • 状态追踪:$O(n)$ 盘位置数组。

总结

本文展示了汉诺塔问题的三种非递归实现:栈模拟递归、二进制模式直接计算、以及状态追踪迭代法。二进制模式利用数学规律在常数空间内生成移动序列,是最优雅的迭代解法。理解这些非递归实现有助于深入掌握递归与迭代的等价性,以及位运算在算法中的巧妙应用。