汉诺塔是练习递归思维的经典问题。本文尝试用更直白的方式解释递归与汉诺塔,加深自己的理解。
如何理解递归
递归的本质是:把问题不断缩小,直到落到「基本情况」。递归难理解,是因为人脑没法像计算机一样跟踪深层调用链;一复杂就容易迷失在步骤里、前功尽弃,久而久之丧失信心。最好的办法是:相信你写的递归是能工作的,不要硬在脑内把每一步都「跑」一遍。
递归的前提
当问题规模较大、直接解不方便时,可以考虑递归。但需满足:
- 能拆成若干更小的同类型子问题,且子问题之间相对独立;
- 具备清晰的「基本情况/终止条件」,否则会无限递归。
如何搭建递归
一个递归函数通常包含 4 个部分:函数定义、基本情况、递归调用、结果返回。设计递归算法,就是明确这 4 件事。
函数定义
递归函数会调用自身,参数要能支撑两件事:完成本层职责、记录递归进度。以斐波那契为例:
|
|
斐波那契的第 n 项由前两项之和决定,n 同时也是进度指示器,无需额外参数。
基本情况
把问题缩小,终会落到最小规模,即基本情况。斐波那契中,第 1、2 项已知:
|
|
递归调用
设 f(n) 为第 n 项,有递推式 f(n)=f(n-1)+f(n-2)。这把问题拆成两个更小的同类问题:
|
|
再次强调:别在脑中追踪整条递归链。站在当前这一层,只关心本层做了什么,并相信子调用会返回正确结果。
返回结果
本层的职责是算出第 n 项并把它返回,供上一层使用:
|
|
这也形成了尾递归可优化的形态:当递归调用是函数的最后一步时,很多语言可将其优化为迭代,降低栈开销。
汉诺塔问题
汉诺塔是递归的经典练习。随圆盘数量增加,步骤迅速增长。
问题分析
若只有 1 个盘,直接把它从起点挪到终点即可。2 个盘时:先把小盘移到中间,再把大盘移到终点,最后把小盘移到大盘上。3 个以上看起来就复杂了。
两条关键事实:
- 小盘可以放在大盘上,因此「一堆盘叠在最大盘上」与「只剩最大盘」的塔,对上层而言等价,可用于缩小问题规模;
- 初始时最大盘在最底部,意味着我们可以先忽略它,先处理其上的 n-1 个小盘。
据此,整体可以拆为三步:
- 把 n-1 个小盘从起点移到中转塔;
- 把第 n 个(最大)盘从起点移到终点;
- 把 n-1 个小盘从中转塔移到终点。
第 1、2 步的目的,是让最大盘到位,从而把规模从 n 缩到 n-1。虽然「一次性搬运 n-1 个盘」在现实中不可行,但在递归的抽象里,我们把它当作「子问题」来解即可。
代码草图
用三个数组表示三根柱子 src
/buf
/tar
,用参数 i
表示当前要处理的盘数:
|
|
参考
- 递归中的逆向思维
- 如何治疗晕递归?
- 汉诺塔问题