【问题标题】:Unable to understand recursions and caching from Fibonacci code无法理解斐波那契代码中的递归和缓存
【发布时间】:2012-03-12 07:26:40
【问题描述】:

我正在尝试更好地理解递归和缓存,但我仍然取得了有趣的进展(有时我有点慢)。我在理解这段代码时遇到了一些小问题:

# Fibonacci recursive with result storage

class FibonacciStorage:
    _storage = { 0:1, 1:1 }
    @staticmethod
    def _fib(number):
        try: # is this already calculated
            return FibonacciStorage._storage[number]   #searches dict, and if value exists then return it
        except KeyError:
            result = FibonacciStorage._fib(number-1) + FibonacciStorage._fib(number-2)  #this is the actual Fibonacci Formula
            FibonacciStorage._storage[number] = result  #adds result to dictionary
            #print FibonacciStorage._storage #this shows the storage list growing with each iteration.
            return result
    @staticmethod
    def fib(number):  #first function, it has two asserts to basically make sure number is whole/positive and if its okay passes it to _fib(where the real work is done)
        # only do the assert statements once
        assert(isinstance(number,int)),"Needs a whole number" 
        assert(number>0),"Needs a positive whole number"
        return FibonacciStorage._fib(number)

# use a more readable name
getFib = FibonacciStorage.fib

print getFib(50)

我在网上找到了这段代码,并试图评论每一行以了解它实际上在做什么。我不明白这是如何递归的,它会循环遍历代码,直到给出正确的结果,但我看不出它是如何调用自身的。

起初我以为是这一行:result = FibonacciStorage._fib(number-1) + FibonacciStorage._fib(number-2),但我很困惑,因为我的print FibonacciStorage._storage 行显示存储缓存不断增长,但它下面的行是一个返回函数。我怀疑我不明白return result 实际上是如何再次触发递归的。

我也不确定存储字典是如何让这个程序运行得如此之快的。我对 fib 序列的简单递归需要很长时间(即使我保存了数据),但在这种情况下,如果你立即执行 print getFib(200)。缓存是如何让它这么快的?

所以总结一下,两个问题:

1) 递归实际上是如何被触发的?如果每个循环都访问我的打印语句?

2) 与其他 fib 序列相比,缓存如何加快速度?类结构或@staticmethod 有影响吗?例如,这似乎是即时的,而 http://en.literateprograms.org/Fibonacci_numbers_(Python) 上的那些有轻微的延迟。

3) 也可能出于兴趣,fibnocci 类型的算法规模(拆分并并行运行)是否只是出于兴趣,还是仅限于一个过程?

任何见解都会有所帮助。

【问题讨论】:

  • 理解递归是一个很好的尝试。斐波那契数列的定义是递归的,但递归实现是一种反模式,因为它非常浪费。考虑fib(7) 将导致24 次调用,并将独立计算fib(3) 的值五次 次。事实上,fib(n) 的调用总数是2 * fib(n) - 1。计算fib(20) (6765) 需要 13,529 次调用。
  • 如果您使用的是 Python 2.x,请始终从 object 继承您的类 - 否则您会收到一些细微的错误(例如发现“​​staticmethod”在您的类中不起作用,或其他)
  • @JimGarrison 如果像上面这样缓存,递归会不会那么糟糕?我试图理解它,但据我所知,到目前为止它并没有重复它的任何工作,所以 fib(20) 不会导致只有超过 20 个循环,因为它们中的大多数都是针对缓存的查找?
  • 是的,缓存结果会有所改善。但是您仍然需要拨打2*fib(n)-1 电话。一个简单的迭代解决方案是唯一的方法。这只是一个警示故事,请记住对于一些递归问题,线性解决方案更好。
  • 你的理解已经超出了基础,但你可能会觉得my answer to a similar question很有趣。

标签: python algorithm


【解决方案1】:

(1)递归调用在这一行:

result = FibonacciStorage._fib(number-1) + FibonacciStorage._fib(number-2)
//                   here ^                        and here ^

你是对的,这是“实际的斐波那契公式”,根据定义是递归的。

(2) 缓存极大地加速了它。不同之处在于持久对象_storage,它为您节省了壮观 数量的重复工作。考虑:

                                 fib(4)
                               /        \
                          fib(3)   +     fib(2)
                         /    \          /     \
                    fib(2) + fib(1)   fib(1) + fib(0)
                    /    \
               fib(1) + fib(0)

这只是fib(4),你已经可以看到冗余了。通过简单地缓存 fib(3)fib(4) 的整数值,您可以将 fib(5) 所需的计算次数从 7 减少到 1。而且节省的费用只会随着您的增加而增加。

【讨论】:

  • 但如果是这样的话,不应该只是去递归吗?而不是在下面做我的打印声明?如果您取消注释它,您会看到它每时每刻都在增长。
  • 我有点了解它在做什么,我有兴趣了解它在幕后的编程方面..例如,我知道缓存使它更快,但我不明白为什么这种缓存比这个网站上的更快:en.literateprograms.org/Fibonacci_numbers_(Python)
  • @learningJava 当递归调用fibreturns 时,它将在发出调用的fib 中的中断处继续。
【解决方案2】:

问题中的斐波那契实现使用称为memoization 的技术来存储已计算的值并在每次需要时有效地检索它们,而不是重新计算它们。这具有巨大的性能优势,因为值只需要计算一次。

关于您的问题:

  1. 递归在这一行被触发:FibonacciStorage._fib(number-1) + FibonacciStorage._fib(number-2),你可以看到_fib过程正在调用自己
  2. 缓存极大地提高了计算速度,请参阅example 以获得有关其工作原理的完整说明。在这种情况下,使用静态方法并不是特别有用,它只会让实现更加做作
  3. 递归的非记忆版本可能会在并行算法中拆分,其中对斐波那契的每次调用都在不同的线程上执行。但是开销会很大,算法复杂度根本不会提高。坚持使用记忆版本,或 read 了解更快的斐波那契计算方法。

【讨论】:

  • 谢谢 Oscar 从来没有听说过 memoization 但看起来像我想学习的东西..谢谢,但是关于 fib 递归,我认为这也是调用它,但它不应该立即去从顶部开始?但是如果你取消注释我的 print 语句,你会看到它的值在增长,这让我认为 print 语句是在每个循环中执行的。如果是这样,那么 FibonacciStorage._fib(number-1) 是如何实际调用它的?如果是的话,我的 print 语句不会永远不会被执行,因为它会构建字典缓存,直到 try 语句起作用
  • 一旦 try 语句工作,它只会返回结果并完成...... print 语句是如何工作的?如果立即调用 FibonacciStorage._fib(number-1)?还是有延迟(在递归之前是否等待函数结束)?对不起,这就是我困惑的地方..
  • 只要值不在缓存中,就在添加后立即调用 print 语句。我认为您应该跟踪程序的执行以更好地理解它,使用调试器。
  • 好的..以前从未这样做过,但我会查找追踪..谢谢 Oscar。
【解决方案3】:

很高兴知道 Python dicts 有一个 __missing__ 方法,可以为缺少的键调用:

class FibonacciStorage(dict):
    def __missing__(self, n):
        self[n] = self[n - 1] + self[n - 2]
        return self[n]

用法:

fib = FibonacciStorage({0: 1, 1: 1})
print(fib[50]) #20365011074

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-08-01
    • 2010-12-03
    • 2016-06-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-04-02
    相关资源
    最近更新 更多