本系列可作为JAVA学习系列的笔记,文中提到的一些练习的代码,小编会将代码复制下来,大家复制下来就可以练习了,方便大家学习。
点赞关注不迷路!您的点赞、关注和收藏是对小编最大的支持和鼓励!
本文篇幅较长,建议先收藏再食用!
系列文章目录
JAVA学习 DAY2 java程序运行、注意事项、转义字符
JAVA学习 DAY5 变量&数据类型 [万字长文!一篇搞定!]
JAVA学习 DAY7 程序逻辑控制【万字长文!一篇搞定!】
目录
案例3:n=3(3个圆盘,记为小圆盘1、中圆盘2、大圆盘3)
前言
小编作为新晋码农一枚,会定期整理一些写的比较好的代码,作为自己的学习笔记,会试着做一下批注和补充,如转载或者参考他人文献会标明出处,非商用,如有侵权会删改!欢迎大家斧正和讨论!
哈喽,各位CSDN的小伙伴们~ 今天给大家带来经典算法问题——汉诺塔(Tower of Hanoi)的超详细解析。汉诺塔作为递归算法的“入门标杆”,不仅是面试高频考点,更是理解“分治思想”的核心案例。不管你是刚接触算法的零基础小白,还是想夯实递归基础的进阶开发者,这篇博客都能帮你彻底吃透汉诺塔,从原理到代码实现,每一步都讲得明明白白,建议收藏后慢慢研读~
在开始正文前,先和大家明确几个核心问题,带着问题学习效率更高:汉诺塔的规则是什么?为什么递归是解决汉诺塔的最优思路?递归的终止条件和递推关系如何确定?不同数量的盘子,移动步骤有什么规律?带着这些问题,我们一步步揭开汉诺塔的神秘面纱~
一、汉诺塔问题的起源与规则
1.1 问题起源
汉诺塔问题源于印度的一个古老传说:相传在古印度的贝拿勒斯圣庙中,有三根金刚石柱子,一根柱子上从下到上叠放着64个大小不同的圆盘(大的在下,小的在上)。僧侣们需要按照特定规则将所有圆盘从第一根柱子移动到第三根柱子,预言称当所有圆盘移动完成时,世界将会毁灭。当然,这只是传说,64个圆盘的移动步骤高达2⁶⁴-1步,即使每秒移动一步,也需要近千亿年才能完成,远远超过了宇宙的年龄。
虽然传说很玄乎,但汉诺塔问题本身却极具算法价值——它的核心解法完美契合递归思想,通过“大事化小、小事化了”的分治思路,将复杂问题拆解为简单子问题,是入门递归的最佳案例。

