怎么用Python写汉诺塔?递归终止条件与参数传递解析
汉诺塔(Tower of Hanoi)是学习编程递归思想时最经典的问题之一。用Python解决汉诺塔问题,核心只需5行左右的代码。然而,许多初学者即便把代码默写下来,仍然觉得思维绕不过弯,主要卡在两个地方:一是不知道为什么终止条件写成 n == 1,二是在连续递归调用时,传进去的柱子参数名字变来变去,把“角色”与“实体”弄混了。本文通过完整可运行的代码、直观的图解和清晰的分层推演,带你彻底搞懂递归终止条件的设计与函数参数的流转逻辑。
一、汉诺塔的游戏规则与三步解题法
汉诺塔的规则非常直接:有三根柱子(通常记作 A、B、C),在源柱子 A 上按从小到大的顺序叠放着 $n$ 个盘子(大盘在下,小盘在上)。目标是将所有盘子全部移动到目标柱子 C 上,中间允许借助辅助柱子 B。移动过程中有两个刚性限制:每次只能移动一个盘子,且任何时候都不能出现“大盘子压在小盘子上面”的情况。
如果尝试一步一步去枚举每个盘子的具体落点,当盘子数量增加时,步骤会呈指数级爆炸($2^n - 1$ 步),大脑很难跟上这个推演过程。递归的威力正在于将整体拆解为宏观任务。要将 $n$ 个盘子从源柱子搬到目标柱子,本质上只需要做三件事:
- 第一步(借道暂存):将上面 $n-1$ 个较小的盘子打包看作一个整体,从源柱子搬到辅助柱子上。此时目标柱子充当临时的中转空位。
- 第二步(移底定基):将最底下那个最大的第 $n$ 号盘子,直接从源柱子一步搬到目标柱子上。此时目标柱子底座已稳。
- 第三步(复位归总):把刚才暂存到辅助柱子上的 $n-1$ 个盘子,从辅助柱子搬到目标柱子上。此时原来的源柱子变成了临时中转空位。
二、完整Python代码实现与运行演示
掌握了上面的三步结构,代码写起来非常平铺直叙。我们在Python环境中直接定义函数 hanoi,接受4个参数:盘子总数 n、源柱子名称 source、目标柱子名称 target 以及辅助柱子名称 helper。
def hanoi(n, source, target, helper):
"""
使用递归打印汉诺塔移动步骤
:param n: 当前要搬动的盘子数量
:param source: 盘子当前所在的柱子(起点)
:param target: 盘子需要到达的柱子(终点)
:param helper: 充当中介过渡的柱子(辅助)
"""
# 1. 递归终止条件
if n == 1:
print(f"移动盘子 1: {source} -> {target}")
return
# 2. 第一步:将上面 n-1 个盘子从 source 借 target 搬到 helper
hanoi(n - 1, source, helper, target)
# 3. 第二步:将第 n 个盘子直接从 source 搬到 target
print(f"移动盘子 {n}: {source} -> {target}")
# 4. 第三步:将 helper 上的 n-1 个盘子借 source 搬到 target
hanoi(n - 1, helper, target, source)
# 运行测试:以 3 个盘子为例
if __name__ == "__main__":
hanoi(3, 'A', 'C', 'B')
在任意安装了Python 3的主流环境中保存并执行上述脚本,终端中输出的具体步骤如下:
# 示例终端输出结果
移动盘子 1: A -> C
移动盘子 2: A -> B
移动盘子 1: C -> B
移动盘子 3: A -> C
移动盘子 1: B -> A
移动盘子 2: B -> C
移动盘子 1: A -> C
对于3个盘子,移动的总步数正好是 $2^3 - 1 = 7$ 步。程序完全满足无大盘压小盘的规则,并平稳结束。
三、核心解析一:递归终止条件的设计考量
任何递归程序必须回答两个核心问题:“何时停下来”以及“每次递归在往哪个方向靠拢”。在汉诺塔中,终止条件就是 if n == 1。
初学者容易产生疑问:为什么不把终止条件设为 n == 0?
在实际编写中,将边界写成 n == 0: return 在算法逻辑上同样能得到正确结果,但把终止条件定在 n == 1 是最清晰、最符合物理行为的选择。因为移动“1个盘子”是物理现实中能进行操作的最小不可分割单元(原子操作):当只有一个盘子时,辅助柱子根本用不上,直接将其从源柱子放到目标柱子上即可,随后直接 return 返回上一层调用。
防错提醒:如果遗漏了 return,或者没有正确减小规模(如错误写成 hanoi(n, ...)),函数就会陷入无穷递归,Python 会抛出经典的报错:RecursionError: maximum recursion depth exceeded。
四、核心解析二:形参、实参与角色交换的秘密
汉诺塔代码最让人眼花缭乱的地方,莫过于这两行递归调用:
hanoi(n - 1, source, helper, target)
hanoi(n - 1, helper, target, source)
为什么同一个柱子(比如 'B'),有时放在第2个参数,有时放在第3个,有时又放在第4个?
1. 区分“形参的岗位”与“实参的身份”
理解这个现象的关键,在于严格区分形参(岗位职责)与实参(实际传递的具体实体):
- 函数签名中的形参是固定的岗位:
def hanoi(n, source, target, helper)。无论什么时候,第2个形参名是source,代表“从哪里出发”;第3个形参名是target,代表“要到哪里去”;第4个形参名是helper,代表“谁来做辅助”。 - 而传入的实参是现实中的三根柱子(实体名字:
'A'、'B'、'C')。实参的物理身份永远不变,但它们在不同子任务中扮演的角色在频繁轮换。
2. 两处递归中实参位置的交换逻辑
在处理第一步时,我们的任务是把 $n-1$ 个盘子从 source 搬到 helper 上暂存。因此对于这个子任务而言:
- 起点依然是原先的
source; - 但终点变成了
helper; - 原来的终点
target则被挪用作为中转辅助。 - 所以调用写法严格对应:
hanoi(n - 1, source, helper, target)。传递给第三个参数(目的地岗位)的是实参变量helper!
在处理第三步时,最大盘已经就位,我们要把暂存在 helper 上的 $n-1$ 个盘子收拢到最终的 target 上。因此对于这个子任务而言:
- 起点变成了
helper; - 终点回到了
target; - 原来的起点
source闲置出来,充当中转辅助。 - 所以调用写法严格对应:
hanoi(n - 1, helper, target, source)。
五、执行轨迹剖析:以 3 个盘子为例的调用树
当我们运行 hanoi(3, 'A', 'C', 'B') 时,计算机底层在内存的“调用栈(Call Stack)”中经历了严谨的压栈与弹栈过程。计算机不会一下子跳到最终结果,而是按二叉树的先序遍历方式逐步推进:
- 首先处理左分支:要移 3 个盘子,必须先解决“将 2 个盘子从 A 移到 B”的问题。于是暂停当前层,压栈调用
hanoi(2, 'A', 'B', 'C')。 - 在
hanoi(2, ...)中,又遇到要把 1 个盘子从 A 移到 C。继续压栈调用hanoi(1, 'A', 'C', 'B')。 - 此时命中终止条件
n == 1,打印移动盘子 1: A -> C,该层结束并弹栈出栈。 - 回到第 2 层,打印当前层的中间动作:
移动盘子 2: A -> B。 - 接着处理右侧分支,把暂存在 C 的盘子收回到 B……依此类推。
六、总结与关键记忆点
只要理清了思路,汉诺塔的递归写法其实非常工整纯粹。在后续复习或自己独立编写时,建议牢记以下两点:
- 终止条件看基元:当盘子数缩减到
n == 1时,达到了物理上单次搬运的最简场景,输出单步移动指令并使用return终止继续向下下钻。 - 参数传递看职责:函数的形参列表定义的是静态的“功能卡位”(出发地、目的地、辅助点);在编写子调用时,不要死记柱子字符,而是想清楚“这批盘子当前从哪里起步、要去向哪里、借谁过渡”,再把对应的柱子变量对号入座填入对应的卡位中即可。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









