递归解法
F(n) = n; n = 0,1
F(n) = F(n-1) + F(n-2),n >= 2;
斐波那契数列的推导式如上,相当简单,所以很容易就能写出递归的解法。
import time
from functools import wraps
def print_func_time(function):
@wraps(function)
def func_time(*args, **kwargs):
t0 = time.perf_counter()
result = function(*args, **kwargs)
t1 = time.perf_counter()
print("Total running time: %s s" % (str(t1 - t0)))
return result
return func_time
def fiBo(n):
if n <= 1:
return n
else:
return fiBo(n - 1) + fiBo(n - 2)
@print_func_time
def solution(n):
print(fiBo(n))
# 0 1 1 2 3 5 8
if __name__ == '__main__':
solution(40)
out:
102334155 Total running time: 41.485671385 s
显然,才 40,时间效率就很低下了。
这是为什么呢?
当我们计算 5 的时候,会进行下面这样的计算:
|--F(1)
|--F(2)|
|--F(3)| |--F(0)
| |
|--F(4)| |--F(1)
| |
| | |--F(1)
| |--F(2)|
| |--F(0)
F(5)|
| |--F(1)
| |--F(2)|
| | |--F(0)
|--F(3)|
|
|--F(1)
为了计算 fibo(5),需要计算 fibo(3),fibo(4);而为了计算 fibo(4),需要计算 fibo(2),fibo(3)……最终为了得到 fibo(5)的结果,fibo(0)被计算了 3 次,fibo(1)被计算了 5 次,fibo(2)被计算了 2 次。可以看到,它的计算次数几乎是指数级的
因此,虽然递归算法简洁,但是在这个问题中,它的时间复杂度却是难以接受的。除此之外,递归函数调用的越来越深,它们在不断入栈却迟迟不出栈,空间需求越来越大,虽然访问速度高,但大小是有限的,最终可能导致栈溢出。 在 linux 中,我们可以通过下面的命令查看栈空间的软限制:
$ ulimit -s
8192
可以看到,默认栈空间大小只有 8M。一般来说,8M 的栈空间对于一般程序完全足够。如果 8M 的栈空间不够使用,那么就需要重新审视你的代码设计了。
改进递归
既然我们知道最初版本的递归存在大量的重复计算,那么我们完全可以考虑将已经计算的值保存起来,从而避免重复计算,该版本代码实现如下:
def fiBo(n, lst):
if n < 2:
return n
else:
lst[n] = fiBo(n-1, lst) + lst[n-2]
return lst[n]
@print_func_time
def solution(n, lst):
print(fiBo(n, lst))
# 0 1 1 2 3 5 8
if __name__ == '__main__':
arr = [0]+[1]*40
solution(40, arr)
out:
102334155 Total running time: 2.6667000000035745e-05 s
显然可见其效率还是不错的,时间复杂度为 O(n)。
但是特别注意的是,这种改进版的递归,虽然避免了重复计算,但是调用链仍然比较长。
迭代解法
显然,让你来算,你肯定不会说我先算 40,然后发现要算 39。
你肯定是算:
0+1=1
1+1=2
2+1=3
3+5=5
5+3=8
……
我们是正向计算的,而且是不断向后计算的。
理解到区别了没。
那么,我们是否也可以这样进行计算呢,显然,是可以的。
def fiBo(n):
n = n+1
pre1 = 0
pre2 = 1
if n < 2:
return n
loop = 1
ret = 0
while loop < n:
ret = pre1 + pre2
pre2 = pre1
pre1 = ret
loop += 1
# 可以使用Python的语法糖代替上面的循环体
# tmp = pre1 + pre2
# ret, pre2, pre1, loop = tmp, pre1, tmp, loop + 1
return ret
@print_func_time
def solution(n):
print(fiBo(n))
# 0 1 1 2 3 5 8
if __name__ == '__main__':
solution(40)
out:
102334155
Total running time: 2.0102999999993543e-05 s
时间复杂度为 O(n)。
尾递归解法
同样的思路,但是采用尾递归的方法来计算。
要计算第 n 个斐波那契数,我们可以先计算第一个,第二个,如果未达到 n,则继续递归计算。
def fiBoProcess(n, pre1, pre2, begin):
if n == begin:
return pre1 + pre2
else:
begin += 1
return fiBoProcess(n, pre2, pre1 + pre2, begin)
def fiBo(n):
if n < 2:
return n
else:
return fiBoProcess(n, 0, 1, 2)
@print_func_time
def solution(n):
print(fiBo(n))
# 0 1 1 2 3 5 8
if __name__ == '__main__':
solution(40)
out:
102334155 Total running time: 2.338399999995966e-05 s
可见,其效率并不逊于迭代法。尾递归在函数返回之前的最后一个操作仍然是递归调用。尾递归的好处是,进入下一个函数之前,已经获得了当前函数的结果,因此不需要保留当前函数的环境,内存占用自然也是比最开始提到的递归要小。时间复杂度为 O(n)。