JOVANA
Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

递归:会调用自身的函数

一个会调用自身的函数听起来像魔术,其实它只是一句诚实的承诺:**直接解决最小的情形,然后相信自己能解决稍小一点的剩余部分**。掌握了[[dsa-recursion|递归]],分治、快速排序和树就都向你敞开了。

永远是两个部分

递归是用自身来定义的函数。每个正确的递归函数都恰好有两个部分。基准情形是一个你能立刻回答、无需再次调用的小输入——它让过程停下来。递归情形做一点点工作,然后在更小的输入上调用同一个函数,相信它会带回正确答案。

最经典的第一个例子是阶乘:n! = n × (n−1)!,而 0! = 1。这个定义本身就是递归——基准情形是 0,每一步剥下一个因子,把其余的推迟下去。

long long fact(int n) {
    if (n == 0) return 1;        // base case
    return n * fact(n - 1);      // recursive case: smaller input
}
// fact(4) = 4 * fact(3) = 4 * 3 * fact(2) = ... = 24
阶乘:一个基准情形,一次在 n-1 上的递归调用。

调用栈替你记账

计算机怎么记得 fact(4) 在 fact(3) 返回后还欠着一个 `4 *`?它用调用栈——正是你在线性结构那一级见过的后进先出结构。每次调用都压入一个栈帧,保存它的局部变量和恢复执行的位置。调用返回时,它的栈帧弹出,控制权交回调用者。

Calling fact(3): frames pushed downward, then popped (return).

  push  fact(3)  waits for fact(2)
  push  fact(2)  waits for fact(1)
  push  fact(1)  waits for fact(0)
  push  fact(0) = 1   <-- base case, returns now
        fact(1) = 1*1 = 1   pop
        fact(2) = 2*1 = 2   pop
        fact(3) = 3*2 = 6   pop  --> answer
每次调用都等待它下面那次;基准情形让栈逐层回退、收束。

这就是为什么递归像一种信任:你写 fact(n) 时就当作 fact(n−1) 已经能用了。栈默默替你记下没做完的工作,你自己永远不必去管。

当递归重复劳动:斐波那契

递归很优雅,但并不天生高效。斐波那契数——fib(n) = fib(n−1) + fib(n−2),其中 fib(0)=0、fib(1)=1——每次会发起两次递归调用。画出递归树就能看到同样的子问题被一遍遍重算。

fib(5)
           ____________ fib(5) ____________
          /                                \
      fib(4)                              fib(3)
      /     \                            /     \
   fib(3)   fib(2)                    fib(2)   fib(1)
   /   \     /  \                      / \
fib(2) f1  f1  f0                   f1  f0
 / \
f1 f0

  fib(3) is computed twice, fib(2) three times ... work explodes.
斐波那契递归树:重叠的子问题让它大致是 O(2^n)。

解决办法是记忆化:第一次算出每个 fib(k) 时就把它记下来,之后直接复用。这能把树压成一条线,把 O(2^n) 变成 O(n)。在动态规划那一级,你会再次遇到这个思想,并大展身手。

递归出错的两种方式

由于每次调用都吃掉一个帧,递归有两种著名的失败方式。无限递归是忘了基准情形(或根本没朝它靠近),于是函数永远调用自己。栈溢出是递归太深——即便完全正确的递归,如果嵌套上百万层,也会崩溃,因为栈的大小是有限的。

// BUG: no base case is ever reached -> infinite recursion -> stack overflow
int bad(int n) {
    return bad(n - 1);    // never stops; n races past 0 into negatives
}
// FIX: a base case that the recursion actually reaches
int good(int n) {
    if (n <= 0) return 0;
    return good(n - 1);
}
务必确保每次调用都更靠近一个可达到的基准情形。