递归优化策略——别让程序“卡住”
较难3递归优化两步走:记忆化与迭代,别让程序“卡住”
递归是一种很“聪明”的编程方式——它让代码自己调用自己,把大问题拆成小问题。比如算台阶数、分糖果、倒着数数,用递归写起来特别清晰。但是递归有两个“小毛病”:重复计算和栈溢出。
- 重复计算:同一个子问题被反复算很多遍,浪费时间。比如算一个数,可能算了成百上千次相同的计算,就像你每天重复做一样的作业,效率很低。
- 栈溢出:递归调用一层套一层,如果层数太多(比如超过1000层),电脑内存会“装满”,程序会崩溃报错。
这一节,我们就来学两种常用的优化方法,让递归跑得又快又稳,不再“卡住”。
问题:斐波那契数列的重复计算
先看一个经典例子——斐波那契数列。数列是这样的:0, 1, 1, 2, 3, 5, 8, 13……每个数等于前两个数之和。用递归写起来很简单:
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
试试算 fib(40),运行一下,你会发现它慢得像蜗牛爬,甚至会等上好几秒。这到底是为什么?
因为递归像一棵树,每次分裂成两个子问题。算 fib(5) 时,fib(3) 被算了两次,fib(2) 被算了五次……数字越大,重复的次数像山一样堆起来。比如 fib(40) 总共需要算超过3亿次,当然慢了。
生活中的例子:假如你要统计全班同学零花钱的总和,但你不直接加,而是分两组:先算第1到第10个同学的和,再算第11到第20个同学的和。分完组后,你又把每个组再分……结果同一个同学被重复统计了好几次,浪费了大量时间。递归的重复计算也是这个道理。
优化1:记忆化(Memoization)——抄作业的小本本
既然算过的结果会被反复用到,那我们就像抄作业一样,把算过的结果记在“小本本”上。下次再需要时,直接翻本子查,不用重新算。这个“小本本”可以是字典,也可以直接用 Python 自带的装饰器 @lru_cache。
方法1:手动用字典缓存
cache = {} # 缓存字典,记录已经算过的结果
def fib_memo(n):
if n in cache: # 如果结果已经在缓存中
return cache[n] # 直接返回,不用再算
if n <= 1:
result = n
else:
result = fib_memo(n-1) + fib_memo(n-2)
cache[n] = result # 把新结果记到小本本上
return result
print(fib_memo(100)) # 瞬间算出,结果是354224848179261915075
手动写字典的好处是你能清楚看到缓存怎么工作,但每次都要写检查代码。更省事的方法是使用 Python 标准库里的装饰器。
方法2:用 @lru_cache 装饰器(推荐)
lru_cache 是 functools 模块里的一个工具,它会自动帮我们把结果缓存起来,用法就像给函数贴个标签。
from functools import lru_cache
@lru_cache(maxsize=None) # maxsize=None 表示缓存不设上限
def fib_fast(n):
if n <= 1:
return n
return fib_fast(n-1) + fib_fast(n-2)
print(fib_fast(100)) # 瞬间输出结果
加上 @lru_cache 后,每个 n 只计算一次,后面的调用直接读缓存。运行速度从好几秒变成了瞬间。就像你有一本随时自动更新的“答案本”,再也不用重复计算。
生活中的例子:老师让你每天抄写100个单词。第一天你老老实实全写一遍;第二天老师又让抄一样的100个单词,如果你已经有昨天的本子,直接复制过去,是不是快得多?记忆化就是那个“本子”。
优化2:改成迭代(循环)——扔掉递归包袱
递归虽然好写,但每调用一次函数,电脑都要准备一块临时空间放参数、变量。层数一多,这些临时空间堆叠起来,就会占满内存,导致“栈溢出”。另一种解决方法是把递归改成循环。循环没有函数调用开销,也不会占据栈空间,特别适合像斐波那契这样可以顺次计算的问题。
斐波那契的迭代版本:
def fib_iterative(n):
a, b = 0, 1 # a是第0项,b是第1项
for _ in range(n): # 循环n次
a, b = b, a + b # 每次往后推一项:新的a变成原来的b,新的b变成a+b
return a # 循环结束后a就是第n项
print(fib_iterative(100)) # 结果和上面一样
优点:
- 没有重复计算,不会栈溢出。
- 运行速度甚至比记忆化递归更快,因为少了函数调用的额外成本。
生活中的例子:排队买冰淇淋,队伍很长(相当于递归层数深)。如果每个人都要拍前面人的肩膀让他传话,容易出错又慢(递归)。但如果你从头开始一个个数过去,直接走到队尾(迭代),就简单多了。迭代就是这种“一步步往前走”的方法。
常见错误与注意事项
-
忘记设置递归深度限制
Python 默认递归深度是1000层。如果你试图用递归算一个深度为2000的问题(比如某些复杂的树结构),程序会报错:RecursionError: maximum recursion depth exceeded。虽然可以用sys.setrecursionlimit(10000)调整深度,但调太高容易导致程序崩溃(内存耗尽),所以能用迭代就用迭代。 -
记忆化但忘了正确存储
新手可能写成:def fib_bad(n): cache = {} # 每次调用都新建一个空字典!缓存永远没存住 if n in cache: ...这样缓存每次递归都会清空,完全没效果。正确做法:把
cache定义在函数外部,或者作为全局变量,或者用装饰器自动管理。 -
误以为记忆化可以解决一切递归问题
记忆化只解决“重复子问题”的情况。如果递归像“汉诺塔”那样没有重复,记忆化就不会提速。另外,记忆化仍然占用额外内存(缓存),如果 n 非常大(比如 10^7),缓存可能太大,导致内存不足。 -
迭代时变量顺序写错
在循环里,我们写a, b = b, a + b。如果写成a = b; b = a + b,那么 a 被修改后,b 用的 a 已经是新的值了,结果全错。Python 的元组赋值可以同时更新,避免了这个错误。
完整可运行示例
下面把三种方法(原始递归、记忆化递归、迭代)放在一起,并分别测试 n=35 的运行时间,让你直观感受差别。
import time
from functools import lru_cache
# 原始递归(很慢)
def fib_raw(n):
if n <= 1:
return n
return fib_raw(n-1) + fib_raw(n-2)
# 记忆化递归(带缓存)
@lru_cache(maxsize=None)
def fib_fast(n):
if n <= 1:
return n
return fib_fast(n-1) + fib_fast(n-2)
# 迭代版(循环)
def fib_iterative(n):
a, b = 0, 1 # a是第0项,b是第1项
for _ in range(n):
a, b = b, a + b # 每次往后推一步
return a
# 测试 n=35,观察时间
n = 35
start = time.time()
result1 = fib_raw(n)
time1 = time.time() - start
print(f"原始递归:fib({n}) = {result1},耗时 {time1:.3f} 秒")
start = time.time()
result2 = fib_fast(n)
time2 = time.time() - start
print(f"记忆化递归:fib({n}) = {result2},耗时 {time2:.6f} 秒")
start = time.time()
result3 = fib_iterative(n)
time3 = time.time() - start
print(f"迭代版:fib({n}) = {result3},耗时 {time3:.6f} 秒")
运行结果(大概):
- 原始递归:几秒甚至十几秒(n再大点电脑会卡住)
- 记忆化递归:少于 0.001 秒
- 迭代版:也少于 0.001 秒
两种优化方法都能让你瞬间得到答案,再也不用等得着急。
学完这些,你还可以看看
- 动态规划(DP):记忆化递归实际上是动态规划的一种实现方式(自顶向下)。如果你想更系统地学习解这类问题,可以看看“背包问题”“最短路径”等。
- 尾递归:在某些语言里,尾递归可以被自动优化成循环(Python 不支持)。但你可以了解这个概念,帮助你理解递归的结构。
- 递归树分析:学会画递归树,能帮你直观看出重复计算的位置,也能帮你决定是否需要优化。
记住:递归是思维的“放大镜”,让我们看清问题的结构;而优化是程序员的“小扳手”,让代码跑得又快又稳。试着用今天学到的方法,去优化你以前写的递归小项目吧!
例题精讲
对于计算斐波那契数列第n项(n较大),直接使用朴素递归(不优化)与使用记忆化递归相比,时间复杂度分别是多少?
将递归算法改写成迭代算法一定能解决栈溢出问题,且运行效率一定更高。
下面是一个使用记忆化递归计算斐波那契数列的函数,请补全代码。
def fib(n, memo):
if n in memo:
return memo[n]
if n <= 1:
return n
memo[n] = ___
return memo[n]以下哪种策略不属于递归优化的常用方法?
下面的递归函数计算阶乘,请将其改写成迭代版本。
递归版本:
def factorial(n):
if n == 0:
return 1
return n * factorial(n-1)
迭代版本:
def factorial_iter(n):
result = 1
for i in range(___, n+1):
result *= i
return result