【问题标题】:Fibonacci sequence/number dynamic programming斐波那契数列/数动态规划
【发布时间】:2019-06-15 10:04:21
【问题描述】:

我正在努力提高我的编程逻辑技能,我正在观看videos 之一,了解如何处理斐波那契数列。

看了6:34的伪代码后,我写了这个:

In [14]: def my_fib(x, memo=dict()):
    ...:     if memo.get(x):
    ...:         return memo[x]
    ...:     if x == 1 or x == 2:
    ...:         result = 1
    ...:     else:
    ...:         result = my_fib(x - 1, memo) + my_fib(x -2, memo)
    ...:     memo[x] = result
    ...:     return result

这很好,但是当我观看视频到最后那个家伙斥责他的 python 代码时,我发现它与我的略有不同。

CS Dojo 代码:

In [68]: def fib_dyn_2(x, memo):     
    ...:     if memo[x] is not None:
    ...:         return memo[x]
    ...:     if x == 1 or x == 2:
    ...:         result = 1
    ...:     else:
    ...:         result = fib_dyn_2(x-1, memo) + fib_dyn_2(x-2, memo)
    ...:     memo[x] = result
    ...:     return result
    ...: 
    ...: def fib_memo(x):
    ...:     memo = [None] * (x + 1)
    ...:     return fib_dyn_2(x, memo)

我使用字典缓存他使用列表存在“轻微”差异。

让我感动的是我的代码似乎快了一点。当得到序列中的数字X >= 100 以及运行相同的数字时,序列不止一次。

即我的代码:

In [4]: %time my_fib(100)
CPU times: user 70 µs, sys: 44 µs, total: 114 µs
Wall time: 92 µs
Out[4]: 354224848179261915075L

CS Dojo 代码:

In [5]: %time fib_memo(100)
CPU times: user 99 µs, sys: 128 µs, total: 227 µs
Wall time: 187 µs
Out[5]: 354224848179261915075L

问题是哪个“更好”或更希望作为答案?

【问题讨论】:

  • 我认为您无法真正回答这个问题。这取决于。通常你只做这样的性能优化,如果你真的需要,因为它们使代码更复杂(即使在你的例子中只是一点点),这通常会降低代码的可读性和可维护性,因为主要逻辑可能“淹没”在技术代码中
  • 所以这种缓存一般来说是个好主意,如果性能是一个真正的问题,但可能视频的创建者跳过了这一步,只是介绍了函数式编程和真正的函数式编程语言的概念这样的一种兑现方式(我认为 haskell 就是这样做的)。
  • @jottbe 我假设您更喜欢基于可读性的CS Dojo 版本,对吗?
  • 啊,好吧,对不起,我被误导了。我认为这是关于函数式编程的,但似乎视频解决了性能问题。嗯,很奇怪,您的解决方案更快,因为像 dict 中的散列查找通常比基于索引的查找慢。顺便说一句,你是如何初始化备忘录的?因此,如果性能没有问题,我也不会使用 CS Dojo 创建的原始版本。
  • 我认为您的代码速度如此之快的原因是因为当您定义my_fib 时,dict() 会在那里创建,并且每次运行该函数时都已经存在。另一方面,fib_memo 每次调用该函数时都会创建一个新列表。如果您只计时 fib_dyn_2 而不是时间可能会更近。希望这可以帮助。 TLDR:您在参数列表中定义的字典会在定义函数后立即创建。

标签: python fibonacci


【解决方案1】:

虽然记忆版本的斐波那契数计算比简单的递归方法要好得多,但我鼓励您检查基于斐波那契数 Matrix Form 的解决方案:

https://stackoverflow.com/a/23462371/1570854

