递归
汉诺塔
问题设定
- 把套在 A 柱上的 6 个圆盘全部移到 B 柱
- 规则:一次只能移动柱子最上端的一个圆盘;小圆盘上不能放大圆盘
- 求最少移动次数
先缩小问题规模(提示:先从小汉诺塔着手)
- 一开始就考虑 6 层会头脑混乱,先找 3 层的解法
- 3 层汉诺塔经过多次尝试可找到移动 7 次的解法
发现规律
- 在来回移动圆盘的过程中会觉得「在重复做相似的事情」
- 之所以产生这种感觉,是因为我们有「发现规律的能力」—— 这种感觉很重要
- 移动 2 个圆盘从 A 到 C 是 3 次,移动 2 个圆盘从 C 到 B 也是 3 次
- 「移动 2 个圆盘」这个动作本身就是「2 层汉诺塔」的解法
递归结构
- 解 n 层:把 n-1 个移到中转柱 → 移动最大盘 1 次 → 把 n-1 个从中转柱移到目标柱
- 用 x(起点)、y(目标)、z(中转)代替 A/B/C,因为三者会不固定地对应
- 当 n = 0 时什么都不做
最少性证明
- 要把最大的圆盘从 A 移到 B,必须先把上面的 5 个圆盘都移到 C
- 因此这个递归解法就是移动次数最少的解法
解析式
观察 0、1、3、7、15、31、63 都是「2 的幂减 1」
只使用 n 表示 H(n) 的式子叫解析式,可以用数学归纳法证明
阶乘的递归定义
- ,其中
- 是定义,不是推导 —— 它的作用是让递归链条能启动
和的定义
- 递归与归纳是同一件事的两种视角
- 归纳:从 n=0 的基底出发,逐步推向 n
- 递归:从 n 出发,退回到 n-1
- 递归与归纳是同一件事的两种视角
斐波那契数列
- 不断繁殖的动物问题
- 第 n-1 天出生的动物在第 n 天还活着
- 第 n-2 天以前出生的动物在第 n 天会繁殖 1 个后代
- 所以第 n 天的总数 = 第 n-1 天 + 第 n-2 天
- 数列:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, …
- 13 世纪由 Leonardo Fibonacci(1170–1250)发现,故名
- 也常以 1 开头:1, 1, 2, 3, 5, …
- 递推公式中把一块砖头都没有的摆法(n=0)算作「1 种」情况,公式才成立
- 出现在鹦鹉螺内壁间隔、葵花种子排法、植物枝叶长法、爬 n 层阶梯(一次走 1 阶或 2 阶)里
- 创作旋律:用 4 分音符和 2 分音符打 n 拍,可打出 F(n+1) 种节奏
- 先打 4 分音符 → 剩余 n-1 拍的情况数
- 先打 2 分音符 → 剩余 n-2 拍的情况数
- 不断繁殖的动物问题
帕斯卡三角形(杨辉三角)
相邻两数之和得出下一行的数
用组合数表示会出现很多阶乘,但仅通过反复计算「相邻两数之和」就能得出 —— 着实让人大吃一惊
组合数的递归定义
左边出现 n、k,右边出现 n-1、k-1 —— 与汉诺塔、阶乘的递归模式非常相似
组合的数学理论解释
- 「n 中选 k 的组合数」= 「n-1 中选 k-1 的组合数」+「n-1 中选 k 的组合数」
- 用格子路线问题理解:从起点到终点的走法数
发现递归结构的要领
- 从 n 层的整体问题中隐去部分问题
- 判断剩余部分是否是 n-1 层的问题
- 隐去自己着眼的树枝,剩余部分就是缩小的树枝
- 本章所有问题(数学归纳法、汉诺塔、阶乘、组合数)都具有递归结构
- 关注特定部分,便可发现剩余部分和自身具有相同的结构
递归图形
- 以递归形式画树:树枝有左右两个分叉,每个分叉又连接着与树本身一样的结构
- 谢尔平斯基三角形(Sierpinski gasket)
- 编程中常见的递归结构:源代码缩进、树形数据结构、XML 语法、快速排序算法
用 Ruby 验证
# 汉诺塔:递归解法与移动次数 |
输出:
H(0)=0 |
H(3)=7 与书上「移动 7 次可解决问题」一致,6 层 63 次。
这一章其实在讲「分解」
作者在最后的课后对话里点破了:
学生:把握结构是关键吧?
老师:对!非常关键!
学生:为什么呢?
老师:因为把握结构是「分解」整个问题的突破口。
递归不是一种编程技巧,是一种分解方式。三个步骤在汉诺塔、阶乘、斐波那契、帕斯卡三角形里完全通用:
找出基底 —— n = 0 时做什么(汉诺塔不动、、砖头 0 种算 1 种)
确认剩余部分是同类问题 —— 规模缩小了,但结构没变
写出递归式 —— 把原问题拆成同类问题的组合
而 这个「反直觉」的定义,在这一章有了完整解释:它不是为了凑公式,是为了让递归链条能启动。这正是上一章笔记13 里那个「推倒不了第一张多米诺骨牌」的延续 —— 定义是为了让链条跑通,而不是因为它「自然」。
帕斯卡三角形那一段最值得反复看:组合数可以写成 这种满是阶乘的式子,但只靠「相邻两数相加」也能算出来。同一个对象的两种表述,一条要阶乘,一条只要加法 —— 而递归的那条更好用。