📄 一句话概括
递归 = 函数调用它自己:把大问题拆成"同类型的小问题",一层层递下去直到能直接回答的最小情形(出口),再一层层把答案归回来。两个必备要素:出口(什么时候停)+ 向出口靠近(每次拆得更小)——缺任何一个都会栈溢出。
📝 本节和前面几节的关系
6.3.2 的嵌套调用是"A 调 B",递归是"A 调 A"——最特殊的一种嵌套调用。它复用 6.3 那套调用栈,所以必须警惕"层数太深、栈装不下"。本节还会把第五章的循环拉过来对照:同一个问题,循环和递归都能解,选谁看问题形状。
一、递归的两要素(缺一不可)
int fact(int n) {
if (n <= 1) return 1; // ① 出口:最小的情形直接给答案
return n * fact(n - 1); // ② 向出口靠近:每次的 n 都小 1
}
| 要素 | 作用 | 缺了会怎样 |
|---|---|---|
| 出口(基准情形) | 什么时候停止拆 | 一直拆下去 → 栈溢出崩溃 |
| 向出口靠近 | 每次拆都更小 | 参数不变 → 死循环,同样崩 |
记忆句:递归像下楼梯——必须每一级都往下走,还得有个最低层。
二、递归的两段动作:"递"与"归"
以 fact(5) 为例:
递(往下钻):
fact(5) = 5 × fact(4)
fact(4) = 4 × fact(3)
fact(3) = 3 × fact(2)
fact(2) = 2 × fact(1)
fact(1) = 1 ← 出口
归(往上拼):
1 → 2×1=2 → 3×2=6 → 4×6=24 → 5×24=120
"递"是去程、"归"是回程——递归的名字就来自这两段。
三、实例 05:求 n 的阶乘(循环版 vs 递归版)
教材的实例是用循环求阶乘,这里把两种写法并排看:
// 版本一:循环(第五章的做法)
int factLoop(int n) {
int r = 1;
for (int i = 1; i <= n; i++) r *= i;
return r;
}
// 版本二:递归(本节的做法)
int factRec(int n) {
if (n <= 1) return 1;
return n * factRec(n - 1);
}
完整程序:
#include <iostream>
using namespace std;
int factLoop(int n) {
int r = 1;
for (int i = 1; i <= n; i++) r *= i;
return r;
}
int factRec(int n) {
if (n <= 1) return 1; // 出口
return n * factRec(n - 1); // 向出口靠近
}
int main() {
for (int n = 1; n <= 6; n++) {
cout << n << "! = " << factLoop(n)
<< " , 递归版 " << factRec(n) << endl;
}
return 0;
}
看到什么算成功:
1! = 1 , 递归版 1
2! = 2 , 递归版 2
3! = 6 , 递归版 6
4! = 24 , 递归版 24
5! = 120 , 递归版 120
6! = 720 , 递归版 720
两列完全一致——递归不是玄学,只是另一种写法。
⚠️ 一个真实的坑:int 装不下 13! 以上(13! ≈ 62 亿 > int 上限约 21 亿),会溢出回绕成负数(2.3.3 的老坑)。试试输出 factRec(15) 看结果有多离谱——这是"类型范围"知识的实战复习。
30 秒实验:把出口条件从 if (n <= 1) 改成 if (n == 1),然后调 factRec(0)——它会一路接到 factRec(-1)、factRec(-2)……栈溢出崩溃。体会"出口必须覆盖所有该停的情形"这句话。
四、什么时候用递归(判断标准)
| 适合 | 不适合 |
|---|---|
| 问题本身是"同类更小问题"的重复:阶乘、斐波那契、汉诺塔、树的遍历、二分查找 | 简单的数字累加、遍历数组——循环更直接更快 |
| 代码能短很多、逻辑一眼看懂 | 递归深度很大(几千层)——栈装不下 |
一句话:递归负责"把问题说清楚",循环负责"把活干快"。能用循环轻松表达就别硬凹递归;但遇到"天然分层"的问题(树、目录、括号匹配),递归往往是最省心的表达。
五、常见误区
- "递归比循环高级"——不是。每次递归都要记位置、开变量(6.3 的栈机制),通常比循环慢。选谁看问题形状。
- "有出口就一定安全"——还得看深度。
factRec(100000)是有出口的,但十万层调用直接撑爆栈。 - "递归调用不用写 return"——这是递归第一大坑。写成
factRec(n - 1);(漏return)时能编过、也确实在递归,但每层都把算出来的值扔了,最后交回垃圾值。写完照着模板逐字比对return。 - "递归里同名变量会互相干扰"——不会。每次调用都新开一套形参和局部变量(6.2.2 的复制 + 6.3 的栈),各层的
n各管各的。
自测三问(点开对答案)
1. 递归的两个必备要素是什么?——出口(停止条件)和向出口靠近(每次问题变小)。 2. "递"和"归"分别指什么?——递是往下拆问题,归是把答案一层层拼回来。 3. 递归里漏写 `return` 会怎样?——能编过、也在递归,但每层的返回值被丢弃,最终得到垃圾值。- ☐ 6.4 变量作用域(下一节)
相关笔记
- 嵌套调用 —— 递归是它的最特殊形态
- while 循环 —— 同一个问题的循环解法
- 整型变量 —— 13! 溢出的原因
- 🔷 C++基础 · 索引 —— 全课路线图