【讨论】:

    【解决方案2】:

    直观地说,基于列表的记忆应该比基于字典的速度略快。我发现调用的算法和顺序对结果有很大的影响,所以公平的比较需要小心(例如使用预分配与追加)

    我做了一些比较测试,似乎证实了这一点。您还可以通过算法中使用的操作/逻辑类型获得显着的性能变化。

    这是测试结果(100 次重复得到第 900 个斐波那契数):

    my_fib(N)     0.0578 Original
    fibo(N)       0.0089 Iterative algorithm
    simpleFibo(N) 0.0248 Single recursion algorithm
    dynaFibo(N)   0.0463 Double recursion with dictionary based memoization
    dynaFibo2(N)  0.0440 Double recursion with list based memoization
    binFibo(N)    0.0012 Iterative exponential algorithm
                         (this one responds in O(log(N)) time)
    

    下面是函数实现:

    def my_fib(x, memo=dict()):
         if memo.get(x):
             return memo[x]
         if x == 1 or x == 2:
             result = 1
         else:
             result = my_fib(x - 1, memo) + my_fib(x -2, memo)
         memo[x] = result
         return result
    
    def fibo(N):
        a = b = 1
        for _ in range(2,N): a,b = b,a+b
        return b
    
    def simpleFibo(N,a=0,b=1):
        if N < 3: return a+b
        return simpleFibo(N-1,b,a+b)
    
    def dynaFibo(N,memo={1:1,2:1}):
        if N not in memo:
            memo[N] = dynaFibo(N-1,memo) + dynaFibo(N-2,memo)
        return memo[N]
    
    def dynaFibo2(N,memo=None):
        if not memo:    memo = [0,1,1]+[0]*N
        if not memo[N]: memo[N] = dynaFibo2(N-1,memo) + dynaFibo2(N-2,memo)
        return memo[N]
    

    编辑添加了在 O(log(N)) 时间内响应的指数算法

    def binFibo(N):
        a,b   = 0,1
        f0,f1 = 1,1
        r,s   = (1,1) if N&1 else (0,1)
        N //=2
        while N > 0:
            a,b   = f0*a+f1*b, f0*b+f1*(a+b)
            f0,f1 = b-a,a
            if N&1: r,s = f0*r+f1*s, f0*s+f1*(r+s)
            N //= 2        
        return r
    

    以及测试程序

    from timeit import timeit
    count = 100
    
    N = 990
    
    t= timeit(lambda:my_fib(N,dict()), number=count) # providing dict() to avoid reuse between repetitions
    print("my_fib(N)",t)
    
    t= timeit(lambda:fibo(N), number=count)
    print("fibo(N)",t)
    
    t= timeit(lambda:simpleFibo(N), number=count) 
    print("simpleFibo(N)",t)
    
    t= timeit(lambda:dynaFibo(N,{1:1,2:1}), number=count) # providing dict() to avoid reuse between repetitions
    print("dynaFibo(N)",t) 
    
    t= timeit(lambda:dynaFibo2(N), number=count) 
    print("dynaFibo2(N)",t)
    
    t= timeit(lambda:binFibo(N), number=count) 
    print("binFibo(N)",t)
    

    顺便说一句,我假设您的目标是探索动态规划。否则,对斐波那契函数使用双重递归肯定是最糟糕的方法。

    【讨论】:

      【解决方案3】:

      我只是试图验证字典和列表版本之间是否存在显着的性能差异。看起来这两种方法之间只有很小的区别。顺便提一句。请注意,我还测量了缓存列表的创建。如果我比较“unix”时间命令打印的时间,我发现根本没有区别,但当然这也测量了操作系统需要加载 python 解释器的时间,因此不太可靠。

      from datetime import datetime
      def fib_cached(n, cache=None):
          if n <= 2:
              return 1
          if cache[n] is None:
              fib_n= fib_cached(n-1, cache) + fib_cached(n-2, cache)
              cache[n]= fib_n
          else:
              fib_n= cache[n]
          return fib_n
      
      
      n= 950
      
      before= datetime.now()
      print(fib_cached(n, cache=[None]*(n+1)))
      print(datetime.now() - before)
      

      【讨论】:

        【解决方案4】:

        经过一番试验,我发现您的代码变体在@AlainT. 的计时中击败了所有其他候选代码,甚至是 迭代 代码。有两个地方性能丢失。一、这个逻辑:

        if memo.get(x):
        

        比简单的慢:

        if x in memo:
        

        由于点击后,您最终会查找该值两次,而不是在下一行查找一次。但是,这里有一个更实质性的改进:

        result = my_fib(x - 1, memo) + my_fib(x - 2, memo)
        

        你已经默认了 memo 参数,你为什么要传递它?您可以通过以下方式显着加快速度:

        result = my_fib(x - 1) + my_fib(x - 2)
        

        我的返工功能:

        def my_fib(x, memo={1:1, 2:1}):
             if x in memo:
                 return memo[x]
        
             memo[x] = result = my_fib(x - 1) + my_fib(x - 2)
        
             return result
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2021-03-16
          • 2020-07-16
          • 1970-01-01
          • 2012-09-06
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-08-07
          相关资源
          最近更新 更多