汉诺塔
算法定义
汉诺塔是一个经典的递归问题,源于印度古老传说。问题描述为:有 3 根柱子(A、B、C)和 n 个大小不同的圆盘,所有圆盘初始套在 A 柱上,且大盘按从小到大顺序叠放(大圆盘在下,小圆盘在上)。要求将所有圆盘从 A 柱移动到 C 柱,过程中需遵守:
- 每次只能移动一个圆盘
- 任何时候都不能将大圆盘放在小圆盘上
- B 柱作为辅助柱使用
汉诺塔算法是解决该问题的步骤性策略,核心通过递归思想将复杂问题分解为简单子问题。
算法思路
汉诺塔算法的递归思路可归纳为:
- 基线条件:当只有 1 个圆盘时,直接将其从源柱移动到目标柱。
- 递归分解:
- 将 n-1 个圆盘从源柱(A)通过目标柱(C)移动到辅助柱(B)
- 将第 n 个圆盘(最大的圆盘)从源柱(A)直接移动到目标柱(C)
- 将 n-1 个圆盘从辅助柱(B)通过源柱(A)移动到目标柱(C)
例如,n=3 时的移动步骤:
- 步骤 1:将 2 个圆盘从 A→B(以 C 为辅助)
- 步骤 2:将第 3 个圆盘从 A→C
- 步骤 3:将 2 个圆盘从 B→C(以 A 为辅助)
算法复杂度
- 时间复杂度:O (2ⁿ),因为每增加 1 个圆盘,移动次数约翻倍(总次数为 2ⁿ - 1)
- 最坏时间复杂度:O (2ⁿ),问题规模固定时,移动步骤唯一
- 平均时间复杂度:O (2ⁿ),不存在输入数据分布差异
- 空间复杂度:O (n),递归调用栈深度为 n(等于圆盘数量)
优点与缺点
- 优点:
- 逻辑清晰,递归实现代码简洁优雅
- 问题分解方式具有启发性,适合理解递归思想
- 解法具有唯一性,能找到最优移动路径(最少步数)
- 缺点:
- 时间复杂度高(指数级),当 n 较大时(如 n>20)实际不可用
- 递归深度受限于栈内存,n 过大会导致栈溢出
- 非递归实现复杂,不易理解
应用场景
- 递归算法教学(展示递归思想的经典案例)
- 程序设计中的递归逻辑验证
- 心理学问题解决研究(问题分解能力测试)
- 工业领域的堆叠问题(如磁盘调度、货物装卸的步骤规划)
- 数学中的组合问题(如置换群、分形结构)
Java 实现及优化版本
public class HanoiTower {
// 移动次数计数器
private static int moveCount = 0;
// 1. 基本递归版(打印移动步骤)
public static void hanoiBasic(int n, char source, char auxiliary, char target) {
// 基线条件:只有一个圆盘时直接移动
if (n == 1) {
moveCount++;
System.out.printf("步骤%d: 从%c移动到%c\n", moveCount, source, target);
return;
}
// 递归步骤1:将n-1个圆盘从源柱移到辅助柱
hanoiBasic(n - 1, source, target, auxiliary);
// 步骤2:将第n个圆盘从源柱移到目标柱
moveCount++;
System.out.printf("步骤%d: 从%c移动到%c\n", moveCount, source, target);
// 递归步骤3:将n-1个圆盘从辅助柱移到目标柱
hanoiBasic(n - 1, auxiliary, source, target);
}
// 2. 优化版1:计数不打印(高效计算移动次数)
public static long hanoiCountOnly(int n) {
// 数学公式:n个圆盘需要2^n - 1次移动
return (1L << n) - 1; // 等价于2^n - 1(仅适合n<=63)
}
// 3. 优化版2:非递归实现(用栈模拟递归,避免栈溢出)
public static void hanoiNonRecursive(int n, char source, char auxiliary, char target) {
java.util.Stack<int[]> stack = new java.util.Stack<>();
// 入栈参数:n, source, auxiliary, target, 是否处理过(0=未处理,1=已处理)
stack.push(new int[]{n, source, auxiliary, target, 0});
moveCount = 0;
while (!stack.isEmpty()) {
int[] current = stack.pop();
int num = current[0];
char s = (char) current[1];
char a = (char) current[2];
char t = (char) current[3];
int processed = current[4];
if (num == 1) {
// 单个圆盘直接移动
moveCount++;
System.out.printf("步骤%d: 从%c移动到%c\n", moveCount, s, t);
} else if (processed == 0) {
// 第一次处理,按逆序入栈(栈是LIFO)
stack.push(new int[]{num, s, a, t, 1}); // 标记为已处理
stack.push(new int[]{num - 1, a, s, t, 0}); // 步骤3
stack.push(new int[]{num, s, a, t, 2}); // 步骤2(实际移动)
stack.push(new int[]{num - 1, s, t, a, 0}); // 步骤1
} else if (processed == 2) {
// 执行实际移动操作
moveCount++;
System.out.printf("步骤%d: 从%c移动到%c\n", moveCount, s, t);
}
}
}
// 4. 优化版3:大数量圆盘的移动次数计算(支持n>63)
public static String hanoiBigCount(int n) {
// 使用字符串计算2^n - 1,避免长整数溢出
StringBuilder result = new StringBuilder("1");
// 计算2^n
for (int i = 0; i < n; i++) {
result = multiplyByTwo(result);
}
// 减1
return subtractOne(result).toString();
}
// 辅助方法:字符串表示的数字乘以2
private static StringBuilder multiplyByTwo(StringBuilder num) {
int carry = 0;
for (int i = num.length() - 1; i >= 0; i--) {
int digit = num.charAt(i) - '0';
int product = digit * 2 + carry;
num.setCharAt(i, (char) (product % 10 + '0'));
carry = product / 10;
}
if (carry > 0) {
num.insert(0, carry);
}
return num;
}
// 辅助方法:字符串表示的数字减1
private static StringBuilder subtractOne(StringBuilder num) {
int i = num.length() - 1;
while (i >= 0 && num.charAt(i) == '0') {
num.setCharAt(i, '9');
i--;
}
if (i < 0) {
return new StringBuilder("0");
}
num.setCharAt(i, (char) (num.charAt(i) - 1));
// 移除前导零
if (num.charAt(0) == '0' && num.length() > 1) {
return new StringBuilder(num.substring(1));
}
return num;
}
public static void main(String[] args) {
int n = 3;
System.out.println("=== 基本递归版(n=" + n + ")===");
moveCount = 0;
hanoiBasic(n, 'A', 'B', 'C');
System.out.println("\n=== 非递归版(n=" + n + ")===");
hanoiNonRecursive(n, 'A', 'B', 'C');
System.out.println("\n=== 移动次数计算 ===");
System.out.println("n=10的移动次数: " + hanoiCountOnly(10));
System.out.println("n=100的移动次数: " + hanoiBigCount(100));
}
}
各版本优化说明
- 基本递归版: 最经典的实现方式,直接体现递归分解思想,代码简洁且能打印具体移动步骤,但 n 较大时(如 n>20)会因递归深度过大导致栈溢出,且打印步骤会消耗大量时间。
- 计数不打印版: 利用数学公式(移动次数 = 2ⁿ-1)直接计算结果,时间复杂度 O (1),适合仅需知道总步数而无需具体移动路径的场景,缺点是 n>63 时会超出 long 类型范围。
- 非递归实现: 使用栈模拟递归调用过程,避免了递归栈溢出问题,可处理更大的 n 值(受栈内存限制),但代码复杂度增加,可读性降低。
- 大数量计数版: 通过字符串操作实现超大数(n>63)的移动次数计算,解决了数值溢出问题,适合理论研究中对超大 n 值的步数估算。
汉诺塔算法是递归思想的完美诠释,其核心价值在于展示了 "将复杂问题分解为相似子问题" 的解题思路。实际应用中,基本递归版适合教学和小规模问题,非递归版适合需要避免栈溢出的场景,而公式计算版则适合快速获取移动次数。由于时间复杂度为指数级,汉诺塔算法更多作为理论研究和教学案例,而非实际工程中的高效算法。
资料
https://www.cnblogs.com/jingmoxukong/p/4359185.html
https://www.zhihu.com/question/24385418