Python反复折磨斐波那契序列

斐波那契序列(Fibonacci sequence)是一系列数字,其中除第 1 个和第 2 个数字之外,其他数字都是前两个数字之和: 0, 1, 1, 2, 3, 5, 8, 13, 21, …

这是一个典型的递归问题,递归问题有两个关键,一是递归式,二是基线条件。

就斐波那契函数而言,天然存在两个基线条件,第 1 个斐波那契数是 0。第 2 个斐波那契数是 1。后续任一斐波那契数 n 的值可用以下公式求得:fib(n) = fib(n − 1) + fib(n − 2)

def fib2(n: int) -> int:
    if n < 2:  # base case
        return n
    return fib2(n - 2) + fib2(n - 1)  # recursive case


if __name__ == "__main__":
    print(fib2(5))
    print(fib2(10))

不妨来数一下(如果加入几次打印函数调用即可看明白),仅为了计算第 4 个元素就需要调用 9 次 fib2()!情况会越来越糟糕,计算第 5 个元素需要调用 15 次,计算第 10 个元素需要调用 117 次,计算第 20 个元素需要调用 21891 次。我们应该能改善这种情况。

结果缓存(memoization)是一种缓存技术,即在每次计算任务完成后就把结果保存起来,这样在下次需要时即可直接检索出结果,而不需要一而再再而三地重复计算。

from typing import Dict
memo: Dict[int, int] = {0: 0, 1: 1}  # our base cases


def fib3(n: int) -> int:
    if n not in memo:
        memo[n] = fib3(n - 1) + fib3(n - 2)  # memoization
    return memo[n]


if __name__ == "__main__":
    print(fib3(5))
    print(fib3(50))

现在一次调用 fib3(20) 只会产生 39 次 fib3() 调用,而不会像调用 fib2(20) 那样产生 21891 次 fib2() 调用。memo 中预填了之前的基线条件 0 和 1,并加了一条 if 语句大幅降低了 fib3() 的计算复杂度。

还可以对 fib3() 做进一步的简化。Python 自带了一个内置的装饰器(decorator),可以自动为任何函数缓存结果。在 fib4() 中,装饰器 @functools.lru_cache() 所用的代码与 fib2() 中所用的代码完全相同。每次用新的参数执行 fib4() 时,该装饰器就会把返回值缓存起来。以后再用相同的参数调用 fib4() 时,都会从缓存中读取该参数对应的 fib4() 之前的返回值并返回。

from functools import lru_cache

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

if __name__ == "__main__":
    print(fib4(5))
    print(fib4(50))

还有一种性能更好的做法,即可以用老式的迭代法来解决斐波那契问题,如代码所示。

def fib5(n: int) -> int:
    if n == 0: return n 
    last: int = 0
    next: int = 1
    for _ in range(1, n):
        last, next = next, last + next
    return next

if __name__ == "__main__":
    print(fib5(5))
    print(fib5(50))

 以上方案中,for 循环体最多会运行 n-1 次。换句话说,这是效率最高的版本。为了计算第 20 个斐波那契数,这里的 for 循环体只运行了 19 次,而 fib2() 则需要 21891 次递归调用。对现实世界中的应用程序而言,这种强烈的反差将会造成巨大的差异!

递归解决方案是反向求解,而迭代解决方案则是正向求解。有时递归是最直观的问题解决方案。例如,fib1() 和 fib2() 的函数体几乎就是原始斐波那契公式的机械式转换。然而直观的递归解决方案也可能伴随着巨大的性能损耗。请记住,能用递归方式求解的问题也都能用迭代方式来求解。

到目前为止,已完成的这些函数都只能输出斐波那契序列中的单个值。如果要将到某个值之前的整个序列输出,又该怎么做呢?用 yield 语句很容易就能把 fib5() 转换为 Python 生成器。在对生成器进行迭代时,每轮迭代都会用 yield 语句从斐波那契序列中吐出一个值:

from typing import Generator

def fib6(n: int) -> Generator[int, None, None]:
    yield 0 
    if n > 0: yield 1  
    last: int = 0  
    next: int = 1  
    for _ in range(1, n):
        last, next = next, last + next
        yield next  # main generation step

if __name__ == "__main__":
    for i in fib6(50):
        print(i)

运行 fib6.py 将会打印出斐波那契序列的前 51 个数。for 循环 for i in fib6(50): 每一次迭代时,fib6() 都会一路运行至某条 yield 语句。如果直到函数的末尾也没遇到 yield 语句,循环就会结束迭代。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值