第 19 课 · 进阶 · 20 分钟

Python 递归

把问题交给一个更小版本的自己。

基线条件递归调用调用树

推荐卡连成一条链:A 拉了 B,B 又拉了 C,后面还有。Maya 想知道每条链有多深,而不是一个个手数。递归在哪里停下来?

递归函数会用更小的输入调用自己。每个递归都需要一个能停止调用的基线条件,而且每一步都必须朝基线靠近。满足这两个条件时,递归可以把嵌套问题表达得更短。 你还会跟踪调用栈、摊平嵌套列表,并缓存重复分支。

先写基线条件

基线条件是不用再调用就能回答的最小输入。factorial(1) 会立刻返回 1。没有基线条件,函数会一直调用自己,直到 Python 抛出 RecursionError。

def factorial(n):
    if n <= 1:
        return 1
    return n * factorial(n - 1)

print(factorial(5))

一次递归调用

两次调用,更小的输入

斐波那契天然是两路递归:fib(n) 依赖 fib(n - 1) 和 fib(n - 2)。前两个值是基线条件。画出小 n 的调用树,就能看出这个版本为什么会重复计算。

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

print(fib(6))

分叉的递归

遍历嵌套列表

递归列表函数先处理第一个元素,再把剩下的列表交给更小的调用。空列表是基线条件。这个模式不只适用于平铺列表,也适用于树和嵌套结构。

def total(items):
    if not items:
        return 0
    return items[0] + total(items[1:])

print(total([1, 2, 3]))

不断缩短的列表

调用栈会反向展开

每次递归调用都会暂停它的调用者。基线条件先返回,然后每一层调用者恢复执行,组合返回值。向下和向上各打印一次,就能看清最终结果为什么是从里往外构建的。

def walk(n):
    if n == 0:
        print("base")
        return 0
    print("enter", n)
    result = walk(n - 1) + n
    print("leave", n, "result", result)
    return result

print("total", walk(3))

向下进入,向上组合

返回新结构,不要依赖全局变量

递归函数应该返回自己的部分结果。全局累加器会让多次调用互相干扰,也难以测试。摊平嵌套列表时,把第一个元素的结果和剩余部分的结果组合起来。

def flatten(items):
    if not items:
        return []
    first, *rest = items
    if isinstance(first, list):
        return flatten(first) + flatten(rest)
    return [first] + flatten(rest)

print(flatten([1, [2, [3, 4]], 5]))

组合返回值,而不是共享状态

缓存重复分支

朴素斐波那契会重复计算相同的子问题。lru_cache 按参数保存结果,对这种形状能把指数级工作变成线性级。只有纯函数、且参数可哈希时,缓存才是安全的。

from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(30))
print(fib.cache_info().hits > 0)

缓存纯函数的子问题

本课要点

  • 每个递归函数都需要基线条件。
  • 每次调用都必须使用更小或更简单的输入。
  • 递归调用返回的值由调用者继续组合。
  • 递归太深或重复太多时,循环可能更清楚。
  • 递归从内向外返回;记忆化能去掉重复分支。

常见错误

  • 忘记基线条件,导致 RecursionError。
  • 用同样的输入调用函数,而不是缩小输入。
  • 忽略分叉递归中的重复计算。
  • 缓存会读取变化中的全局状态或修改输入的函数。
轮到你

写一个递归函数,把列表中的数字相加,然后打印 [2, 4, 6] 的结果。

动手试试

改改变量、先预测输出,再运行代码。第一次点击“运行代码”时会下载 Python 运行时,可能需要几秒钟。

浏览器内 Python本地运行 · 无需账号
首次运行会从 CDN 下载 Pyodide。修改后点击“运行代码”。

练习本课

12 道题,作答后立即看到解释。

查看 12 道题答案开始练习
← lambda 与 map正则表达式 →