女孩和机器人一起思考

一起动脑筋 · 先看一个小故事

女孩用三张任务卡画出递归,向下时叠上新卡,返回时拿走顶卡。

把过程摊开来看

沿着编号看一遍,再用自己的话讲一遍
  1. 进入 f(3)栈多一张
  2. 进入 f(2)再多一张
  3. 进入最小任务停止增加
  4. 返回一张一张取走

每一层普通递归调用都需要记住返回位置,以及本层仍要使用的信息。

栈顶是当前执行的层,下面的是等待中的层。观察栈的高度,就能看到递归有多深。

看见调用,也看见返回

任务卡从左到右表示从栈底到栈顶。最右边是当前正在执行的一层。

第 1 步 · 进入 factorial(3)
f(3)

这一层要算 3 × f(2),先等待 f(2)。

第 2 步 · 进入 factorial(2)
f(3) 等待f(2)

这一层要算 2 × f(1),再新增一层。

第 3 步 · 继续到最小情况
f(3) 等待f(2) 等待f(1) 等待f(0) = 1

f(0) 直接返回 1,不再新增调用。

第 4 步 · 回到 factorial(1)
f(3) 等待f(2) 等待f(1) = 1

计算 1 × 1,得到 1 并返回。

第 5 步 · 回到 factorial(2)
f(3) 等待f(2) = 2

计算 2 × 1,得到 2 并返回。

第 6 步 · 回到 factorial(3)
f(3) = 6

计算 3 × 2,得到 6。本次调用完成。

01从 f(3) 开始看

void f(int n) {
    if (n == 0) {
        return;
    }

    f(n - 1);
}

调用 f(3) 后,会继续调用 f(2)f(1)f(0)

02调用栈怎样一层层增加?

第 1 步

f(3)

第 2 步

f(2)
f(3)

第 3 步

f(1)
f(2)
f(3)

再调用 f(0) 时,还会继续增加一层。

03每一层的 n 是同一个变量吗?

不是。每个函数调用都有自己的形参对象。

f(3)

n = 3

f(2)

n = 2

f(1)

n = 1

f(0)

n = 0

这些调用同时活跃时,各自都保留自己的状态。

04为什么上一层要“等”下一层?

f(3) 调用 f(2) 时,f(3) 还没有结束。

它必须等 f(2) 完成并返回后,才能继续执行调用语句之后的代码。

05到达基本情况以后发生什么?

f(0) 直接 return回到 f(1)回到 f(2)回到 f(3)

这时调用栈开始从最上层逐层减少。

06调用栈示意图是真实内存的精确照片吗?

不是。它是非常重要的程序执行模型,也是常见实现方式。

具体栈帧中放哪些内容、怎样对齐、哪些变量放寄存器,都由编译器、平台 ABI 和优化决定。

你已经知道了什么

  • 递归的每一次调用都有独立调用状态。
  • 常见实现会把活跃调用组织在调用栈中。
  • 每层递归有自己的参数和局部变量。
  • 更深层调用完成后,才能回到上一层继续。
  • 达到基本情况后,调用会按相反顺序逐层返回。

下一篇:递归是怎样一层一层返回的?

轮到你来试一试

一层卡片返回时,应拿走底部还是顶部?

想好了吗?点开看解释

顶部。底部还有尚未完成的调用,必须等上面的结果回来。