递归
💡 核心思想
递归是函数直接或间接调用自身的编程技巧。它的核心思想是:将大问题分解为结构相同的更小子问题,直到子问题足够小可以直接求解。每个递归都需要两个要素:递归边界(终止条件)和递推关系(如何缩小问题规模)。
递归是整个算法体系的地基。DFS、分治、动态规划、回溯——全部建立在递归思维之上。初学者最常犯的错误是忘记写终止条件(无限递归导致栈溢出),或者递推关系写错导致结果不对。我的建议是:先想清楚"最小问题怎么解",再想"如何从更小的问题推出当前问题"。
🎯 直觉理解
想象俄罗斯套娃:要打开最外面的大套娃,你先得打开它,里面有个小一点的,再打开,一直开到最小的那个(终止条件)。然后你从最小的开始,把每层的结果组合起来。递归就是这样:一路拆到底,再一路组合回来。
📝 算法流程
- 明确函数的语义:f(n) 代表什么?
- 确定递归边界(base case):最小的 n 时直接返回
- 写出递推关系:f(n) 如何由 f(n-1) 或其他更小的子问题得到
- 信任递归:假设子问题已经正确解决,只关注当前层
$$f(n) = \begin{cases} 1 & n = 0 \\ n \times f(n-1) & n > 0 \end{cases} \text{(阶乘示例)}$$
📊 复杂度分析
| 指标 | 复杂度 |
|---|---|
| 时间 | 取决于递推关系,用主定理分析 |
| 空间 | $O(\text{递归深度})$(栈空间) |
💻 参考实现(C++)
C++ (C++17)
#include <bits/stdc++.h>
using namespace std;
// 递归三要素示例
// 1. 阶乘:f(n) = n * f(n-1), f(0) = 1
long long factorial(int n) {
if (n <= 1) return 1; // 终止条件
return n * factorial(n - 1); // 递推关系
}
// 2. 斐波那契数列(朴素递归,仅作演示)
long long fib(int n) {
if (n <= 1) return n;
return fib(n-1) + fib(n-2);
}
// 3. 汉诺塔
void hanoi(int n, char from, char mid, char to) {
if (n == 1) { cout << from << " -> " << to << "\n"; return; }
hanoi(n-1, from, to, mid); // 上面 n-1 个从 from 借 to 到 mid
cout << from << " -> " << to << "\n"; // 最大的那个直接移
hanoi(n-1, mid, from, to); // n-1 个从 mid 借 from 到 to
}
int main() {
cout << "5! = " << factorial(5) << endl;
hanoi(3, 'A', 'B', 'C');
return 0;
}⚠️ 常见坑点
忘记终止条件导致栈溢出(Stack Overflow)
递推方向错误(应该是 n-1 但写成了 n+1)
重复计算子问题(如朴素斐波那契是 $O(2^n)$)——应用记忆化
递归深度过大(>10^5)时应考虑改写为迭代
📚 相关题目
| 题目 | 来源 | 难度 | 备注 |
|---|---|---|---|
| P1044 栈 | 洛谷 | CSP-J | 卡特兰数递推 |
| P1028 数的计算 | 洛谷 | CSP-J | 简单递归+记忆化 |