跳到主要内容

递归

递归用同类子问题求解当前问题。本文负责终止、调用成本和记忆化。

1. 面试核心​

先记住这几条,再读正文
  • 终止条件。 递归终止需要每条分支都向可达的终点推进。只写终止条件还不够,还要保证参数或状态不断接近它。卡 · 子问题与终止条件
  • 时间与空间。 递归时间取决于全部调用完成的工作,栈空间取决于同时存在的最大调用深度。大量分支与很深的调用链,分别影响这两项成本。
  • 记忆化。 记忆化缓存完整状态的结果,消除同一子问题的重复求值。缓存键必须包含影响结果的全部条件。卡 · 记忆化与缓存键

2. 子问题与终止条件​

两个非负整数满足 gcd(a,b)=gcd(b,a%b)。b 大于 0 时,余数严格小于 b,所以最终降至 0。

static int gcd(int a, int b) {
if (a < 0 || b < 0) throw new IllegalArgumentException();
if (b == 0) return a;
return gcd(b, a % b);
}

gcd(18,12) 经 (12,6)、(6,0) 返回 6。本例约定 gcd(0,0)=0。终止条件与递减量共同给出终止证明。

3. 调用次数与深度​

递推式 T(n)=T(n-1)+O(1) 表示每次减少一项。时间与栈空间都为 O(n)。每次减半且只访问一支时,两者为 O(log n)。

斐波那契的直接递归重复计算同一参数。总调用数指数增长,最大活动深度却只有 O(n)。一次调用结束后,它的栈帧就不再占据活动路径。

4. 记忆化与缓存键​

斐波那契只有 n+1 种非负参数。每个参数只计算一次,可把时间降为 O(n)。缓存与递归栈各为 O(n),大数运算成本另计。

缓存键必须包含影响结果的所有参数。例如网格路径受位置和剩余资源共同影响。只缓存位置会混合不同子问题。

未计算状态应与合法结果区分。若 0 也是答案,可另用 computed 数组记录。记忆化在完成计算后保存结果。有环依赖仍需检测计算中状态,或改用匹配的求解方法。

5. 使用边界​

Java JVM 为方法调用保存栈帧。规范未承诺尾递归自动消除栈帧。十万层深链应改为迭代或显式栈。

显式栈仍需保存未完成任务,其空间应计入复杂度。树的分治结果可在子调用返回后合并。共享路径的恢复机制见回溯。

6 · 面试题卡片​

先想答案,再翻面先在心里答一遍,再点开卡片对照参考回答。卡片之间可以左右切换,看完会回到这里。
逐张翻看 2 张 →
未标记的卡读完后明天进入复习;已标记的按状态安排
本页题目与参考回答

6.1 有终止条件的递归为何仍会不结束或栈溢出?​

考察重点:状态推进与栈深度上界。

相关内容:子问题与终止条件、使用边界。

参考回答

有终止条件仍不够,还要保证每条分支能到达它,且递归深度不超过线程栈容量。

状态循环会导致无限调用;即使调用次数有限,过深的调用链也可能栈溢出。应证明每次调用都在推进,并根据最坏深度选择递归或迭代。

6.2 记忆化为何需要完整缓存键和独立的未计算标记?​

考察重点:状态等价与合法零值。

相关内容:记忆化与缓存键。

参考回答

完整键才能确保复用的是同一个子问题。

省掉资源等条件会把不同结果混在一起。未计算标记还要与合法的 0 区分,否则会重复计算或误判。

7. 参考资料​