剑指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. 小结
一个简单的斐波那契数列,我们最熟悉的算法题,竟然可以上升到如此高度。可见对于算法的追求,是永无止境了。上述算法也只是当前我能想到的,再优化的算法还可以一起讨论。好了,下题见。相关文章推荐
- 【剑指Offer】斐波那契数列
- 剑指Offer之斐波那契数列
- 剑指offer——7.斐波那契数列
- 剑指offer--斐波那契数列
- 剑指offer:斐波那契数列
- [剑指offer]算法7 斐波那契数列
- 【剑指Offer】斐波那契数列及其延伸问题——JavaScript实现
- 剑指offer: 斐波那契数列
- 剑指offer面试题9 斐波那契数列及青蛙跳台阶问题
- 剑指offer 面试题9 斐波那契数列/青蛙跳台阶/矩形覆盖/变态跳台阶
- 剑指offer-斐波那契数列
- [牛客网,剑指offer,python] 斐波那契数列
- [牛客网,剑指offer,python] 斐波那契数列
- 【剑指offer】面试题9:斐波那契数列
- 剑指offer-算法题练习:part9 变态跳台阶-斐波那契数列问题
- 剑指Offer之斐波那契数列问题
- 剑指Offer学习总结-斐波那契数列
- 剑指offer题9_斐波那契数列
- 【剑指Offer】007 斐波那契数列
- 剑指offer---斐波那契数列