{ }codelper

学习 › C

进阶 · 21

递归

函数调用自己:终止条件、调用栈深度、循环与记忆化。

递归就是函数调用自己。
下面的例子用递归求各位数字之和(digit_sum(12345) 是 5 + digit_sum(1234)),并一边记录一边求斐波那契数。

#include <stdio.h> int digit_sum(int n) {    if (n < 10) return n; /* 바닥: 한 자리면 그대로 */    return n % 10 + digit_sum(n / 10);} long long memo[91]; /* 전역 배열은 0으로 시작해요 */ long long fib(int k) {    if (k <= 1) return k; /* 바닥 */    if (memo[k] == 0) memo[k] = fib(k - 1) + fib(k - 2); /* 한 번 구한 값은 적어 두기 */    return memo[k];} int main(void) {    int n, k;    scanf("%d %d", &n, &k);    printf("%d\n", digit_sum(n));    printf("%lld\n", fib(k));    return 0;}
stdin
12345
50
stdout
15
12586269025

要点

  • 把大问题缩小成同样形状的小问题。
    小到不能再缩小时就直接返回答案,这就是终止条件(base case)。
  • 写法和普通函数一样。
    唯一的不同是函数内部又调用了自己的名字。
  • 每调用一次,参数和局部变量就在调用栈上压入一层,返回时再弹出一层。
    所以深度是有限的。
  • 像求 1 到 n 的和、倒数这样一条直线走下去的工作,用循环更好:没有深度限制,也更快。
    递归在会分岔的工作里才发挥作用:文件夹里的文件夹、树形数据、尝试所有可能。
  • 把算过的值记在字典或数组里(记忆化),就不会重复计算。
    把这个思路扩展成一张表,就是动态规划。

各语言的写法

Pythondef f(n): 里调用 f(n - 1) — 默认深度上限 1000 层,超过就抛出 RecursionError。
在函数上面加上 @functools.cache 就能记忆化
JavaScriptfunction f(n) { } — 太深会抛出 RangeError: Maximum call stack size exceeded
TypeScript和 JavaScript 一样
Java在前面写返回类型(static int f(int n))— 太深会抛出 StackOverflowError
C#static int F(int n) — StackOverflowException 无法用 catch 捕获,程序会直接结束
C++int f(int n) — 栈溢出时通常以 Segmentation fault 崩溃
C和 C++ 一样
Craykon名字(参数) { } 里用 返回 — 几千层时会提示“函数调用层次太深,已停止”。
列表传递时不会被复制,所以例子里的记忆列表(新列表(k + 1, 0))被所有调用共用

常见错误

永远到不了终止条件

def f(n):
    if n == 0:
        return 0
    return n + f(n)

→ f(n) 无休止地调用自己,最后因深度错误而停止

每次都朝终点缩小(f(n - 1))。
动手前先确认:什么时候停?
每次是否更近?

不带记忆地求斐波那契

def fib(k):
    if k <= 1:
        return k
    return fib(k - 1) + fib(k - 2)

→ 同样的值被一遍遍重算,fib(50) 要调用几百亿次

把算过的值记在字典或数组里,大约调用 100 次就结束了。

各语言的差异
递归一定要有终止条件,而且每次调用都必须朝它靠近。
如果深度可能超过几千(比如输入 n 可达 10 万),一开始就写成循环。

练习题