从斐波那契数列说起

​斐波那契(Fibonacci)数列是数学中一个著名的数列,有很多神奇的特性,在多个领域有广泛使用。定义如下数列为斐波那契数列:

[算法基础] 从斐波那契数列说起(一)_矩阵快速幂

该如何编写程序求解出斐波那契数列第n项呢?


一、递归法

根据上述公式,可以很容易用Python实现如下代码:

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

上述程序实现简单,且可读性强,只需要一行代码即可完成。这种解法是递归算法,将f(n)拆分为f(n-1)与f(n-2),在函数体内循环调用函数本身,直至达到终止条件f(0)与f(1)。我们以计算f(5)为例画图拆分求解过程:

[算法基础] 从斐波那契数列说起(一)_时间复杂度_02

用同一种颜色标注的方块为重复计算的内容。可以看到仅仅是计算f(5),就存在非常多的重复计算情况。我们也能发现,随着n越来越大,计算时间呈指数增长。读者可以尝试使用这种方法分别计算f(10)、f(20)、f(30)、f(40)、f(50)等,比较耗时增长的情况。实际上,该算法的时间复杂度为0(2n)级别,计算效率很低。

二、递推法

递归算法的计算过程是从后往前,一直计算到f(0)和f(1)。一种简单的办法是利用f(n)的递推公式,从前往后,这样避免了递归,用一个循环就可以直接得到结果,代码如下:

def fib_for(n):

result = [_ for _ in range(n + 1)]

for i in range(2, n + 1):

result[i] = result[i - 1] + result[i - 2]

return result[n]

这种办法存储一个长度为n+1的列表,由f(0)和f(1)计算得到f(2),再由f(1)和f(2) 计算得到f(3),依次类推,得到f(n)。这种方法时间复杂度为0(n)级别,会很快得到期望结果。但同时我们发现,这种方法需要占用一个n+1的列表。针对这一问题,我们可以进一步优化。

我们在计算中重复使用公式f(n)=f(n-1)+f(n-2),而该公式只占用三个变量,且最终我们只需要获得f(n)即可,不用保留中间值。因此我们可以不要存储列表,而采用滚动赋值的方法,做如下修改:​

def fib_foropt(n):
if n < 2:
return n
a, b = 0, 1
for _ in range(2, n + 1):
a, b = b, a + b
return b

这种方法将空间复杂度由0(n)降为0(1),大幅节省了内存占用。

三、矩阵快速幂法

​递推法的时间复杂度为0(n),矩阵快速幂法可以进一步降低时间复杂度至0(logn),当n>1时,由f(n)=f(n-1)+f(n-2),可以得:​

[算法基础] 从斐波那契数列说起(一)_矩阵快速幂_03

重复矩阵相乘,进一步得

[算法基础] 从斐波那契数列说起(一)_斐波那契数列_04

问题转化为计算[算法基础] 从斐波那契数列说起(一)_时间复杂度_05。很明显,如果[算法基础] 从斐波那契数列说起(一)_矩阵快速幂_06直接相乘n-1次可以得到结果,但是时间复杂度依旧是0(n)。使用快速幂法方法可以使时间复杂度降低为0(logn)。具体地,某个正整数a的n次幂an有如下性质:

[算法基础] 从斐波那契数列说起(一)_时间复杂度_07

我们将矩阵[算法基础] 从斐波那契数列说起(一)_时间复杂度_08套用以上公式,编写代码为:


  def multiply(a, b):
c = [[0, 0], [0, 0]]
for i in range(2):
for j in range(2):
c[i][j] = (a[i][0] * b[0][j] + a[i][1] * b[1][j])
return c

def fib_mat(n):
if n < 2:
return n

def matrix_pow(a, m):
if m == 0:
return 1
if m == 1:
return a

res = matrix_pow(a, m >> 1)
res = multiply(res, res)
if m & 1:
res = multiply(res, a)
return res

out = matrix_pow([[1, 1], [1, 0]], n - 1)
return out[0][0]

分析总结

对比以上三类解法,我们可以发现,同一个问题,可以有不同的算法解决。对于斐波那契数列问题,递归法实现容易,可读性好,后期易于维护,但是运行效率低。矩阵快速幂法运行效率高,占用内存小,但实现复杂,后期维护成本高。相对而言,递推法编程比较容易,代码清晰,运算速度快,更加适用于解决该问题。

不同的算法效率不同,占用内存不同,编写复杂程度不同,后期维护的难易程度也不同。因此,在我们日常工作中,需要有意识地考虑算法的性能,平衡编程、维护、效率等方面的关系,选择合适的算法。本系列后续文章会逐步介绍算法的相关基础知识,希望在归纳整理过程中,和读者共同成长。