题目描述
大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项。
n<=39
测试用例:
0 1 2 3 4 … 38
对应输出应该为:
0 1 1 2 … 39088169
1.使用简单的递归:
源码:
public class Test1 {
public int Fibonacci(int n) {
if(n<=1) {return n;}
else {return Fibonacci(n-1)+Fibonacci(n-2);}
}
}
但这样肯定不行,因为可能会导致Stack Overflow。
原因:
当计算序列为4的斐波那契数的时候,
Fibonacci(4) = Fibonacci(3) + Fibonacci(2);
= Fibonacci(2) + Fibonacci(1) + Fibonacci(1) + Fibonacci(0);
= Fibonacci(1) + Fibonacci(0) + Fibonacci(1) + Fibonacci(1) + Fibonacci(0);
由于没有记录Fibonacci(1)和Fibonacci(0)的结果,对于程序来说它每次递归都要进行重复计算。所以仅仅计算Fibonacci(4)就需要分解为三步。
对上述代码进行适当的改进:
2.简单的动态规划
此方法算是属于以空间换时间的做法了。
使用一个数组将所有的递归结果都记录,到时只需返回数组中的值即可。时间复杂度到了O(n)
public class Test1 {
public int Fibonacci(int n) {
if (n <= 1) {
return n; //考虑负数,和F(0)F(1)的情况
}
int[] mark = new int[n + 1];//创建一个大小容纳一个斐波那契数列的数组
mark[0] = 0; //将前两位进行默认地初始化
mark[1] = 1;
for (int i = 2; i <= n; i++) {
mark[i] = mark[i - 1] + mark[i - 2];//使用递归
}
return mark[n]; //传回数组末尾的值
}
}
那么既然知道了浪费空间,是否可以避免这一点并进行优化呢?
3.使用循环:
此时,算法复杂度同样达到了O(n),也避免了开辟一个不必要的数组
public class Test1 {
public int Fibonacci(int n) {
int Fn1 = 1;//相当于F(n-1)
int Fn2 = 0;//相当于F(n-2)
int result = 0;
if (n <2)
return n;
for (int i = 2; i <= n; i++) {
result = Fn1 + Fn2;
Fn2 = Fn1;
Fn1 = result;
}
return result;
}
}
4.尾递归
存在着一种特殊的递归优化方法可以达到上面循环的功效:低空间复杂度,低时间复杂度。
原理:递归本质上是栈,可能导致栈溢出,只要避免溢出就可以了。
尾递归:如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。
特性:尾递归函数的特点是在回归过程中不用做任何操作。具体解释请参考尾递归只是Java中并没有对尾递归做优化,但是当我们使用不那么大的数字的时候,尾递归却也表现出比第一种方法更好的运行测试结果。所以我们可以有如下代码
源码:
public class Test1 {
public int Fibonacci(int n) {
return Fibonacci(n, 0, 1);
}
private static int Fibonacci(int n, int acc1, int acc2) {
if (n == 0)
return 0;
if (n == 1)
return acc2;
else
return Fibonacci(n - 1, acc2, acc1 + acc2);
}
}
5.矩阵快速幂方法:
线性代数中,存在如下等式:
等式可以简化为:
算法核心是:
* 1.矩阵的乘法
* 2.矩阵快速幂(因为如果不用快速幂的算法,时间复杂度也只能达到O(N),而此算法复杂度为O(logN))
* 此处只粘贴处源码
* 具体的理解可以参考知乎大神的回答,很详细易懂:王希的回答
源码:
public class Test1 {
public int Fibonacci(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 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 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;
}
}
运行测试:
*第一种:
运行时间:1277ms
占用内存:15256k
*第二种:
运行时间:20ms
占用内存:15316k
*第三种:
运行时间:15ms
占用内存:20780k
*第四种:
运行时间:24ms
占用内存:15280k
*第五种:
运行时间:16ms
占用内存:16784k
总结:
在Java中因为并没有对尾递归做优化,所以Java程序员在涉及递归的问题上时,一般使用循环而不是递归。
在这个问题上,给出n<=39,并没有涉及很大的数,所以使用尾递归的测试结果居然出乎意料的好。
这几种方法,简单的递归最容易理解,矩阵快速幂复杂度最低。平时折中使用的话还是选择循环或者动态规划方法更好一些。
在针对尾递归做过优化的语言中也可选择使用尾递归。简单易懂且好用。