斐波那契数列的时间复杂度
# 斐波那契递归函数
def fib(n):
if n < 3:
return 1
return fib(n-2) +fib(n-1)
分析:
使用递归树的方式来计算斐波那契数列的时间复杂度
这里假设我们在递归的最后两层进行了n次操作
由斐波那契的递归树图我们可以看出,从下到上进行递归运算的时候,程序运行的次数是以2^x方式增加的。
我们来计算一下f(7)的运行次数
想要得到运行次数,首先我们要得到递归的深度,其中有2层为基本层
n = 7 - 2 = 5
计算运行次数
T(n) = 2^0 + 2^1 + 2^2 + 2^3 + n = 2^4 + n -1
如果递归的深度足够深的话,最后两个基数层的(n-1)次相比于2的指数级可以忽略不计,所以斐波那契的时间复杂度为
O(fib(n)) = O(2^n)
斐波那契数列的空间复杂度
空间复杂度主要是看内存的占用空间,在计算斐波那契数列时,会在内存中开辟多个内存空间用来进行运算,我们来分析一下它的空间占用情况。
def main():
fib(7)
if __name__ == "main":
main()
分析步骤:
- 执行main函数,main进栈并占据一块空间
- 执行fib(7),fib(7)=fib(6) + fib(5),无法直接得到fib(7)的值,故fib(7)会进栈并占用一块空间
- 接下来计算fib(6)=fib(5)+fib(4)和fib(5)=fib(4)+fib(3),均无法直接得到值,所以fib(6)和fib(5)进栈并分别占用一块空间
- 以此类推,直至递归的最底层fib(1)和fib(2)进栈
- 当fib(1)和fib(2)进栈,fib(1)=fib(2)=1,它们两个是由确切的值的,所以在它们进栈后,得到1后会立即被弹出,并且切换到它的上一个节点中,即f(3)中,此时计算出f(3)=f(1)+f(2)=2
- f(3)得到结果后,会被从栈中弹出,并且切换到f(3)的上一个节点中,即f(4)中,因为f(4)=f(3)+f(2),所以f(3)和f(2)会再次进栈,f(3)进栈后重复第5步过程后弹出,f(2)进栈后被赋值为1后弹出,此时f(4)=f(3)+f(2)=2+1=3,f(4)得到值后被弹出,切换到f(4)的上一个节点,即f(5)中
- f(5) = f(4)+f(3),此时,f(4)重复第6步过程得到值后被弹出,f(3)重复第5步过程得到值后被弹出,计算出f(5)的值并将f(5)弹出,同时切换到f(5)的上一个节点,即f(6)中
- 重复上述过程直至求出结果
总结:
在整个分析过程中,主要运用到了栈和上下文切换的概念,从分析过程中我们可以了解到,针对fib(7)函数,栈中最多创建了7个内存空间,所以,斐波那契数列的空间复杂度为O(n)。
动态规划(Dynamic Programing)
以上述的过程,我们在计算f(7)时,会计算好多次f(3),我们现在要考虑:在第一次计算完f(3)后,能不能将它保存起来,当再遇到f(3)后,能不能直接得到f(3)的值,而不是通过再将f(3)以f(3)=f(2)+f(1)的计算方式得到f(3)的值
我们将这种思维称作DP(Dynamic Programing)算法,它的核心思路是:能复用的尽量去复用,而不是去计算,通过创建一个数组,将已经计算好的填入进去,方便以后调用
斐波那契数列代码改进:
import numpy as np
def fib(n):
tmp = np.zeros(n)
tmp[0] = 1
tmp[1] = 1
for i in range(2,n):
tmpi = tmp[i-2] + tmp[i-1]
return tmp[n-1]
优点:它的时间复杂度为O(n),空间复杂度为O(n),时间复杂度得到了优化
缺点:浪费内存空间,当我们在计算f(7)时,只需要知道fib(6)和fib(5)的值即可,但此时fib(1)~fib(4)依然各占据着一份内存空间
改进思路:只让有关的函数占用内存,例如计算fib(7)时,我们只给fib(6)和fib(5)腾出内存空间来
改进代码:
def fib(n):
a,b = 1,1
c = 0
for i in range(2,n):
c = a + b
a = b
b = c
return c
由改进后的代码来看,空间中只保存了a,b,c三个变量,所以改进后的时间复杂度为O(n),而空间复杂度为O(1)
思考:
- 怎么在O(1)的时间复杂度下计算fib(n) (提示:通项公式,如下)