一、递归的基本概念与核心要素
递归(Recursion)是计算机科学中一种强大的问题求解范式,其核心思想是:将原问题分解为一个或多个规模更小的同类子问题,通过求解子问题来得到原问题的解。递归不仅是一种编程技巧,更是一种重要的思维模式——它要求我们具备"相信递归"(Leap of Faith)的能力,即假设子问题可以被正确解决,再在此基础上构建原问题的解。
1.1 递归三要素
任何一个正确的递归函数都必须满足以下三个核心条件:
递归的核心三要素
- 基线条件(Base Case): 递归的终止条件,即问题规模小到可以直接求解而无需继续递归。缺少基线条件将导致无限递归,最终引发栈溢出错误。基线条件通常对应问题的最小子实例。
- 递归条件(Recursive Case): 将原问题分解为规模更小的同类子问题,并调用函数自身处理这些子问题。递归条件必须确保每次都向基线条件逼近。
- 递归公式(Recurrence Relation): 定义原问题与子问题之间的定量关系。递归公式是递归算法的数学基础,也是后续复杂度推导的依据。
1.2 递归的思维范式
编写递归函数时,需要完成以下三个步骤:
- 明确函数含义: 清晰定义函数的输入参数和返回值。递归函数应当只关注其"做什么"而非"怎么做"。
- 确定基线条件: 找到问题的最简形式,直接返回结果。这是递归停止的出口。
- 分解问题: 将当前问题转化为更小的同类问题,并利用递归调用的结果构建答案。
def recursive_function(parameters):
if base_case_condition(parameters):
return base_case_value
smaller_result = recursive_function(reduced_parameters)
return combine(smaller_result, current_parameters)
以经典的阶乘函数为例,它完美诠释了递归三要素:基线条件是 n == 0 时返回 1;递归条件是 n > 0 时调用 factorial(n-1);递归公式为 n! = n * (n-1)!。
def factorial(n):
"""计算n的阶乘(递归版本)"""
if n == 0:
return 1
return n * factorial(n - 1)
每个递归调用都隐含着信任——我们相信 factorial(n-1) 能正确返回 (n-1)! 的值,然后乘上 n 即可。这种"递归的信仰之跃"是掌握递归思维的关键。
二、递归的调用栈机制
2.1 栈帧与调用栈
理解递归的底层执行机制,必须先理解调用栈(Call Stack)。每当程序调用一个函数时,系统会在调用栈上为该函数分配一个栈帧(Stack Frame),用于存储:函数的局部变量、参数值、返回地址(调用函数的位置)。当函数执行完毕后,其栈帧被弹出,控制权返回到调用点。
递归调用本质上就是函数不断调用自身,每次调用都会在栈顶压入一个新的栈帧。这些栈帧层层堆积,直到遇到基线条件触发回归,再逐层弹出。这一过程分为两个阶段:
- 递推阶段(递去): 从初始调用开始,每次递归调用都压入新栈帧,向基线条件逼近。此阶段尚未计算结果,只是创建调用上下文。
- 回归阶段(归来): 达到基线条件后,从最内层开始逐层返回结果,每返回一层就弹出一个栈帧。此阶段才是真正计算结果的时候。
2.2 阶乘函数的栈执行过程
以 factorial(4) 为例,其执行过程如下:
递推阶段(压栈)
call factorial(4) → n=4, 期望 4 * factorial(3)
call factorial(3) → n=3, 期望 3 * factorial(2)
call factorial(2) → n=2, 期望 2 * factorial(1)
call factorial(1) → n=1, 期望 1 * factorial(0)
call factorial(0) → 基线条件, 返回 1
回归阶段(弹栈)
return 1 → factorial(0) 弹出
return 1 * 1 → factorial(1) 弹出
return 2 * 1 → factorial(2) 弹出
return 3 * 2 → factorial(3) 弹出
return 4 * 6 = 24 → factorial(4) 弹出, 得到最终结果
整个过程在栈上同时存在 5 个栈帧。如果递归深度过大,栈帧数量超过系统限制,就会发生栈溢出(Stack Overflow)。Python 默认递归深度限制约为 1000 层,超出后将抛出 RecursionError。
import sys
print(sys.getrecursionlimit())
def deep_recursion(n):
if n == 0:
return 0
return 1 + deep_recursion(n - 1)
sys.setrecursionlimit(100)
try:
deep_recursion(200)
except RecursionError as e:
print(f"栈溢出: {e}")
sys.setrecursionlimit(1000)
2.3 栈溢出的深层原因
栈溢出本质上是因为每个栈帧都占用内存。一个典型的函数栈帧可能包含:返回地址(8字节)、保存的基址指针(8字节)、局部变量(若干字节)、参数(若干字节)。当递归深度达到数千层时,栈空间(通常为1MB-8MB)被耗尽,操作系统会强制终止程序以防止内存越界。
此外还需注意,Python 的递归限制不仅受栈空间约束,还受解释器的保护机制限制。CPython 使用 _Py_CheckRecursionLimit 主动检查递归深度,默认为 1000,这远低于实际的栈空间上限,目的是避免程序因栈溢出而崩溃(Python 的 C 层面栈溢出无法优雅捕获)。
三、经典递归问题
3.1 斐波那契数列
斐波那契数列是最经典的递归教学案例,其定义本身就是递归的:F(0)=0, F(1)=1, F(n)=F(n-1)+F(n-2)。
def fib_naive(n):
if n <= 1:
return n
return fib_naive(n - 1) + fib_naive(n - 2)
def fib_memo(n, memo={}):
if n <= 1:
return n
if n not in memo:
memo[n] = fib_memo(n - 1, memo) + fib_memo(n - 2, memo)
return memo[n]
print(fib_memo(100))
朴素递归为什么慢?
朴素斐波那契递归的时间复杂度为 O(2n),原因在于存在大量的重复计算。例如计算 fib(5) 时需要计算 fib(3) 两次、fib(2) 三次。fib(n) 的递归调用总次数约为 2n 量级。记忆化递归通过缓存已计算的结果,将每次子问题只计算一次,复杂度降为 O(n)。
3.2 汉诺塔
汉诺塔(Tower of Hanoi)是递归思想的最佳体现之一。问题的核心是:将 n 个盘子从 A 柱移动到 C 柱,每次只能移动一个盘子,且大盘子不能压在小盘子上。
递归解法极其简洁:
- 将上面 n-1 个盘子从 A 移到 B(借助 C)—— 递归子问题
- 将最大的盘子直接从 A 移到 C —— 直接操作
- 将 n-1 个盘子从 B 移到 C(借助 A)—— 递归子问题
def hanoi(n, source, target, auxiliary):
"""将n个盘子从source柱移动到target柱,借助auxiliary柱"""
if n == 1:
print(f"移动盘子 1: {source} -> {target}")
return
hanoi(n - 1, source, auxiliary, target)
print(f"移动盘子 {n}: {source} -> {target}")
hanoi(n - 1, auxiliary, target, source)
hanoi(3, "A", "C", "B")
汉诺塔的时间复杂度为 O(2n),即 n 个盘子需要 2n - 1 步。这是递归分治策略的一个极端案例——问题规模每增加 1,工作量翻倍。传说中 64 个金盘需要 264 - 1 步,每秒移动一步需要约 5845 亿年才能完成。
3.3 二叉树遍历
二叉树的递归遍历是递归在数据结构中的典型应用。前序、中序、后序遍历的三行代码完美展现了递归的简洁之美。
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def preorder(root):
if not root:
return
print(root.val, end=" ")
preorder(root.left)
preorder(root.right)
def inorder(root):
if not root:
return
inorder(root.left)
print(root.val, end=" ")
inorder(root.right)
def postorder(root):
if not root:
return
postorder(root.left)
postorder(root.right)
print(root.val, end=" ")
这三种遍历方式的时间复杂度均为 O(n),空间复杂度在最坏情况下为 O(n)(斜树),平均情况下为 O(log n)(平衡树)。递归遍历的优雅之处在于:无需手动维护栈,系统调用栈自然地记录了遍历路径。
3.4 全排列与组合
全排列问题是回溯算法(Backtracking)的经典代表。回溯可以看作递归的一种特殊形式——在递归的每一层做选择和撤销选择。
def permute(nums):
"""返回数组 nums 的所有全排列"""
result = []
used = [False] * len(nums)
def backtrack(path):
if len(path) == len(nums):
result.append(path[:])
return
for i in range(len(nums)):
if used[i]:
continue
used[i] = True
path.append(nums[i])
backtrack(path)
path.pop()
used[i] = False
backtrack([])
return result
print(permute([1, 2, 3]))
全排列的时间复杂度为 O(n!),组合问题(如从 n 个元素中选 k 个)的时间复杂度为 O(C(n,k))。回溯算法的共性模式是"选择→递归→撤销选择",其本质是在递归树上做深度优先遍历。
def combine(n, k):
result = []
def backtrack(start, path):
if len(path) == k:
result.append(path[:])
return
for i in range(start, n + 1):
path.append(i)
backtrack(i + 1, path)
path.pop()
backtrack(1, [])
return result
四、递归的时间与空间复杂度分析
4.1 时间复杂度:主定理
分析递归算法的时间复杂度,最常用的工具是主定理(Master Theorem)。对于形如 T(n) = aT(n/b) + f(n) 的递推关系,其中 a 是子问题个数,n/b 是子问题规模,f(n) 是分解与合并的代价,主定理给出了三种情况:
主定理的三种情况
令 ccrit = logba:
- 若 f(n) = O(nc) 且 c < ccrit: 则 T(n) = O(nccrit)。递归占主导,合并代价较小。
- 若 f(n) = O(nc) 且 c = ccrit: 则 T(n) = O(nc log n)。两端平衡。
- 若 f(n) = O(nc) 且 c > ccrit: 则 T(n) = O(f(n))。合并代价占主导。
常见递归算法的时间复杂度速查表:
| 递归算法 |
递推关系 |
时间复杂度 |
空间复杂度 |
| 二分查找 |
T(n) = T(n/2) + O(1) |
O(log n) |
O(log n) |
| 归并排序 |
T(n) = 2T(n/2) + O(n) |
O(n log n) |
O(n) |
| 快速排序(平均) |
T(n) = 2T(n/2) + O(n) |
O(n log n) |
O(log n) |
| 斐波那契(朴素) |
T(n) = T(n-1) + T(n-2) + O(1) |
O(2n) |
O(n) |
| 汉诺塔 |
T(n) = 2T(n-1) + O(1) |
O(2n) |
O(n) |
| 二叉树遍历 |
T(n) = T(k) + T(n-k-1) + O(1) |
O(n) |
O(h) |
4.2 空间复杂度:栈深度的代价
递归的空间复杂度主要取决于递归栈的最大深度,而非总调用次数。每次递归调用占用的栈帧在返回前不会释放。因此:
- 线性递归(每次只调用一次自身,如阶乘):空间复杂度 O(n),最坏情况线性增长。
- 树形递归(每次调用多次自身,如朴素斐波那契):空间复杂度 O(n) 而非 O(2n),因为每一时刻栈上仅存在一条路径上的帧。
- 尾递归(递归调用在函数末尾):理想情况下空间复杂度可优化为 O(1),具体取决于语言实现。
递归空间复杂度的常见误解
很多初学者误以为斐波那契朴素递归的空间复杂度是 O(2n),这是错误的。虽然总调用次数是指数级的,但调用栈的深度(同时存在的栈帧数)最多为 n(因为递归树的最长路径是从根到叶子)。因此朴素斐波那契的空间复杂度实际上是 O(n)。理解这一点有助于准确评估递归算法的资源消耗。
五、递归与迭代的转换
5.1 什么时候需要转换?
递归虽简洁,但在实际工程中面临三个主要问题:栈溢出风险、性能开销(函数调用、参数传递、栈帧分配均有成本)、以及调试困难(深层调用的调用栈难以追踪)。因此,将递归转换为等效的迭代版本是一项重要技能。
5.2 显式栈模拟
将递归转换为迭代的最通用方法是用程序员栈替代系统调用栈。即手动维护一个栈数据结构,模拟递归的压栈和弹栈过程。以前序遍历为例:
def preorder_recursive(root):
if not root:
return
print(root.val)
preorder_recursive(root.left)
preorder_recursive(root.right)
def preorder_iterative(root):
if not root:
return
stack = [root]
while stack:
node = stack.pop()
print(node.val)
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
显式栈模拟的核心模式:
- 创建一个栈,初始压入递归的"入口"参数
- 循环直到栈为空
- 每次从栈中弹出当前需要处理的状态
- 根据状态决定是继续递归(压入子状态)还是计算结果
对于更复杂的递归(如后序遍历),迭代版本需要使用标记法或双栈法来模拟递归的"回归"阶段。
def postorder_iterative(root):
if not root:
return
s1, s2 = [root], []
while s1:
node = s1.pop()
s2.append(node)
if node.left:
s1.append(node.left)
if node.right:
s1.append(node.right)
while s2:
print(s2.pop().val)
def inorder_iterative(root):
stack, curr = [], root
while stack or curr:
while curr:
stack.append(curr)
curr = curr.left
curr = stack.pop()
print(curr.val)
curr = curr.right
5.3 尾递归自动转换
一些编译器/解释器可以对尾递归进行自动优化(Tail Call Optimization, TCO),将其转换为等价的循环,从而避免栈增长。但 Python 不进行此优化(详见下一章)。在不支持 TCO 的语言中,可以手动将尾递归改写为循环:
def factorial_tail(n, acc=1):
if n == 0:
return acc
return factorial_tail(n - 1, n * acc)
def factorial_iter(n):
acc = 1
while n > 0:
acc *= n
n -= 1
return acc
对于更通用的递归转迭代,还可以使用蹦床函数(Trampoline)技术。蹦床函数通过不断调用返回的 thunk(零参数闭包或函数对象)来模拟递归,将栈上的递归转换为堆上的迭代。
def trampoline(f):
"""蹦床装饰器:将递归调用转移到while循环中执行"""
def wrapped(*args, **kwargs):
result = f(*args, **kwargs)
while callable(result):
result = result()
return result
return wrapped
def factorial_thunk(n, acc=1):
if n == 0:
return acc
return lambda: factorial_thunk(n - 1, n * acc)
factorial_safe = trampoline(factorial_thunk)
print(factorial_safe(10000))
蹦床的实质是将递归调用延迟执行——每次递归不直接在栈上调用,而是返回一个闭包,由外层的 while 循环依次调用这些闭包。这使得递归深度不再受栈大小限制。
六、尾递归的概念与优化
6.1 什么是尾递归?
尾递归(Tail Recursion)是一种特殊形式的递归,其中递归调用是函数体中执行的最后一个操作,且递归调用的返回值直接被当前函数返回,不再进行任何额外计算。换言之,尾递归函数在递归调用之后不再有"回归"阶段。
int factorial(int n) {
if (n == 0) return 1;
return n * factorial(n - 1);
}
int factorial_tail(int n, int acc) {
if (n == 0) return acc;
return factorial_tail(n - 1, n * acc);
}
尾递归的判定标准非常严格:递归调用必须是 return 语句中的唯一表达式。以下情况都不是尾递归:
- return 1 + f(n-1) —— 递归返回后还有加法
- return f(n-1) + f(n-2) —— 两次递归调用,且中间有加法
- print(f(n-1)); return result —— 递归调用不是在 return 语句中
6.2 尾调用优化(TCO)的原理
尾调用优化的核心思想是:当前栈帧在尾调用之后不再被使用,因此可以在尾调用之前释放当前栈帧,或者直接用被调用函数的栈帧覆盖当前栈帧。这样做的效果是:
- 递归深度不再受栈空间限制——尾递归等价于循环
- 空间复杂度从 O(n) 降为 O(1)
- 消除了函数调用/返回的开销,性能接近循环
TCO 的实现机制(伪代码):
factorial_tail(5, 1)
→ factorial_tail(4, 5)
→ factorial_tail(3, 20)
→ factorial_tail(2, 60)
→ factorial_tail(1, 120)
→ factorial_tail(0, 120)
→ return 120
factorial_tail(5, 1)
→ factorial_tail(4, 5)
→ factorial_tail(3, 20)
→ factorial_tail(2, 60)
→ factorial_tail(1, 120)
→ factorial_tail(0, 120)
→ return 120
6.3 Python 为什么不优化尾递归?
CPython 解释器不支持尾调用优化,这是 Python 语言设计者 Guido van Rossum 的明确选择。其原因可归纳为以下几点:
CPython 不支持 TCO 的原因
- 调试体验优先: TCO 会破坏完整的调用栈信息。当尾递归优化发生后,栈上只保留当前帧,程序员无法回溯完整的调用链来进行调试。Python 的设计哲学高度重视可调试性。
- 栈回溯(Traceback)准确性: Python 的异常处理依赖完整的栈帧链(__traceback__)。TCO 会丢失栈帧信息,导致异常追踪不准确,严重影响调试体验。
- 装饰器和元编程兼容性: Python 支持函数装饰器、性能分析器(Profile)、sys.settrace 等内省机制,这些机制依赖栈帧的完整性。TCO 会破坏这些工具的正常工作。
- 性能收益有限: Python 的函数调用开销本身远高于 C/JVM 等编译型语言。即使做了 TCO,尾递归仍不如直接使用 while 循环高效。与其优化尾递归,不如直接推荐用户使用迭代。
- JIT 缺失: CPython 是纯解释器(无 JIT 编译),无法在编译阶段进行栈帧复用的底层优化。而支持 TCO 的语言(如 V8/JavaScript、JVM 上的 Scala)通常都有 JIT 编译器。
6.4 语言对 TCO 的支持对比
| 编程语言 |
支持 TCO? |
实现方式 |
备注 |
| Scheme(Lisp) |
是 |
语言规范强制要求 |
所有实现必须进行尾调用优化,这是 Scheme 语言的核心特性之一 |
| Haskell |
是 |
惰性求值 + 编译器优化 |
由于惰性求值,尾递归形式通常不是最高效的;更常用 fold 等组合子 |
| Scala |
是 |
@tailrec 注解 + JVM 字节码优化 |
带有 @tailrec 注解的尾递归函数会被编译器自动转换为循环 |
| JavaScript(ES6) |
部分 |
ES6 规范要求严格模式下的 TCO |
仅 V8(Node.js)部分支持;Safari 支持;Chrome 和 Firefox 已移除或未完全实现 |
| C/C++ |
编译器可选 |
编译器优化(-O2/-O3) |
非强制,取决于编译器和优化级别;GCC/Clang 通常会在优化时进行 TCO |
| Java |
否 |
JVM 规范不要求 |
HotSpot JVM 不做 TCO,但可用 Lambda + 蹦床模拟 |
| Python |
否 |
CPython 明确不支持 |
Guido 的明确决策;可用蹦床、装饰器、或转迭代替代 |
| Go |
否 |
未实现 |
Go 的设计者认为循环更清晰,不鼓励深度递归 |
| Rust |
部分 |
LLVM 后端可能优化 |
不保证;官方推荐使用迭代或递归但控制深度 |
6.5 Python 中尾递归的替代方案
在 Python 中,面对深度递归的需求,有以下几种实践方案:
方案一:手动转换为迭代(首选)
最直接高效的方式。尾递归形式的函数都可以很自然地改写为 while 循环。这也是 Pythonic 的方式——"显式优于隐式"。
def factorial_iter(n):
acc = 1
while n > 0:
acc *= n
n -= 1
return acc
方案二:使用蹦床(Trampoline)
当必须保持递归风格时(如教学目的或复杂状态管理),可用蹦床将递归转移到堆上执行。
方案三:增大递归限制
sys.setrecursionlimit(10000) 可临时增大递归深度限制,但治标不治本——更大的深度意味着更大的栈空间消耗,最终仍有上限。
方案四:使用协程(Coroutine)
Python 的 async/await 或 yield 生成器可以模拟递归调用,将状态保存在堆上而非栈上。
def dfs_generator(root):
if not root:
return
yield root.val
yield from dfs_generator(root.left)
yield from dfs_generator(root.right)
七、递归的分治与减治策略
7.1 分治法(Divide and Conquer)
分治法将原问题分解为多个规模相近的独立子问题,分别求解后合并结果。分治法的递归模式为:T(n) = aT(n/b) + f(n),其中 a >= 2(多个子问题),b > 1(等规模划分)。
def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i]); i += 1
else:
result.append(right[j]); j += 1
result.extend(left[i:] or right[j:])
return result
分治法的典型应用:归并排序(O(n log n))、快速排序(O(n log n) 平均)、二分查找(O(log n))、最大子数组和(O(n log n))、最近点对(O(n log n))。
7.2 减治法(Decrease and Conquer)
减治法(也称递减法)每次递归只解决一个规模更小的子问题,且不需要合并(或合并代价极低)。减治法的递归模式为:T(n) = T(n-b) + f(n) 或 T(n) = T(n/b) + f(n)。
def binary_search(arr, target, low, high):
if low > high:
return -1
mid = (low + high) // 2
if arr[mid] == target:
return mid
elif arr[mid] > target:
return binary_search(arr, target, low, mid - 1)
else:
return binary_search(arr, target, mid + 1, high)
分治 vs 减治的对比:
| 维度 |
分治法 |
减治法 |
| 子问题数量 |
多个(通常 >= 2) |
一个 |
| 合并步骤 |
必需,通常代价较高 |
不需要或极简单 |
| 典型递推式 |
T(n) = 2T(n/2) + O(n) |
T(n) = T(n/2) + O(1) |
| 典型时间复杂度 |
O(n log n) 或更高 |
O(log n) 或 O(n) |
| 典型算法 |
归并排序、快速排序 |
二分查找、欧几里得算法 |
| 递归树形态 |
宽而浅 |
窄而深(单链) |
7.3 减治的三种变体
- 减常量: 每次递归将问题规模减少固定量,如 T(n) = T(n-1) + O(1)。典型为插入排序 O(n2)。
- 减常因子: 每次递归将问题规模缩小固定比例,如 T(n) = T(n/2) + O(1)。典型为二分查找 O(log n)。
- 减可变规模: 每次递归减少的量取决于输入数据,如快速排序的划分操作、欧几里得算法(辗转相除法)。
int gcd(int a, int b) {
if (b == 0) return a;
return gcd(b, a % b);
}
八、核心要点总结
- 递归三要素: 基线条件(终止递归)、递归条件(分解问题)、递归公式(定义关系)。三者缺一不可。
- 调用栈机制: 每次递归调用压入新栈帧,回归时弹出。栈深度决定空间复杂度。栈溢出发生在递归深度超过系统限制时。
- 经典递归问题: 阶乘(线性递归)、斐波那契(树形递归,需注意重复计算)、汉诺塔(双递归,指数复杂度)、二叉树遍历(天然递归结构)、全排列/组合(回溯模式:选择-递归-撤销)。
- 复杂度分析: 使用主定理分析递归时间复杂度。注意空间复杂度取决于栈最大深度,而非总调用次数。
- 递归转迭代: 通用方法为显式栈模拟。尾递归可直接转为循环。蹦床技巧可将递归转移到堆上执行。
- 尾递归优化: 递归调用必须是函数最后一个操作。编译器通过栈帧复用实现 O(1) 空间。CPython 明确不支持 TCO,原因是调试体验优先——"显式迭代优于隐式递归"。
- 分治 vs 减治: 分治将问题分解为多个子问题(如归并排序),需要合并步骤;减治每次只解决一个子问题(如二分查找),无需合并。
九、进一步思考
递归思维的工程应用
- 递归 vs 迭代的选择: 当问题天然具有递归结构(如树、图、分治算法)时,优先使用递归,代码清晰度高;当递归深度超过数百层时,必须考虑转换为迭代。
- 记忆化是递归的"加速器": 许多看似低效的递归算法,加上 lru_cache 或显式 memo 后,性能可与迭代匹敌。动态规划的本质就是"递归 + 记忆化"。
- 递归的调试技巧: 在递归函数入口打印缩进信息可以可视化调用过程;使用 trace 模块可记录调用序列;单元测试应覆盖基线条件和各种规模的递归路径。
- 函数式语言中的递归: 在 Scheme、Haskell 等函数式语言中,递归是主要的控制流机制(无循环语法),因此 TCO 是语言规范的必要组成部分。
- 实际项目中的建议: Python 项目中优先使用迭代;若必须递归,确保深度可控;对于树形数据,考虑使用 BFS 或队列避免栈溢出。
练习与挑战
- 用递归实现快速排序,并分析其最坏情况下的时间复杂度
- 将二叉树的中序遍历递归版本改写为迭代版本(尝试使用颜色标记法)
- 不使用 sys.setrecursionlimit,设计一个深度可达 10000 的阶乘函数
- 比较尾递归阶乘与普通递归阶乘在支持 TCO 的语言(如 Scala)中的性能差异
- 实现一个通用的递归转迭代工具函数(输入为递归函数和参数,输出为迭代版本)