Lesson 0010 · 递归

C++ 进阶 — 第 3 课

上节课(0009 结构体与 class):把名字、分数捆成 Student,再用比较函数给它排序。
这节课:换一种写“重复”的方式 —— 函数自己调用自己。递归的写法更贴近数学定义,是树、图这些结构的核心工具,也是后面分治、回溯的入口。
三句话速览
  1. 递归就是函数自己调用自己。
  2. 两件必写:终止条件、每次把问题改小。
  3. 递归靠调用栈,太深会栈溢出。

1. 递归是什么

一句话:函数调用自己,每次把问题缩小一点,直到最简单的情况直接返回。

// 循环版:求 1+2+...+n
int sum(int n) {
    int s = 0;
    for (int i = 1; i <= n; i++) s += i;
    return s;
}

// 递归版
int sum(int n) {
    if (n == 1) return 1;              // 终止条件(最简单的情况)
    return n + sum(n - 1);             // 缩小问题:n + 前面 n-1 个的和
}

怎么执行的?以 n=3 为例

sum(3) = 3 + sum(2)
              = 2 + sum(1)
                    = 1          ← 触底,开始返回
              = 2 + 1 = 3
       = 3 + 3 = 6

一路拆下去,触底后再一路算回来 —— 像走楼梯下楼再上楼。

2. 递归两要素

要素例子没写会怎样
终止条件if (n == 1) return 1;无限递归 → 栈溢出 💀
缩小规模return n + sum(n - 1);不缩小也会无限递归

3. 经典例子

阶乘

int fact(int n) {
    if (n == 0) return 1;          // 0! = 1
    return n * fact(n - 1);        // n! = n * (n-1)!
}

斐波那契

int fib(int n) {
    if (n <= 2) return 1;           // fib(1)=1, fib(2)=1
    return fib(n - 1) + fib(n - 2); // fib(n) = fib(n-1) + fib(n-2)
}
递归版斐波那契 慎用! fib(40) 会算到天荒地老 —— 大量重复计算。实际用循环版(你之前写的那个)或加“记忆化”。

最大公约数(辗转相除法)

int gcd(int a, int b) {
    if (b == 0) return a;
    return gcd(b, a % b);
}
// gcd(48, 18) = gcd(18, 12) = gcd(12, 6) = gcd(6, 0) = 6

4. 递归 vs 循环

对比项递归循环
代码量少、优雅稍多
可读性贴近数学定义贴近执行过程
性能函数调用有开销更快
适合树、图、分治简单重复
经验法则:能用循环简单解决的用循环。当问题本身是“自相似”结构(树、分治、回溯)时,递归更自然。

5. 递归的代价 —— 调用栈

int sum(int n) {
    if (n == 1) return 1;
    return n + sum(n - 1);
}
// sum(100000) → 10 万个函数调用堆在栈上 → 栈溢出崩溃

每个函数调用占用一小块内存(栈帧),递归太深会爆。一般递归深度别超过几千到几万。

6. 坑点清单

7. 小测

Q1. 递归最重要的是什么?
Q2. fact(0) 返回什么?(fact 是阶乘函数)
Q3. 递归版斐波那契为什么慢?

8. 动手练习

题目 1:递归求阶乘
写递归函数 int fact(int n),求 n 的阶乘。读入 n,输出 n!。
提示:fact(n) = n * fact(n-1),fact(0) = 1。
题目 2:递归求最大公约数
用辗转相除法(递归版)求两个数的最大公约数。
提示:gcd(a, b) = gcd(b, a % b),终止条件 b == 0。
卡住时的自查顺序
  1. 终止条件写了吗?判断的值是规模真正会缩减到的那一个吗(比如阶乘是 n == 0)?
  2. 每次递归,参数有没有变小?没变小就是死循环式的无限递归。
  3. 运行时报“栈溢出”或者直接崩:先用小得多的输入(比如 n=5)试,先确认逻辑对,再加大。
  4. 算出来是错的数:把每次调用的参数 cout 出来,一层层看它在哪一步开始不对。

本课小结

下一课预告
0011 栈、队列与优先队列 —— 递归背后那个“调用栈”到底怎么工作,这一课把它换成看得见的 stack 容器;顺便学队列和优先队列,为后面的 BFS、贪心打基础。

← 上一课:结构体与面向对象入门 · 课程总览