1.2 核心规则
汉诺塔问题的规则非常简单,明确以下3点即可:
-
有3根柱子,通常命名为A(起始柱)、B(辅助柱)、C(目标柱);
-
有n个大小不同的圆盘,初始时全部叠放在A柱上,且圆盘从下到上依次变小(大圆盘在下,小圆盘在上,初始状态是稳定的);
-
移动过程中需遵守两个核心约束:① 每次只能移动1个圆盘;② 任何时刻,任意一根柱子上的圆盘都必须满足“大圆盘在下,小圆盘在上”(不能出现小圆盘压在大圆盘上面的情况)。
问题目标:在遵守上述规则的前提下,将A柱上的n个圆盘全部移动到C柱上,B柱作为辅助柱配合移动。
1.3 直观案例(n=1、2、3时的移动步骤)
为了让大家直观理解,我们先从最简单的n=1、n=2、n=3入手,手动拆解移动步骤,找到其中的规律。
案例1:n=1(1个圆盘)
这是最基础的情况,只有1个圆盘,直接从A柱移动到C柱即可,仅需1步:
移动步骤:A → C(将A柱上的圆盘直接移到C柱)
移动次数:1步(规律:2¹-1=1)
案例2:n=2(2个圆盘,记为小圆盘1、大圆盘2)
有2个圆盘时,需要借助B柱作为辅助,步骤如下:
-
第一步:将A柱上的小圆盘1移动到B柱(A → B),此时A柱剩大圆盘2,B柱有小圆盘1,C柱空;
-
第二步:将A柱上的大圆盘2移动到C柱(A → C),此时A柱空,B柱有小圆盘1,C柱有大圆盘2;
-
第三步:将B柱上的小圆盘1移动到C柱(B → C),此时所有圆盘都移动到C柱,任务完成。
移动次数:3步(规律:2²-1=3)
核心思路:先将上方的小圆盘移到辅助柱,再将下方的大圆盘移到目标柱,最后将辅助柱上的小圆盘移到目标柱。
案例3:n=3(3个圆盘,记为小圆盘1、中圆盘2、大圆盘3)
n=3时,步骤稍多,但核心思路和n=2一致,只是多了一层拆解,步骤如下:
-
第一步:A → C(将小圆盘1从A移到C);
-
第二步:A → B(将中圆盘2从A移到B);
-
第三步:C → B(将小圆盘1从C移到B,此时B柱有中圆盘2、小圆盘1,满足规则);
-
第四步:A → C(将大圆盘3从A移到C,此时大圆盘到位,后续只需处理B柱上的两个圆盘);
-
第五步:B → A(将小圆盘1从B移到A);
-
第六步:B → C(将中圆盘2从B移到C,此时C柱有大圆盘3、中圆盘2);
-
第七步:A → C(将小圆盘1从A移到C,所有圆盘移动完成)。
移动次数:7步(规律:2³-1=7)
核心观察:n=3时,我们先将A柱上方的2个圆盘(子问题,n=2)借助C柱移动到B柱,再将A柱最下方的大圆盘3移动到C柱,最后将B柱上的2个圆盘(子问题,n=2)借助A柱移动到C柱。这正是递归的核心思想——将n个圆盘的问题,拆解为n-1个圆盘的子问题!
二、汉诺塔的递归思想拆解(核心重点)
通过上面的案例,我们已经隐约发现了汉诺塔的移动规律。接下来,我们正式拆解递归思想,这是掌握汉诺塔的关键,也是理解递归“终止条件”和“递推关系”的核心。
2.1 递归的核心逻辑:分治思想
递归解决问题的核心是“分治”——将一个复杂的大问题,拆解为与原问题结构相同但规模更小的子问题,直到子问题小到可以直接解决(终止条件),再通过子问题的解反推原问题的解。
对于n个圆盘的汉诺塔问题(起始柱A,辅助柱B,目标柱C),我们可以将其拆解为3个步骤,这也是汉诺塔递归的核心递推关系:
-
第一步:将A柱上的n-1个圆盘,借助目标柱C,移动到辅助柱B上(此时A柱只剩最下方的第n个大圆盘,B柱有n-1个圆盘,C柱空);
-
第二步:将A柱上剩下的第n个大圆盘(最大的圆盘),直接移动到目标柱C上(此时大圆盘已到位,后续无需再移动);
-
第三步:将B柱上的n-1个圆盘,借助起始柱A,移动到目标柱C上(此时所有圆盘都移动到C柱,任务完成)。
大家可以对照n=3的案例再梳理一遍:n=3时,第一步是将A柱的2个圆盘(n-1=2)借助C移到B;第二步将A的大圆盘3移到C;第三步将B的2个圆盘借助A移到C。这和我们手动拆解的步骤完全一致!
2.2 递归的终止条件
递归不能无限进行,必须有一个“终止条件”——当问题规模小到一定程度时,直接返回结果,不再继续递归。
对于汉诺塔问题,终止条件非常明确:当n=1时,直接将圆盘从起始柱移动到目标柱,无需递归拆解(因为1个圆盘的移动步骤是确定的,无需再分治)。
总结递归的两大核心要素(汉诺塔版):
-
终止条件:n == 1 → 直接移动(起始柱 → 目标柱);
-
递推关系:n > 1 → ① 移n-1个到辅助柱;② 移第n个到目标柱;③ 移n-1个到目标柱。
2.3 递归调用的逻辑链条(以n=3为例)
为了让大家更清晰地理解递归调用的过程,我们以n=3(A→C,B辅助)为例,拆解递归调用的逻辑链条(箭头表示调用关系):
hanoi(3, A, B, C) → ① hanoi(2, A, C, B) → ① hanoi(1, A, B, C) → 执行A→C(终止);
→ ② 执行A→B(hanoi(2, A, C, B)的第二步);
→ ③ hanoi(1, C, A, B) → 执行C→B(终止);
→ ② 执行A→C(hanoi(3, A, B, C)的第二步);
→ ③ hanoi(2, B, A, C) → ① hanoi(1, B, C, A) → 执行B→A(终止);
→ ② 执行B→C(hanoi(2, B, A, C)的第二步);
→ ③ hanoi(1, A, B, C) → 执行A→C(终止);
整个过程下来,正好是7步,和手动拆解的步骤完全一致。这里需要注意的是:递归调用时,起始柱、辅助柱、目标柱的角色是动态变化的,比如hanoi(2, A, C, B)中,目标柱变成了B,辅助柱变成了C,这是递归的关键,也是很多人容易混淆的点。
三、汉诺塔的代码实现(Java语言)
理解了递归思想后,代码实现就非常简单了。我们以Java语言为例,编写汉诺塔的递归代码,同时加入步骤打印和移动次数统计,让结果更直观。
3.1 基础版代码(打印移动步骤)
基础版代码的核心是实现递归逻辑,打印每一步的移动路径,代码如下(附带详细注释):
/**
* 汉诺塔递归实现(基础版)
* @param n 圆盘数量
* @param start 起始柱(如'A')
* @param assist 辅助柱(如'B')
* @param target 目标柱(如'C')
*/
public class HanoiTowerBasic {
public static void main(String[] args) {
int n = 3; // 圆盘数量,可修改为任意正整数
System.out.println("汉诺塔移动步骤(n=" + n + "):");
hanoi(n, 'A', 'B', 'C');
}
// 递归方法:实现n个圆盘从start柱借助assist柱移动到target柱
public static void hanoi(int n, char start, char assist, char target) {
// 终止条件:n=1时,直接移动
if (n == 1) {
System.out.println("移动圆盘" + n + ":" + start + " → " + target);
return;
}
// 第一步:将n-1个圆盘从start借助target移动到assist
hanoi(n - 1, start, target, assist);
// 第二步:将第n个圆盘从start移动到target
System.out.println("移动圆盘" + n + ":" + start + " → " + target);
// 第三步:将n-1个圆盘从assist借助start移动到target
hanoi(n - 1, assist, start, target);
}
}
代码运行结果(n=3)
运行上述代码,当n=3时,输出结果如下,和我们手动拆解的步骤完全一致:
汉诺塔移动步骤(n=3):
移动圆盘1:A → C
移动圆盘2:A → B
移动圆盘1:C → B
移动圆盘3:A → C
移动圆盘1:B → A
移动圆盘2:B → C
移动圆盘1:A → C
3.2 进阶版代码(统计移动次数+优化输出)
基础版代码只能打印步骤,我们可以优化代码,加入移动次数统计(通过静态变量实现),同时优化输出格式,让结果更清晰。代码如下:
/**
* 汉诺塔递归实现(进阶版):统计移动次数+优化输出
*/
public class HanoiTowerAdvanced {
// 静态变量:统计移动次数(递归调用中共享)
private static int moveCount = 0;
public static void main(String[] args) {
int n = 4; // 可修改为任意正整数(n=64时次数会非常大,不建议测试)
System.out.println("====================汉诺塔问题====================");
System.out.println("圆盘数量:" + n);
System.out.println("起始柱:A,辅助柱:B,目标柱:C");
System.out.println("==================================================");
hanoi(n, 'A', 'B', 'C');
System.out.println("==================================================");
System.out.println("汉诺塔移动完成!总移动次数:" + moveCount);
System.out.println("理论移动次数(2^n - 1):" + (Math.pow(2, n) - 1));
}
public static void hanoi(int n, char start, char assist, char target) {
if (n == 1) {
moveCount++; // 移动次数+1
System.out.printf("第%-3d步:移动圆盘%d → %c → %c%n", moveCount, n, start, target);
return;
}
// 第一步:n-1个圆盘从start→assist(辅助柱为target)
hanoi(n - 1, start, target, assist);
// 第二步:第n个圆盘从start→target
moveCount++;
System.out.printf("第%-3d步:移动圆盘%d → %c → %c%n", moveCount, n, start, target);
// 第三步:n-1个圆盘从assist→target(辅助柱为start)
hanoi(n - 1, assist, start, target);
}
}
代码运行结果(n=4)
运行进阶版代码,n=4时输出结果如下,同时验证了移动次数(2⁴-1=15步):
====================汉诺塔问题====================
圆盘数量:4
起始柱:A,辅助柱:B,目标柱:C
==================================================
第1 步:移动圆盘1 → A → B
第2 步:移动圆盘2 → A → C
第3 步:移动圆盘1 → B → C
第4 步:移动圆盘3 → A → B
第5 步:移动圆盘1 → C → A
第6 步:移动圆盘2 → C → B
第7 步:移动圆盘1 → A → B
第8 步:移动圆盘4 → A → C
第9 步:移动圆盘1 → B → C
第10 步:移动圆盘2 → B → A
第11 步:移动圆盘1 → C → A
第12 步:移动圆盘3 → B → C
第13 步:移动圆盘1 → A → B
第14 步:移动圆盘2 → A → C
第15 步:移动圆盘1 → B → C
==================================================
汉诺塔移动完成!总移动次数:15
理论移动次数(2^n - 1):15.0
四、常见问题与易错点提醒(避坑指南)
在学习汉诺塔和编写递归代码时,很多小伙伴会遇到一些问题,这里整理了4个高频易错点,帮大家避坑:
4.1 易错点1:递归调用时,柱子的角色混淆
这是最常见的错误!很多人在写递归调用时,直接照搬参数顺序(start, assist, target),忽略了柱子角色的动态变化。比如第一步“将n-1个圆盘从start借助target移动到assist”,此时辅助柱是target,目标柱是assist,所以递归调用应该是hanoi(n-1, start, target, assist),而不是hanoi(n-1, start, assist, target)。
解决办法:记住递归的3个步骤,明确每一步的“起始柱、辅助柱、目标柱”,调用时严格对应参数顺序(参数顺序:n, 起始柱, 辅助柱, 目标柱)。
4.2 易错点2:终止条件缺失或错误
如果递归没有终止条件(或终止条件错误,比如n==0),会导致无限递归,最终抛出StackOverflowError(栈溢出异常)。因为每次递归调用都会在栈中开辟空间,无限递归会耗尽栈内存。
解决办法:明确终止条件是n==1,此时直接移动圆盘,无需再递归。
4.3 易错点3:移动次数统计错误
很多人会在递归方法外部定义局部变量统计次数,导致每次递归调用时变量被重置,统计结果错误。比如在main方法中定义int count=0,然后在hanoi方法中count++,这样count的值永远是1。
解决办法:使用静态变量(如进阶版代码中的moveCount)或成员变量统计次数,确保递归调用中共享同一个变量;也可以通过数组(引用类型)传递计数器(因为基本类型传递的是值拷贝,引用类型传递的是地址)。
4.4 易错点4:圆盘数量n为非正整数
如果输入的n是0或负数,递归会进入无效状态(n==1的终止条件永远不会触发),导致无限递归或逻辑错误。
解决办法:在调用递归方法前,加入参数校验,确保n是正整数。比如在main方法中添加:
if (n <= 0) { System.out.println("圆盘数量必须为正整数!"); return; }
五、汉诺塔的拓展与思考
5.1 移动次数的规律
通过前面的案例和代码,我们可以总结出汉诺塔移动次数的规律:n个圆盘的汉诺塔,最少移动次数为2ⁿ - 1次(这是最优解,无法再减少步骤,因为每一步只能移动1个圆盘,且必须遵守规则)。
比如:n=1→1次(2¹-1),n=2→3次(2²-1),n=3→7次(2³-1),n=4→15次(2⁴-1),以此类推。当n=64时,移动次数为2⁶⁴-1≈1.84×10¹⁹次,这也是传说中“世界毁灭”的依据。
5.2 非递归实现汉诺塔
除了递归实现,汉诺塔也可以通过非递归方式实现(比如借助栈模拟递归过程),但非递归实现的逻辑更复杂,不如递归直观。核心思路是:用栈存储每一步的递归状态(n, start, assist, target),然后通过循环模拟递归调用的过程,直到栈为空(所有步骤执行完成)。
非递归实现适合想深入理解递归本质的小伙伴,这里给出核心思路(具体代码可留言获取):
-
创建一个栈,将初始状态(n, A, B, C)压入栈;
-
循环:弹出栈顶元素,若n==1,执行移动步骤;否则,按递归的逆顺序将状态压入栈(因为栈是先进后出,所以需要逆序压入第三步、第二步、第一步);
-
重复循环,直到栈为空。
5.3 汉诺塔的实际应用场景
汉诺塔虽然是一个理论算法问题,但它的核心思想(递归、分治)在实际开发中应用广泛,比如:
-
文件系统的目录遍历(递归遍历子目录);
-
树形结构的遍历(二叉树的前序、中序、后序遍历,递归实现更简洁);
-
复杂问题的拆解(比如大型项目的模块拆分、大数据的排序算法等)。
六、总结与学习建议
到这里,汉诺塔问题的详细解析就全部完成了。总结一下核心内容:汉诺塔的核心是递归思想,通过将n个圆盘的问题拆解为n-1个圆盘的子问题,直到n=1(终止条件),再逐步完成所有移动步骤;代码实现非常简洁,关键是明确递归的终止条件和递推关系,避免混淆柱子的角色。
对于零基础学习者,建议按以下步骤学习:
-
先手动拆解n=1、2、3的移动步骤,找到规律;
-
理解递归的核心逻辑(分治+终止条件),明确3个递推步骤;
-
对照基础版代码,逐行理解递归调用过程,重点关注柱子角色的变化;
-
尝试修改代码(比如修改圆盘数量、统计移动次数),验证自己的理解;
-
进阶学习:尝试编写非递归实现,深入理解递归与栈的关系。
汉诺塔作为递归的入门案例,看似简单,但能帮我们快速掌握递归的核心思想,为后续学习更复杂的算法(如动态规划、回溯算法)打下基础。如果在学习过程中遇到问题,欢迎在评论区留言交流,我会及时回复大家的疑问~
最后,感谢大家的耐心阅读!如果觉得这篇博客对你有帮助,别忘了点赞、收藏、转发三连~ 后续我会持续更新更多经典算法的详细解析,关注我,一起深耕算法,提升编程能力!
总结
以上就是今天要讲的内容,本文简单记录了JAVA学习笔记,大家根据注释理解,您的点赞关注收藏就是对小编最大的鼓励!
1万+

被折叠的 条评论
为什么被折叠?



