斐波那契数列
放完国庆假期,自己也该投入学习中了。先温故学习一下斐波那契数列,又称黄金分割数列,它是算法题经典中的经典,斐波那契数列在数学和生活以及自然界中都非常有用,具体的编程求解方法也有很多。这里整理一下,做个记录。
起源定义
参考知乎。
斐波那契数列最早被提出是印度数学家Gopala,他在研究箱子包装物件长度恰好为1和2时的方法数时首先描述了这个数列。
也就是这个问题:
有n个台阶,你每次只能跨一阶或两阶,上楼有几种方法?
而最早研究这个数列的当然就是斐波那契(Leonardo Fibonacci)了,他当时是为了描述如下情况的兔子生长数目:
第一个月初有一对刚诞生的兔子
第二个月之后(第三个月初)它们可以生育
每月每对可生育的兔子会诞生下一对新兔子
兔子永不死去
这个数列出自他赫赫有名的大作《计算之书》,后来就被广泛的应用于各种场合了。
这个数列是这么定义的:
(注意,并非满足第三条的都是斐波那契数列,卢卡斯数列(A000032 - OEIS)也满足这一特点,但初始项定义不同)
求解方法
目前整理了斐波那契数列的4种解法。
斐波那契数列:
Fib(0) = 0
Fib(1) = 1
Fib(n) = Fib(n-1) + Fib(n-2)
F() = 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 …
在生活中,斐波那契数列有如下的自然现象:
诸如此类的还有向日葵、蜂巢、蜻蜓翅膀、海螺、人类耳朵轮廓等等。
递归做法
递归还是挺好理解的,斐波那契数列也是递归原理讲解的经典题目。
1 | /** |
由于每一次调用recursion方法都会再调用recursion两次,就像树节点下的两个子节点。斐波那契数列树因此而来。
比如求解f(5),有以下的树结构:
递归调用的时间复杂度是很恐怖的,O(n)=2^n。如果计算超过100,计算速度就很慢了,而且可能会造成栈溢出异常。
循环做法
循环做法可以借助一个数组,来存储中间的计算过程,便于查询和计算。
比如算n=50后,再计算n=6可以直接在数组中取即可,无需再计算。
1 | /** |
当然,也可以直接计算,减少存储空间的浪费。
1 | /** |
循环做法在时间复杂度上肯定是优于递归做法的。
矩阵做法
参考博客。
矩阵做法需要一定的线性代数的知识,主要是矩阵的相乘,矩阵的基本性质,单位矩阵。
矩阵的基本性质回顾:
乘法结合律:(AB)C=A(BC).
乘法左分配律:(A+B)C=AC+BC
乘法右分配律:C(A+B)=CA+CB
对数乘的结合性:k(AB=(kA)B=A(kB)
转置:(AB)T=BTAT
矩阵乘法一般不满足交换律。
首先是矩阵的相乘:
1 | /** |
然后是一个矩阵的n次幂,矩阵的幂就是矩阵的连乘:
比如:
1 | 16=2*2*2*2 |
类比于
1 | A^9=A*((A^2)^2)^2) |
代码的话可以这样编写:
1 | /** |
接下来就是斐波那契数列递推公式的推导过程了:
数列的递推公式为:f(1)=1,f(2)=2,f(n)=f(n-1)+f(n-2) (n>=3)
用矩阵表示为:
进一步,可以得出直接推导公式:
由于矩阵乘法满足结合律,在程序中可以事先给定矩阵的64,32,16,8,4,2,1次方,加快程序的执行时间。
给定的矩阵次幂,与二进制有关是因为,如下的公式存在解,满足Xi={0或1}:
为了保证解满足 Xi={0或1},对上述公式的求解从右向左,即求解顺序为Xn,Xn-1,Xn-2,….,X1,X0。
公式做个概括就是如下:
有了公式,再结合上面的矩阵运算的方法,可以很轻松的写出代码:
1 | /** |
使用矩阵的n次快速幂,可以使得计算由线性而升高一个纬度,转为对数计算,计算时间大大减少。
算法的时间复杂度为O(n)=logn。
公式做法
公式的推导过程可以参考百度百科,目前有三种证明方法,分别是初等代数下的利用待定系数法构造等比数列的两种方法和利用线性代数中矩阵特征方程求解。
值得注意的是公式本身是正确的,而在于计算机在计算根号和除法时可能会有误差。
这里粘上特征方程证明法:
1 | /** |
测试结果
先贴上完整代码:
1 | /** |
再贴上测试代码:
1 | public class FibonacciTest { |
测试结果:
分别测试在n比较小情况下,和n比较大的情况下的测试结果。
1 | 当n=10时: |
文末总结
斐波那契数列经典但也时编程的基本功题目。同时,它也能体现很多编程的技巧和算法的思想,比如理解递归,包含动态规划和分治的思想。
具体的经典变形比如:跳台阶,矩形覆盖(参见《剑指Offer》)。