在程序员笔试面试中,斐波那契数列的计算是很经典的一道题,本人在腾讯现场笔试题中就碰到过。比如这样的一道题:
有N根香蕉,猴子每次只能拿一根或者两根,请问有多少种不同的拿法?
针对这道题列出递推式 F(n)=F(n−1)+F(n−2) ,可以看到这就是斐波那契数列的递推式。
一般来说,如果我们直接把上面的递推式改写成代码,那么代码时间复杂度会是指数级的,原因是会有大量的重复计算。这显然不是令人满意的做法。由于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
这段代码的时间复杂度从指数级直降到了线性。
但是这个时间复杂度还是不够快。
实际上,斐波那契数列是有矩阵形式的解析解的:
# 矩阵乘法
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]
由于这个矩阵解析解非常好记,而且代码实现也简单,所以它也成了面试中的最优解,一般面试中答到这里基本就可以停下来了。
实际上很多人中学的时候可能就知道斐波那契数列的通项公式,它是下面这样子的:
还有没有更快的方法呢?不妨参考一下一些优秀开源库中的斐波那契数列算法。
以GMP(GNU多重精度算术库)为例,它并没有使用常见的矩阵快速幂的方法,而是跟文章最开始提到的方法一样,使用递推式。但是它使用的递推式要更复杂:
这个算法是这样子的:
首先把n转化为二进制形式,然后从最高位(第0位)开始向最低位遍历,设n的二进制前 i 位(0到(i-1)位)表示的值为 ki ,那么 F(ki) 和 F(ki−1) 都是已知的。
如果第 i 位为1,那么就用已知的 F(ki) 和 F(ki−1) 计算 F(2ki+1) 和 F(2ki) ,得到 F(ki+1) 和 F(ki+1−1)
如果第 i 位为0,那么就用 F(ki) 和 F(ki−1) 计算 F(2ki) 和 F(2ki−1) ,得到 F(ki+1) 和 F(ki+1−1)
可以看到迭代的次数跟n的二进制长度一致,所以其时间复杂度也是 O(log2n) 。
需要注意的是,上述递推式中的 F2(k) 和 F2(k−1) 是可以复用的,实际上每一次迭代只需要进行两次大整数乘法运算。
举个栗子:
要求F(25),已知F(-1)=1,F(0)=0
先把25化成二进制形式(11001),从最高位开始,它是1,那么此时k=0:
第3位为0,此时k=3:
第4位为0,此时k=6:
第5位为1,此时k=12,得到最终结果:
下面是代码实现
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