您的位置:首页 > Web前端

剑指offer——斐波那契数列

2017-03-23 22:17 302 查看

1. 问题描述

大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项。

n<=39

2. 求解方法

首先,我们要明确一下斐波那契数列,因为对于斐波那契数列还是有一些争议的,正确的斐波那契数列是这样的:0,1,1,2,3,5,8…

但是我们接触斐波那契数列的时候,是由生兔子接触的,所以会误认为是1,1,2,3,5,8…

再者,我们程序员的第几项,都是从第0项开始,这点没有什么异议吧。接下来我们就开始解题了。这其实是一个非常古老,而我们又非常熟悉的题目,那么该怎么解呢?

2.1 Level1

递归

正如上一讲所说的,这个题目实在是太熟悉,以至于我们都熟悉了它的公式:

f(n)=f(n−1)+f(n−2)

因此,我们为了省事,自然而然的想到了递归:

public static int Fibonacci(int n) {
if(n<=1)
return n;
else{
return Fibonacci(n-1)+Fibonacci(n-2);
}
}


这个样子,我相信做算法题目的肯定都知道,但是这样做会有大量的冗余计算,一旦给出了一个特别大的数,这样做只能Stackoverflow。那么如果我们把这些算出过的数存起来不就解决了么?这就是Level2。

2.2 Level2

迭代

正如Level1所讲,如果我们把所有的数目都计算好,这样不就不会有冗余计算了么?没错:

//数组方式,减少重复计算。
public static int Fibonacci(int n) {
if(n<2){
return n;
}
else{
int[] array=new int[n+1];
array[0]=0;
array[1]=1;
for(int i=2;i<=n;i++){
array[i]=array[i-1]+array[i-2];
}
return array
;
}
}


但是这样就真的已经足够优化了么?会不会太浪费空间了?没错,下面我们使用动态规划的思想来做,这就是Level3。

2.3 Level3

动态规划讲究的是状态转移,其实就是说,对于这个斐波那契数列来讲,只需要考虑当前值和之前的2个值即可。每次循环都可以进行一次状态转移。

//动态规划
public static int Fibonacci(int n) {
//前2个数
int last=0;
//前1个数
int recent=1;
//当前数
int now=n;
while(n>=2){
now=last+recent;
last=recent;
recent=now;
n--;
}
return now;
}


但是这样似乎还是不够简洁,我们来优化一下:

//动态规划的思想,更简化
public static int Fibonacci(int n) {
int last=0;
int recent=1;
while(n-->0){
recent+=last;
last=recent-last;
}
return last;
}


这样的话,就是很简洁的状态了,基本上都是有效语句。好吧,这时候的时间复杂度已经是O(n)级别了。应该到这就结束了才对。

2.4 Level4

NO!NO!NO!当然还不是最优,因为如果要是硬要优化的话,时间复杂度可以降为O(logn)。来看看是怎么解的吧。

/* O(logN)解法:由f(n) = f(n-1) + f(n-2),可以知道
* [f(n),f(n-1)] = [f(n-1),f(n-2)] * {[1,1],[1,0]}
* 所以最后化简为:[f(n),f(n-1)] = [1,1] * {[1,1],[1,0]}^(n-2)
* 所以这里的核心是:
* 1.矩阵的乘法
* 2.矩阵快速幂(因为如果不用快速幂的算法,时间复杂度也只能达到O(N))
*/
public static int Fibonacci4(int n){
if (n < 1)
{
return 0;
}
if (n == 1 || n == 2)
{
return 1;
}
int[][] base = {{1,1},
{1,0}};
//求底为base矩阵的n-2次幂
int[][] res = matrixPower(base, n - 2);
//根据[f(n),f(n-1)] = [1,1] * {[1,1],[1,0]}^(n-2),f(n)就是
//1*res[0][0] + 1*res[1][0]
return res[0][0] + res[1][0];
}


这里面还需要两个辅助方法:

