• 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)

    1. def fib2(n: int) -> int:
    2. if n < 2: # base case
    3. return n
    4. return fib2(n - 2) + fib2(n - 1) # recursive case
    5. if __name__ == "__main__":
    6. print(fib2(5))
    7. print(fib2(10))

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

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

    1. from typing import Dict
    2. memo: Dict[int, int] = {0: 0, 1: 1} # our base cases
    3. def fib3(n: int) -> int:
    4. if n not in memo:
    5. memo[n] = fib3(n - 1) + fib3(n - 2) # memoization
    6. return memo[n]
    7. if __name__ == "__main__":
    8. print(fib3(5))
    9. 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() 之前的返回值并返回。

    1. from functools import lru_cache
    2. @lru_cache(maxsize=None)
    3. def fib4(n: int) -> int:
    4. if n < 2:
    5. return n
    6. return fib4(n - 2) + fib4(n - 1)
    7. if __name__ == "__main__":
    8. print(fib4(5))
    9. print(fib4(50))

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

    1. def fib5(n: int) -> int:
    2. if n == 0: return n
    3. last: int = 0
    4. next: int = 1
    5. for _ in range(1, n):
    6. last, next = next, last + next
    7. return next
    8. if __name__ == "__main__":
    9. print(fib5(5))
    10. print(fib5(50))

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

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

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

    1. from typing import Generator
    2. def fib6(n: int) -> Generator[int, None, None]:
    3. yield 0
    4. if n > 0: yield 1
    5. last: int = 0
    6. next: int = 1
    7. for _ in range(1, n):
    8. last, next = next, last + next
    9. yield next # main generation step
    10. if __name__ == "__main__":
    11. for i in fib6(50):
    12. print(i)

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

  • 相关阅读:
    程序员不写注释:探讨与反思
    Linux编程之线程池的设计与实现
    CAP:多重注意力机制,有趣的细粒度分类方案 | AAAI 2021
    《从0开始写一个微内核操作系统》0-环境准备
    Simlab python二次开发1-将所有缸套内表面半径加大1mm
    基于SSM的美食网站设计设计与实现-计算机毕业设计源码+LW文档
    课本在servlet中检索参数并验证,最后向用户发送验证消息
    Linux shell
    React 入门:实战案例 TodoList 添加一条 Todo 到列表
    详解Python中哈希表的使用。站在开发者角度,与大家一起探究哈希的世界。
  • 原文地址:https://blog.csdn.net/harleyrecsys/article/details/125968196