什么是斐波那契数列?
斐波那契数列(Fibonacci sequence)是以意大利数学家列昂纳多·斐波那契
的名字命名的数列。该数列具有一些很好的性质,比如在
斐波那契数列的通项公式:
通项公式
第
其中
证明
显然
满足通项公式。
假设对整数
那么
满足通项公式。
其中,由于
证毕。
利用上面证明的通项公式,可以得出斐波那契数列相邻两项之间有如下近似关系
因为
所以
发散思维
将上面给出的等式稍作修改
即
或
这个等式是否具有普遍性?即若以
表示某一整数数列
我下面将给出一个证明,这个证明通过给出
证明:
显然为整数。 当
这表明
这就证明了我们的猜想。
从证明的结果,
的递推公式应该是
当m等于5时,
斐波那契数的计算
约定
从前文的分析我们知道,
这会导致计算
为简单起见,后文关于时间复杂度的分析将基于下面这个不合理的约定:
不论整数长度,一律将加法和乘法的时间复杂度规定为。
朴素算法
树递归
将斐波那契数列的递推公式直接转换成Python代码如下
def fib0(n):
if n < 2:
return n
else:
return fib0(n-1) + fib0(n-2)
这个函数的效率极低,一秒内能够计算出前35项就很不错了。因为采用了树递归,时间复杂度为指数量级。简单画一下代码流程图,就可以看出这里面有着众多的重复计算。
为了计算该函数的时间复杂度,我们先来计算一下递归树中的节点个数。
设
下面以
其中求和部分
运用数学归纳法很容易证明上式,下面展示一种计算证明的过程。
令
则
左右累加求和
等式两边同时减掉
即
令
与实际相符,验证了此公式的正确性。
因此,基于前文的约定
动态规划
简单修改一下上面的代码,将计算过程记忆化(动态规划),得到下面的代码,时间复杂度为
def fib1(n):
def fibrec(n):
if n < 2:
return n
if v[n] is None:
v[n] = fibrec(n - 1) + fibrec(n - 2)
return v[n]
v = [None for i in range(n + 1)]
return fibrec(n)
随便打印前100项不是什么问题,但计算前1千项会爆栈。
for i in range(100):
print("fib1(%3d): %d" % (i, fib1(i)))
尾递归
下面试着将代码转换成尾递归的形式,方法是将中间结果作为函数参数进行传递
def fib2(n):
def f(u, v, i):
if i == 0:
return u
return f(v, u + v, i - 1)
return f(0, 1, n)
这个版本与上面动态规划的版本一样,时间复杂度都是
消除递归
上面都采用了自顶向下的递归方法,所以写出来的函数效率很低。考察上面图中的递归树,可以发现,只要我们从左下角的
def fib3(n):
v = [0, 1]
for i in range(2, n + 1):
v.append(v[i - 1] + v[i - 2])
return v[n]
如果要计算斐波那契数列的前
def fib4(n):
u, v = 0, 1
for i in range(n):
u, v = v, v + u
return u
快速算法
矩阵快速幂算法
矩阵快速幂算法计算斐波那契数的原理很简单,只需了解快速幂算法的原理和矩阵乘法即可。若不懂快速幂算法,可以参考我的博文快速幂算法
算法原理
当
然后就可以用快速幂算法来加速运算了。
代码实现
def fib_fast_expt(n):
def mul22(x, y):
return [
[ x[0][0] * y[0][0] + x[0][1] * y[1][0],
x[0][0] * y[1][0] + x[0][1] * y[1][1]
],
[
x[1][0] * y[0][0] + x[1][1] * y[1][0],
x[1][0] * y[1][0] + x[1][1] * y[1][1]
]
]
def fast_expt(x, n):
if n == 0:
return [[1, 0], [0, 1]]
m = fast_expt(x, n >> 1)
y = mul22(m, m)
if (n & 0x1) == 1:
y = mul22(y, x)
return y
if n > 1:
return fast_expt([[1, 1], [1, 0] ], n - 1)[0][0]
return n
采用了快速幂算法加速的矩阵快速幂算法,时间复杂度显然是
更快的算法
我是自己无意间得出该这个公式之后才进一步从别人那儿了解到矩阵快速幂算法的,这个算法要比矩阵快速幂算法快几倍。因为公式宽度的限制,下面以
在
不难发现
看到这个公式,你大概就知道该从哪儿着手计算过程的优化了。
这里可以分为两种情况进行讨论。 1.
2.
归纳为一个公式如下:
乍一看,这个算法的时间复杂度应该是
但我们应该清楚,之所以这样,是因为我们像普通算法中最普通的那种递归算法那样遍历了整棵递归树,因此这里面有着很多的重复计算
运行下面的代码
isodd = lambda x: bool(x & 0x1)
def fib_record(n):
def fib(n):
if n not in numbers:
numbers[n] = 0
numbers[n] += 1
if n < 2:
return n
x = fib((n >> 1) - 1)
y = fib(n >> 1)
if isodd(n):
x += y
return x * x + y * y
else:
return y * (y + 2 * x)
numbers = {}
return (fib(n), numbers)
for i in sorted(r[1]):
print("%3d: %d" % (i , r[1][i]))
可以看到,其中有许多重复计算。因此,我们完全可以运用动态规划的思想来优化算法。当然,即使不进行优化,这个公式也会比一般的
根据动态规划的思想,我们要么记录子问题的解,要么自底向上进行求解。
记录子问题解的方法较为简单,下面是代码
def fast_fib_memory1(n):
def dp(F, n):
if n not in F:
k = n >> 1
dp(F, k - 1)
dp(F, k)
if isodd(n):
F[k + 1] = F[k] + F[k - 1]
F[n] = F[k]**2 + F[k + 1]**2
else:
F[n] = F[k] * (F[k] + 2 * F[k - 1])
F = {0: 0, 1: 1}
dp(F, n)
return F[n]
记忆化该算法后,其时间复杂度与矩阵快速幂算法一致,都是
另外,我们完全可以用Python
的lru_cache
自动实现记忆化,就效率和便捷性而言,非常好。
from functools import lru_cache
@lru_cache(maxsize=128)
def fast_fib_lur_cache(n):
if n < 2:
return n
x = fast_fib_lur_cache((n >> 1) - 1)
y = fast_fib_lur_cache(n >> 1)
if isodd(n):
x += y
return x * x + y * y
else:
return y * (y + 2 * x)
下面是一个自底向上的版本,代码不是很容易理解,不过从中很容易看出这个算法的高效性——计算次数正比于输入的二进制长度。理解这段代码的关键依然是二进制,你可以从计算
def fast_fib_bottom_up2(n):
x, y, l = 1, 0, n.bit_length()
for i in range(l - 1, 0, -1):
if isodd(n >> i):
x, y = y * (y + 2 * x), (x * x + y * y)
y += x
else:
y, x = y * (y + 2 * x), (x * x + y * y)
if isodd(n):
x += y
return x * x + y * y
return y * (y + 2 * x)
下面是另一个自底向上的版本,可以从中发现许多对称性
def fast_fib_bottom_up1(n):
x, y, l = 1, 0, n.bit_length()
for i in range(l - 1, 0, -1):
u = x**2
v = y**2
z = (x + y)**2
if isodd(n>>i):
x = z - u
y = z + v
else:
x = u + v
y = z - u
z = (x + y)**2
if isodd(n):
return z + y**2
else:
return z - x**2
最后,附一份完整代码和测试截图。fibonacci.py
2019/12/30 更新。 好久没登知乎了,今天登录知乎,发现有大佬评论我的斐波那契数快速计算的文章。跟着评论中的链接,读了两位大佬的斐波那契数快速算法的文章,了解到了Cassini
公式
比如,
这个公式很简单,证明过程也很简单,可以对下列等式两端的矩阵同时取行列式,也可以从上面给出的斐波那切数列的通项公式着手。
Cassini
公式还可以变形为
利用Cassini
公式,我们便可以对上面的快速算法进行优化,请看
将公式
重新写为
将上面变形后的Cassini
公式带入,得
最后我们得到如下这个公式,每次迭代只需计算两次乘法
这就是GMP
大数运算库用来计算fibonacci数的公式。