python leetcode 斐波那契数列 动态规划 递归 算法

题目链接

https://leetcode-cn.com/problems/fibonacci-number/

斐波那契数,通常用F(n)表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0F(1) = 1 F(n) = F(n - 1) + F(n - 2),其中 n > 1 给你 n ,请计算 F(n) 。

示例 1:

输入:2 输出:1

解释:F(2) = F(1) + F(0) = 1 + 0 = 1


纯递归


递归有两个基本要素:基例以及递归关系式。

  • 基例:F(0) = 0,F(1) = 1,即递归结束的地方
  • 递归关系式:F(n) = F(n-1) + F(n- 2)

def fib(n):
   #base case
   if n <= 1:
      return n
   elif n >= 2:
      return fib(n-1) + fib(n-2)

然后根据python语法特性,可以缩写为一句话

def fib(n):
  return fib(n-1) + fib(n-2) if n >= 1 else n

优化


F(20) = F(19)+F(18)

F(19) = F(18)+F(17)

然后我们发现 F(18) 重复,即冗余结构。这样的运算时间会很大,

节点数 * 每个子问题所需要的时间 = 时间复杂度

显而易见,这种情况下为O(2^N)

纯递归的缺点就在于存在过多冗余结构。


递归加记忆


将指数级时间将为多项式级时间。

我们可以将已经出现计算好的存在一个备忘录里面,这样我们下一次计算之前,先去查一遍表就行,不需要再重新计算。

memo = {}
def fib(n):
    #查表
    if n in memo:
       return memo[n]
    #base case:
    elif n <= 1:
       return n
    else:
       ans = fib(n-1) + fib(n-2)
       memo[n] = ans
       return memo[n]

解释


当然,上述的备忘录既可以采用数组,同时也可以用哈希表。

另外,如果追求完美一点,可以将基例存储到备忘录里面,这样可以减少一定程度的空间复杂度。

memo = {0:0,1:1}

这里介绍的是由上而下的方法,当然也可以是由下而上的。虽然比纯递归好,但是时间复杂度依然很高,leetcode上为1500-1600ms左右。


bottem-up-solution


def fib(n):
    memo = {0:0,1:1}
    for i in range(2,n+1):
        memo[i] = memo[i-1] + memo[i-2]
    return memo[n]

解释


for循环的左边是因为备忘录里面已经有键0,1了,所以从2开始。然后我们最后要计算的是memo[n],i = n 所以要为n+1

优化


这个的时间复杂度会大大降低,两者看似都是查表,但是由下而上的这种不会重新迭代函数,只会不断计算表。leetcode数据为36ms

算法方面的优化到此为止,但是关于空间复杂度可以更优化一点,

import sys

h = {0:0,1:1}
l = [0,1]

print(sys.getsizeof(h))
print(sys.getsizeof(l))

#232
#72

字典的空间复杂度远大于列表,所以可以更优化一点。单方面来讲,复杂度跟元素大小无关。跟元素个数有关。

def fib(n):
    if n == 0:
       return 0
    memo[1 for i in range(n+1)]
    memo[0],memo[1] = 0,1

    for i in range(2,n+1):
        memo[i] = memo[i-1] + memo[i-2]
    return memo[n]

解释


关于第一行生成len = n+1的原因,因为最后返回索引为n,而索引从0开始

而且除了列表生成器的表达,还可以 memo = [1] * (n+1)

之所以提取生成长度为n+1的列表,是为了防止列表索引不够,进行有序的替换。

因为是替换的,所以初始化列表的值是任取的,只要保证基例正确就行。

要小心的是,如果输入0没有开头的if语句是会报错的,原因在于生成了长度为1的列表,却有memo[1] = 1这种操作,然而,列表并不像字典一样可以创建,则导致长度不够用报错

关于for 循环

for i in range(2,1)
for i in range(2,2)
#两者均不会报错且都不执行

经验之谈


还有就是力扣的很多时候判别的不准,大家只要往死里优化,看看几个最优解就行。不必苛求100%,大家可以复制几个100%,有些是运气100%,到你这就不是100%

拓展


滚动变量

def fib(n):
    if n == 0:
       return 0
    for i in range(n):
        a,b = b,a+b
    return a

这种方法显然不是最优解,但是值得一说的是,按照一些书中python的代码规范。这种结构可读性不好,建议少用。

还有一个更流氓一点的解法:

面对答案编程

class Solution:
    def fib(self, n: int) -> int:
        a = [0,1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040]
        return a[n]

细心的同学注意到n的范围为[0,30],所以生成一个长度为31的列表就行。

上一篇:计算斐波那契数列


下一篇:重学c语言8