众所周知,斐波那契数列的定义是:f(0) = 0, f(1) = 1, 且当 n > 1 时 f(n) = f(n-1) + f(n-2)。今天讨论个相对简单的问题:斐波那契数列第一千万项怎么求?
暴力算法(V0)
最容易想到的是直接翻译递推式,二话不说撸出代码:
func
很可惜当输入为 n = 10000000 时这段代码在我的电脑上跑了足足有 15 分钟,风扇呼呼地转。
矩阵快速幂优化(V1)
因为从定义我们很容易得到:
问题转成了如何快速求出 R,这里对 n 分奇偶讨论:
根据这一版的递推公式来实现的代码:
func
当输入为 n = 10000000 时大约 2.435 秒出结果,当然我们也可以把递归改成迭代(速度没有明显差距):
func
矩阵对称性优化(V2)
之所以写这篇文章,主要就是因为网上大部分的资料都止步于矩阵快速幂,其实在这基础上还有可以秀微操的空间。我们再来仔细看看 R 的定义:
你会发现之前的运算其实做了很多无用功:
- z 和 y 必然相等,z 没必要再计算一遍。
- t = x - y,因此 t 也没必要再计算一遍。
最后你会发现,原来只需要计算矩阵第一列的那两个元素即可:
根据这一版的公式再次实现代码:
func
这次只需要 0.843 秒了,同样我们可以把递归改成迭代(速度没有明显差距):
func
一种更慢的写法(Slow)
其实不管是V1还是V2,除了上面的递归和迭代两种写法,网上还能见到另一种写法:
func
很不幸这种写法虽然更容易实现,但在绝大多数情况下要更慢一些,下表是各个函数在我电脑上的运行时间汇总:
想知道为什么可以查一查秦九韶算法(Horner's method),我们原始的写法实际上是利用了这个方法进行优化的。
补充版本:Cassini’s formula(V3)
V2版本看起来似乎已经做到极致了,因为对于一个 n 它只需要执行 3*log(n) 次乘法运算(数据大到一定程度时乘法运算是主要开销)。
还能再优化吗?可以的。(受到 fss.sosei:斐波那契数列与Python的尾递归蹦床 连载【6】 的启发)
再来分析V2版本里面的 3 次核心乘法,其本质上就是:
这时候我们需要用到一个叫做 Cassini’s formula 的公式了:
证明很简单,考虑 R 矩阵的行列式就可以了,这里不多赘述。利用这个公式,我们可以进行以下推导:
也就是说,原来 xy 还可以由 xx 和 yy 得到,免去了一次乘法,基于此优化的代码如下:
func
这段代码在 n = 10000000 时运行了 0.522 秒,比起V2版本的 0.843 秒还要好不少,当然我们也可以把递归改成迭代(速度没有明显差距):
func
最后附上相关的辅助代码:
type