斐波那契数列
说到递归问题,大家应该都知道这个经典案例:斐波那契数列
0, 1, 1, 2, 3, 5, 8, 13, 21…
其递归式为 f ( n ) = f ( n − 2 ) + f ( n − 1 ) f(n) = f(n-2) + f(n-1) f(n)=f(n−2)+f(n−1)
python 实现
def fib1(n: int) -> int:
if n < 2: # base case
return n
return fib1(n - 2) + fib1(n - 1) # recursive case
看似问题已经解决了,但是作为一个优秀的程序员,你应该立刻想想:这玩意儿快不快?怎么优化?
我们来测试一下时间,python 代码计时
class Timer(object):
def __enter__(self):
self.t0 = time.time()
def __exit__(self, exc_type, exc_val, exc_tb):
print('[time spent: {time:.3f}s]'.format(time = time.time() - self.t0))
with Timer():
print(fib1(5)) # [time spent: 0.000s]
with Timer():
print(fib1(40)) # [time spent: 26.883s]
可见,当我们用上述递归代码计算斐波那契数列的第40个数,就需要半分钟了,这还得了。
问题的根源在与函数的调用次数是随着输入大小指数增长的!
而且其中有百分之九十九点酒的计算都是冗余的!
如何避免冗余计算?
把中间过程保留下来,以空间换时间!
空间换时间
from typing import Dict
memory: Dict[int, int] = {0: 0, 1: 1} # base cases
def fib2(n: int) -> int:
if n not in memory:
memory[n] = fib2(n - 1) + fib2(n - 2) # memoization
return memory[n]
再来看看时间效率:
with Timer():
print(fib2(500)) # [time spent: 0.000s]
保留了中间结果,计算量从 O ( 2 n ) O(2^n) O(2n) 立马变成了O(n),只不过以 O ( n ) O(n) O(n) 的空间为代价,何乐而不为呢!
lru_cache
Least Recently Used (LRU) 缓存,在 python 自带的 functools 库中,根据参数缓存每次函数调用结果,对于相同参数的,无需重新函数计算,直接返回之前缓存的返回值
from functools import lru_cache
@lru_cache(maxsize=None)
def fib3(n: int) -> int:
if n < 2: # base case
return n
return fib3(n - 2) + fib3(n - 1) # recursive case
with Timer():
print(fib3(500))
'''
139423224561697880139724382870407283950070256587697307264108962948325571622863290691557658876222521294125
[time spent: 0.000s]
'''
递归改迭代
def fib4(n: int):
yield 0 # special case
if n > 0: yield 1 # special case
last= 0 # initially set to fib(0)
next= 1 # initially set to fib(1)
for _ in range(1, n):
last, next = next, last + next
yield next # main generation step
if __name__ == "__main__":
for i in fib4(500):
print(i)
同样是秒速完成