//矩阵乘法
public static int[][] multiMatrix(int[][] m1,int[][] m2) {
//参数判断什么的就不给了,如果矩阵是n*m和m*p,那结果是n*p
int[][] res = new int[m1.length][m2[0].length];
for (int i = 0; i < m1.length; i++) {
for (int j = 0; j < m2[0].length; j++) {
for (int k = 0; k < m2.length; k++) {
res[i][j] += m1[i][k] * m2[k][j];
}
}
}
return res;
}
/*
* 矩阵的快速幂:
* 1.假如不是矩阵,叫你求m^n,如何做到O(logn)?答案就是整数的快速幂:
* 假如不会溢出,如10^75,把75用用二进制表示:1001011,那么对应的就是:
* 10^75 = 10^64*10^8*10^2*10
* 2.把整数换成矩阵,是一样的
*/
public static int[][] matrixPower(int[][] m, int p) {
int[][] res = new int[m.length][m[0].length];
//先把res设为单位矩阵
for (int i = 0; i < res.length; i++) {
res[i][i] = 1;
} //单位矩阵乘任意矩阵都为原来的矩阵
//用来保存每次的平方
int[][] tmp = m;
//p每循环一次右移一位
for ( ; p != 0; p >>= 1) {
//如果该位不为零,应该乘
if ((p&1) != 0) {
res = multiMatrix(res, tmp);
}
//每次保存一下平方的结果
tmp = multiMatrix(tmp, tmp);
}
return res;
}


这样的话,时间复杂度是可以达到O(logn)的水平。有的同学可能会说了,写了这么多代码,这个时间复杂度会不会增大了。在上一题目中,我们对于O(logn)和O(n)的差距做了一个比较,如果知道其差异,就可以明白,上面那么多的代码只会增加系数的大小,相比较那种动辄几十倍的差异,这点系数的增加会随着n的增大而不断会被忽略。

2.5 Level5

黑魔法在这里!O(logn)的时间复杂度已经足够优秀了,怎么,还能继续优化?没错!你没有看错!没错!还可以继续优化!

如果懂得组合数学的话,那么这里的黑魔法可以把时间复杂度降为O(1),对,你没有看错,真的是O(1),而且代码长度也会大大缩短!这个黑魔法的名字就叫:用生成函数求解递推关系。

具体的学术上的知识同学感兴趣的话可以自行查阅,下面给出代码:

//用生成函数求解递推关系,时间复杂度O(n)
public static int Fibonacci(int n){
double root=Math.sqrt(5);
return (int)Math.round(Math.pow(((1 + root)/2), n) / root - Math.pow(((1 - root)/2), n) / root);
}


没错,就是这2句话,如果你高兴的话,还可以浓缩成1句话!那么这么长一串,到底说的是什么意思呢?

我把它改写成这样子大家就会明白了:

public static int Fibonacci(int n){
double root=Math.sqrt(5);
double x1=Math.pow(((1 + root)/2), n) / root;
double x2=Math.pow(((1 - root)/2), n) / root;
return (int)Math.round(x1-x2);
}


之所以用Math.round(),是因为double都会有那么一丁点的误差,所以需要使用四舍五入来补上这点残差。它其实是递推公式的解:

f(n)=(1+5√2)n−(1−5√2)n5√

3. 算法分析

3.1 就事论事

就第一种方案来说,最简单,其时间复杂度是这样的,前n项和的公式为:

Sum(n)=f(n+2)−1

其中:

f(n)=(1+5√2)n−(1−5√2)n5√

也就是说是O(kn)级别的,这种级别肯定是要炸了的。因此,虽然简单,但是没有办法计算大于40以上的,这也是为什么题目规定n≤39的原因。

第二种方案,没什么问题,时间复杂度为O(n),但是空间复杂度同样是O(n),如果内存比较紧俏的话,也是难以接受的。

第三种动态规划,时间复杂度为O(n),但是把空间复杂度缩减为了O(k),这样就解决了上述两个问题,基本上是可以接受的。考虑到这层面就算可以的了。

第四种,使用矩阵。如果对于线性代数掌握不好的,对于矩阵运算也不熟悉,很难做出这样的算法。虽然它的时间复杂度为O(logn),但较难实现,数学好的除外。

第五种,使用生成函数。如果没学过组合数学,那么生成函数也是难以想到的,不过,如果熟悉一点组合数学的话,写出这个算法并不困难。况且,它的时间复杂度是O(1),当然,指数计算现在来看已经不是太慢了。

3.2 思维层面

这里思维的跳度还是比较大的,第一种使用递归,完全符合正常想法。第二种使用数组存放,这样是全局来看。但是仍然有冗余存在。第三种使用动态规划,掌握到了核心特性。第四种和第五种,都是超出了正常算法范畴,如果没有背景知识,很难掌握到,但是一旦掌握,那将是质的飞跃。

4. 小结

一个简单的斐波那契数列,我们最熟悉的算法题,竟然可以上升到如此高度。可见对于算法的追求,是永无止境了。上述算法也只是当前我能想到的,再优化的算法还可以一起讨论。好了,下题见。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息