斐波那契数列算法优化
一. 引言
这道题对我有很特殊的意义,就以博客第一篇文章的方式记录下吧。
二. 何来斐波那契数列
在wikipedia中讲到:斐波那契在研究兔子的生育时发现了这个数列。条件是这样:
- 第一个月初有一对刚诞生的兔子
- 第二个月之后,即第三个月初他们可以生育
- 每月每对可生育的兔子会诞生下一对新兔子
- 兔子永不死去
假设在第N月共有兔子a对,第N+1月共有兔子b对,则N+2月该有多少对兔子?
首先其必定大于b对(废话。。), 具体的数量应该等于 b + b中能生新兔子的对数,第N月的a对兔子显然还能继续生,在N+1月新生的b-a对新兔子根据条件在N+2月时还不能生新兔子。因此N+2月的兔子总数为 b + a。由此推出了斐波那契数列的递归式。
二. 斐波那契数列的定义
,递归定义,简单,至少看着简单
三. 涉及到斐波那契数列的算法题
爬楼梯,一次只能爬一个台阶或两个台阶,问到第N层有几种爬法。f(n)即为斐波那契数列的定义,不过有一点小的差别就是:当n=0时,需要f(n) =1, 以满足 f(2) = 2,因为显然到第二层有两种爬法。
四. 如何计算斐波那契数列
1.递归法
按照定义,利用程序实现递归。
1 int fibonacci ( int n ){ 2 if(n == 0) 3 return 0; 4 if(n == 1) 5 return 1; 6 return fibonacci(n-1) + fibonacci(n-2); 7 }
递归的时间复杂度为o(2^n)。
2.递推法
2.1 普通递推法
递归写的程序看上去简洁明了,实际机器运行时却不是如此,深度的递归占用很多栈空间,容易造成溢出。另外在递归计算fibonacci数列过程中有很多重复计算。计算f(n)需要计算f(n-1),f(n-2)。计算f(n-1)时又要计算一遍f(n-2)。在n数字很大时,这样重复的计算非常耗时且没有意义。
解决方法就是使用一个记录数组,将递归法转为递推法。如果需要计算f(n),我们就从f(0)开始计算,一直计算到f(n)。这样的解法消除了重复计算,因为每次计算新值都是从之前计算过的数组中取。递推法的时间复杂度为 o(n),空间复杂度亦为 o(n)。
2.2 优化递推法
递推法其实还能够进行优化,分析一下:在计算f(n)时我们只需要 f(n-1), f(n-2),数组index在n-2之前的元素已经没有存在的必要。其实使用两个变量来保存需要计算出f(n)的前两个元素即可。不明白?看代码。
01 int fibonacci(int n){ 02 int n_1 = 0, n_2 = 1; 03 int temp; 04 for(int i = 2; i < n ; i++){ 05 temp = n_1; 06 n_1 = n_2; 07 n_2 += temp; 08 } 09 return n_2; 10 }
需要的信息在 n_1 , n_2这两个变量间不断的推演,初始 n_1 代表 f(0),n_2代表f(1), 当计算完f(2) = f(0) + f(1)后 ,f(0)的值我们已经不需要了,此时为了计算f(3)我们需要的是f(1)和刚刚计算好的f(2), 亦即令n_1代表f(1),n_2代表f(2),将n_1与n_2进行交换,然后再将n_2赋值为刚计算好的新值。 时间复杂度为o(n),空间复杂度优化为o(1)。
3.更优解?
通过上面介绍的方法,我们已经将时间复杂度优化为o(n),空间复杂度优化为o(1)。还有更优解吗?或者说更优解的思路在哪?
3.1 斐波那契数列递推式
在优化的递推法中已经提到,n_1,n_2 两个变量不断的推演去求得斐波那契数列的值,而n_1, n_2实际都可以表示为f(0)与f(1)组合的多项式,看下面的例子
1. f(2) = f(1)+f(0)
2. f(3) = f(2)+f(1)=2*f(1) + f(0)
3. f(4) = f(3) + f(2) = 3*f(1) + 2*f(0)
......
n+1. f(n) = f(n-1) + f(n-2) = A*f(1) + B*f(0)
So, 有没有可能求出A与B的值?
如果以矩阵行列式的方式来表示这种推演:
3.2 矩阵乘方运算的优化
问题转化为如何计算n-1次矩阵的相乘,普通的解法就是矩阵相乘n-1次,时间复杂度为o(n),与递推解法相比没有什么优化。但是乘方运算显然可以进行优化,不需要n-1次那么多。
为了书写方便,将矩阵看做一个自然数M,问题转换为怎样优化 M^N的计算,初中生也能想到 :
通过二分法的优化,每一次乘方运算都减了一半的运算量。代码描述如下:
1 int pow(int M , int N){ 2 if(N == 1) 3 return M; 4 if(N%2 == 0){ 5 int temp = pow(M,N/2); 6 return temp*temp; 7 } 8 return pow(M,N-1) * M; 9 }
上述代码的时间复杂度为O( logN ),O(N)到O(logN)已经是很大的进步了,但是,还能优化吗?显然可以,上述算法依然是一个递归算法,那其必然也存在递归的通病,容易栈溢出。
3.3 优化中的优化
好吧,我们依然用解决斐波那契数列的思路来解决这题, 递归转为递推,为表示方便以 f[N] 表示M的N次方的值,我们从f[0]开始计算一直计算到f[N]。
f[0] = 1 , f[1] = M , f[2] = f[1] * f[1] , f[3] = f[2] * f[1] , f[4] = f[2] * f[2] , ........
写着写着发现有点不对劲,哪里不对劲呢?f[4]需要用到f[3]的值吗?f[N]需要用到f[N-1],f[N-2],直到f[N/2+1]的值吗?我们显然做了很多无效计算,也存储了很多我们显然不需要的值。
So, 问题又转换为:给你一个数N,如何在递推中判断是否该存储遇到的值。
这里懒得写公式了,就copy下编程之美上的解释:
分析:
以一个通项为例当ak(即n在二进制表示中的第K位)为0时,其值为1,不需要任何计算,当ak等于1时,我们需要将之前的乘积再乘以来得到A^n。而显然
可以由来求得
算法描述:(使用pre来表示, num记录乘积的中间值,最终求得的num即为A^n)
01 int pow(int M, int N){ 02 int pre = 1, num = 1; 03 for( ; N > 0; N = N>>1){ 04 if(pre == 1) 05 pre = M; 06 else 07 pre = pre*pre; 08 if(N & 1) 09 num = pre* num; 10 } 11 return num; 12 }
该算法的时间复杂度为 O( logN ),空间复杂度为 O( 1 )
五. 结束
写完了重读一遍发现废话有点多=。=,但至少写的是自己的整个思考过程,跟算法的优化一样,慢慢改进吧^_^。