Python中的递归,备忘录和可变默认参数

时间:2019-03-08 18:19:37

标签: python performance optimization fibonacci memoization

“基本”的含义不只是使用lru_cache。所有这些都是“足够快”的-我不是在寻找最快的算法-但是时间让我感到惊讶,所以我希望我能学到一些关于Python“工作”的方式。

简单循环(/尾递归):

def fibonacci(n):
    a, b = 0, 1
    if n in (a, b): return n
    for _ in range(n - 1):
        a, b = b, a + b
    return b

简单的记忆:

def fibonacci(n, memo={0:0, 1:1}):
    if len(memo) <= n:
        memo[n] = fibonacci(n - 1) + fibonacci(n - 2)
    return memo[n]

使用发电机:

def fib_seq():
    a, b = 0, 1
    yield a
    yield b
    while True:
        a, b = b, a + b
        yield b

def fibonacci(n):
    return next(x for (i, x) in enumerate(fib_seq()) if i == n)

我希望第一个简单易行,最快。不是。尽管有递归和许多函数调用,但第二个是迄今为止最快的。第三个很酷,并使用“现代”功能,但速度更慢,令人失望。 (我很想在某些方面将生成器视为记忆的替代方法-因为他们记住了它们的状态-并且由于它们是用C实现的,所以我希望它们会更快。)

典型结果:

loop: about 140 μs
memo: about 430 ns
genr: about 250 μs

那么任何人都可以特别说明为什么记忆比简单循环快一个数量级吗?

编辑:

现在知道我(像我之前的许多人一样)只是偶然发现了Python的可变默认参数。此行为说明了执行速度的实际和表面上的收益。

1 个答案:

答案 0 :(得分:1)

您所看到的是记忆的重点。第一次调用该函数时,memo缓存为空,因此必须递归。但是,下次使用相同或较低的参数调用它时,答案已经在缓存中,因此它会立即返回。如果您执行了数千个通话,那么您将摊销第一个通话的时间。这就是使备忘成为如此有用的优化的原因,而您只需第一次支付费用。

如果要查看刷新缓存需要多长时间,并且必须执行所有递归操作,则可以将初始缓存作为基准调用中的显式参数传递:

fibonacci(100, {0:0, 1:1})