关于斐波那契数列的一些总结

引言

在程序员笔试面试中,斐波那契数列的计算是很经典的一道题,本人在腾讯现场笔试题中就碰到过。比如这样的一道题:

有N根香蕉,猴子每次只能拿一根或者两根,请问有多少种不同的拿法?

针对这道题列出递推式 F(n)=F(n1)+F(n2) ,可以看到这就是斐波那契数列的递推式。

简单迭代算法

一般来说,如果我们直接把上面的递推式改写成代码,那么代码时间复杂度会是指数级的,原因是会有大量的重复计算。这显然不是令人满意的做法。由于F[N]的值实际上只依赖于F[N-1]和F[N-2],那么我们可以将它优化成迭代形式的代码,比如下面的代码:

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

这段代码的时间复杂度从指数级直降到了线性。
但是这个时间复杂度还是不够快。

矩阵快速幂

实际上,斐波那契数列是有矩阵形式的解析解的:

(Fn+1FnFnFn1)=(1110)n

可以看到式子右边是常数矩阵幂的形式,如果n个矩阵直接相乘求幂,那么时间复杂度还是O(n),但是求幂是有快速幂算法的。
所谓快速幂,以求5的100次方为例,首先把100化成二进制形式,得到:
5100=5(1100100)2

然后将上式化成算式
5100=516451325016508514502501=56453254

注意到
52=515154=5252...564=532532

每一步的结果都可以通过上一步的结果计算得出,迭代次数与n的二进制位数一致,所以它的时间复杂度是 O(log2n)
实际代码如下:

# 矩阵乘法
def mat_mul(a, b):
    tmp = [0] * 4
    tmp[0] = a[0] * b[0] + a[1] * b[2]
    tmp[1] = a[0] * b[1] + a[1] * b[3]
    tmp[2] = a[2] * b[0] + a[3] * b[2]
    tmp[3] = a[2] * b[1] + a[3] * b[3]
    return tmp


def fib2(n):
    mat = [1, 1, 1, 0]  # 斐波那契解析解里的常数矩阵
    res = [1, 0, 0, 1]  # 单位矩阵
    while n > 0:
        if n & 1 == 1:
            res = mat_mul(res, mat)
        mat = mat_mul(mat, mat)
        n >>= 1
    return res[1]

由于这个矩阵解析解非常好记,而且代码实现也简单,所以它也成了面试中的最优解,一般面试中答到这里基本就可以停下来了。

关于通项公式

实际上很多人中学的时候可能就知道斐波那契数列的通项公式,它是下面这样子的:

F(n)=15(1+52)n15(152)n

在网上也见过有人认为这才是计算斐波那契数列最快的方法,甚至还有人认为它的时间复杂度是O(1)。
这种结论完全是错误的。
首先这个通项公式使用了 5 这个无理数,在计算机中是没有办法对无理数进行直接计算的,必须取近似值,那么问题来了,到底需要精确到多少位才能保证最终结果不出错呢?
其次,要注意到这个通项公式中含有变量n,也就是需要求某个数的n次幂。上面已经提到,求幂的时间复杂度是对数级的,这条公式的时间复杂度自然也就是对数级的。

更快的方法

还有没有更快的方法呢?不妨参考一下一些优秀开源库中的斐波那契数列算法。
以GMP(GNU多重精度算术库)为例,它并没有使用常见的矩阵快速幂的方法,而是跟文章最开始提到的方法一样,使用递推式。但是它使用的递推式要更复杂:

F(2k+1)F(2k1)F(2k)=4F2(k)F2(k1)+2(1)k=F2(k)+F2(k1)=F(2k+1)F(2k1)

这个算法是这样子的:
首先把n转化为二进制形式,然后从最高位(第0位)开始向最低位遍历,设n的二进制前 i 位(0到(i-1)位)表示的值为 ki ,那么 F(ki) F(ki1) 都是已知的。

  • 如果第 i 位为1,那么就用已知的 F(ki) F(ki1) 计算 F(2ki+1) F(2ki) ,得到 F(ki+1) F(ki+11)

  • 如果第 i 位为0,那么就用 F(ki) F(ki1) 计算 F(2ki) F(2ki1) ,得到 F(ki+1) F(ki+11)

可以看到迭代的次数跟n的二进制长度一致,所以其时间复杂度也是 O(log2n)
需要注意的是,上述递推式中的 F2(k) F2(k1) 是可以复用的,实际上每一次迭代只需要进行两次大整数乘法运算。

举个栗子:
要求F(25),已知F(-1)=1,F(0)=0
先把25化成二进制形式(11001),从最高位开始,它是1,那么此时k=0:

F(1)F(1)F(0)=4F2(0)F2(1)+2=1=1=0

然后第2位,同样为1,此时k=1:
F(3)F(1)F(2)=4F2(1)F2(0)2=2=1=F(3)F(1)=1

第3位为0,此时k=3:

F(7)F(5)F(6)=4F2(3)F2(2)2=13=F2(3)+F2(2)=5=F(7)F(5)=8

第4位为0,此时k=6:

F(13)F(11)F(12)=4F2(6)F2(5)+2=233=F2(6)+F2(5)=89=F(13)F(11)=144

第5位为1,此时k=12,得到最终结果:

F(25)=4F2(12)F2(11)+2=75025

下面是代码实现

def fib3(n):
    b = bin(n)
    fn_prev = 1  # 设F[-1]=1只是为了编程方便,数学上F[-1]是没有意义的
    fn = 0  # F[0] = 0
    sign = 2
    for i in b[2:]:
        prev_sqr = fn_prev * fn_prev
        fn_sqr = fn * fn
        fn = 4 * fn_sqr - prev_sqr + sign  # F[2k+1] = 4*F[k]^2 - F[k-1]^2 + 2*(-1)^k
        fn_prev = fn_sqr + prev_sqr  # F[2k-1] = F[k]^2 + F[k-1]^2
        if i == '0':
            fn = fn - fn_prev  # F[N] = F[2k] = F[2k+1] - F[2k-1], F[N-1] = F[2K]
            sign = 2
        else:
            fn_prev = fn - fn_prev  # F[N] = F[2K+1], F[N-1] = F[2K]
            sign = -2
    return fn

性能对比

测试环境:

CPU:i5 3210M
系统:Windows 10 64bit
Python:Python 3.5 64bit+gmpy2

测试代码:

t1 = timeit("fib1(1000000)", setup="from main import fib1", number=5) / 5
t2 = timeit("fib2(1000000)", setup="from main import fib2", number=5) / 5
t3 = timeit("fib3(1000000)", setup="from main import fib3", number=5) / 5
print(t1)
print(t2)
print(t3, t2 / t3)

测试结果

五次平均耗时(秒,四舍五入到万分位) 提速(相对于矩阵快速幂的提升倍数)
简单迭代 12.1146 N/A(时间复杂度不同没有比较意义)
矩阵快速幂 0.6199 1.0
优化迭代 0.0489 12.7

可以看到优化的迭代方法比快速幂快了一个数量级

参考文献

https://github.com/qiwsir/algorithm/blob/master/fibonacci.md
https://en.wikipedia.org/wiki/Fibonacci_number
https://gmplib.org/manual/Fibonacci-Numbers-Algorithm.html

你可能感兴趣的:(算法)