递归算法实战:汉诺塔与合并有序链表
递归往往是初学者感到最棘手的概念之一。很多人看到函数调用自己时会产生畏惧感,觉得无法追踪执行流程。其实,理解递归的关键在于建立'宏观视角'——不要纠结于函数内部每一层的细节展开,而是信任当前函数的功能定义。只要明确函数要完成什么任务,并设计好递归终止条件,剩下的交给递归本身即可。
递归的核心思维:宏观视角
看待递归最重要的点,是清楚这个函数的功能到底是什么。递归的本质是自己调用自己,就像我们调用普通函数一样,目的是利用其功能。
以归并排序为例,mergesort 的功能是将无序数组变为有序。实现时,我们将数组分为左右两部分,分别调用 mergesort 让两边有序,最后合并。这里不需要关心左边具体怎么排好的,只需要相信调用 mergesort(left) 后,左边就已经有序了。同理,右边也是如此。
这种思维方式能避免陷入无限递归的细节泥潭。写递归时,只需关注两点:
- 递归结束条件:保证有出口,通常是问题规模缩小到最小单位。
- 功能假设:假设子问题已被解决,专注于当前层如何组合结果。
下面通过两个经典题目来实践这一思路。
1. 汉诺塔
题目描述
有三根柱子 A、B、C。A 柱上有 n 个大小不同的圆盘,从小到大叠放。要求将所有盘子从 A 移动到 C,过程中大盘不能压在小盘上,且每次只能移动一个盘子。
解题思路
这是一个典型的分治问题。
- 基础情况:如果只有 1 个盘子,直接从 A 移到 C。
- 递归步骤:对于 n 个盘子:
- 将上面的 n-1 个盘子从 A 借助 C 移到 B。
- 将第 n 个(最大的)盘子从 A 直接移到 C。
- 将 B 上的 n-1 个盘子借助 A 移到 C。
关键在于步骤 1 和 3,它们本质上是同一个问题的缩小版。
C++ 代码实现
class Solution {
public:
void hanota(vector<int>& A, vector<int>& B, vector<int>& C) {
dfs(A, B, C, A.size());
}
private:
void dfs(vector<int>& x, vector<int>& y, vector<int>& z, int n) {
// 递归结束条件:只剩一个盘子,直接移动
if (n == 1) {
z.push_back(x.());
x.();
;
}
(x, z, y, n - );
z.(x.());
x.();
(y, x, z, n - );
}
};


