函数调用自己——把一个大问题拆成同结构的小问题,直到小到可以直接回答为止。
递归是函数直接或间接地调用自身来解决问题的技术。它把一个大问题分解成若干个结构完全相同、但规模更小的子问题,当子问题小到可以直接回答时(触底),再把答案一层层往上返回。
一个经典的生活类比:要知道自己是第几代人,只需要问父亲"你是第几代",然后加一就行了。父亲用同样方法问爷爷,爷爷再问曾爷爷……直到找到有族谱记载的那一位(边界)——答案再一路往回传。
用斐波那契数列来看,递归写法和数学定义几乎一模一样:
| 1 | long long fib(int n) { |
| 2 | if (n <= 2) return 1; // 边界:直接回答 |
| 3 | return fib(n-1) + fib(n-2); // 拆成子问题 |
| 4 | } |
| 5 | |
| 6 | // fib(5) → fib(4)+fib(3) → (fib(3)+fib(2))+(fib(2)+fib(1)) |
| 7 | // → ((fib(2)+fib(1))+1)+(1+1) → ((1+1)+1)+(1+1) → 5 |
每次函数调用都会在栈上压入一个"帧",记录当前的参数和返回地址。下图展示 fib(4) 从下探到回升的完整过程:
fib(4) 展开成一棵树,可以清楚地看到哪些子问题被重复计算了:
fib(2) 在这棵树里被计算了两次。当 n 更大时,重复计算会呈指数级增长。fib(40) 的调用次数超过 3 亿次——这就是朴素递归慢的原因,下一节用记忆化解决它。
解决方案很简单:算过的子问题结果存起来,下次直接查表,不重复算。这叫记忆化(Memoization),也叫"带缓存的递归"。
加上记忆化之后,每个子问题只会被真正计算一次,时间复杂度从 O(2ⁿ) 直降到 O(n)。
| 1 | long long memo[100]; // 记忆表,全局初始化为 0 |
| 2 | |
| 3 | long long fib(int n) { |
| 4 | if (n <= 2) return 1; // 边界 |
| 5 | if (memo[n]) return memo[n]; // ✅ 查表:算过就直接返回 |
| 6 | memo[n] = fib(n-1) + fib(n-2); // 算完存入表中 |
| 7 | return memo[n]; |
| 8 | } |
递归在竞赛中不只用于数列计算,以下是最常见的几类:
| 1 | int a[8], n; |
| 2 | bool used[8]; |
| 3 | |
| 4 | void perm(int depth) { // depth:当前填到第几位 |
| 5 | if (depth == n + 1) { // 边界:n 位都填完了 |
| 6 | for (int i=1;i<=n;i++) cout<<a[i]<<" "; |
| 7 | cout << endl; return; |
| 8 | } |
| 9 | for (int i = 1; i <= n; i++) { // 枚举这一位填什么 |
| 10 | if (used[i]) continue; // 已用过就跳过 |
| 11 | a[depth] = i; used[i] = true; |
| 12 | perm(depth + 1); // 递归填下一位 |
| 13 | used[i] = false; // 回溯:撤销选择 |
| 14 | } |
| 15 | } |
| 1 | // 计算 a^n mod p,O(log n) |
| 2 | long long qpow(long long a, long long n, long long p) { |
| 3 | if (n == 0) return 1; // 边界:a^0 = 1 |
| 4 | if (n % 2 == 0) // 偶数次幂:(a²)^(n/2) |
| 5 | return qpow(a*a%p, n/2, p); |
| 6 | return a * qpow(a, n-1, p) % p; // 奇数次幂:a × a^(n-1) |
| 7 | } |
| 1 | // 先序遍历(根 → 左子树 → 右子树) |
| 2 | void preorder(int u) { |
| 3 | if (u == 0) return; // 边界:空节点 |
| 4 | cout << u << " "; |
| 5 | preorder(left[u]); // 递归左子树 |
| 6 | preorder(right[u]); // 递归右子树 |
| 7 | } |
| 问题 | 原因 | 解决方法 |
|---|---|---|
| 栈溢出(Stack Overflow) | 递归层数太深,系统调用栈空间耗尽 | 改用递推(迭代),或增大栈空间 |
| 重复计算(TLE) | 同一子问题被反复求解 | 加记忆化数组(memo[])缓存结果 |
| 死递归(无限循环) | 边界条件写错,或规模没有收敛 | 检查边界是否覆盖所有终止情况 |
| 整数溢出 | 递归层数深时中间结果超过 int 范围 | 改用 long long,注意取模时机 |