前言
很多人在学习编程的时候,都会遇到递归算法,比较经典的一道递归题就是斐波那契数列,原题大家应该都懂,这里不过多介绍。
在【剑指offer】中也出现了这道题,我发现这种经典的递归算法中,出现很多不足,因此本篇博客记录了解斐波那契数列的优化思路以及简要介绍其拓展题型。
正文
原题:
大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。
n<=39
思路1:
思路1也就是最经典的解法,即递归求解,代码如下:
代码
public class Solution {
public int Fibonacci(int n) {
// 递归版本,比较费时,因为要不断申请空间,且存在大量重复的计算
// 由于题目要求从0开始,第0项为0,因此有些小出入,不过思想都是一样的
if (n <= 2) {
return (n == 0) ? 0 : 1;
}
return Fibonacci(n - 2) + Fibonacci(n - 1);
}
}
代码讲解
这里面我直接给出一张图,假设n = 5,该代码的运行过程如下图所示:
其实不难发现,这种思路存在着大量重复计算,在计算Fibonacci(5)的时候,我们已经算了Fibonacci(3),但是在计算Fibonacci(4)的时候,我们还要再次计算Fibonacci(3),如下图所示,圈出来的部分就是重复计算:
当n = 5时,重复计算4次,随着n不断增大,重复计算的次数也会越来越多,提交代码发现时间真的很慢,而且内存占用也比较高,这是因为在递归函数中,每调用一次自己,就会在栈中再次申请一个存储新函数的内存,而一个方法块中会调用两次自己,其时间跟内存的开销都是很大的。
思路2:
既然递归开销很大,那我们可以使用循环替代(顺便说一句,基本上所有的递归题都可以用循环来实现)。
那如何存储斐波那契数列的前两个数的值呢?我们可以使用一个长度为n的数组存储,数组中存放了完整的斐波那契数列,代码如下:
代码
public class Solution {
public int Fibonacci(int n) {
// 循环版本,空间复杂度为O(N),使用数组存在每一个值
// 当n小于2时,就不用申请数组空间了,直接返回n
if (n < 2)
return n;
int[] nums = new int[n + 1];
// 初始化数组,第1个元素和第2个元素都为1
nums[1] = 1;
nums[2] = 1;
// i从3开始,因为前两个元素已经确定,接下来要从第3个元素开始计算
int i = 3;
while (i <= n) {
// 第i个元素等于前两个元素相加
nums[i] = nums[i - 2] + nums[i - 1];
i++;
}
return nums[n];
}
}
代码讲解
当n小于2的时候,我们就不需要申请数组了,直接返回n
if (n < 2)
return n;
nums数组用来存放斐波那契数列,其长度为n + 1,这是因为考虑到题意是从0开始的因素,第1个和第2个索引的值都为1
int[] nums = new int[n + 1];
// 初始化数组,第1个元素和第2个元素都为1
nums[1] = 1;
nums[2] = 1;
i对应数组的索引下标,初始值为3,也就是从第3个元素计算,因为前两个数已经初始化好了
int i = 3;
这里面的作用就是计算斐波那契了,nums第i个元素的值等于前两个元素相加
while (i <= n) {
// 第i个元素等于前两个元素相加
nums[i] = nums[i - 2] + nums[i - 1];
i++;
}
最后直接返回第n个元素的值即可,数组nums的值如下所示,这里假设n为5:
索引 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
值 | 0 | 1 | 1 | 2 | 3 | 5 |
从题意可以看出,时间复杂度为O(N),空间复杂度为O(N),这是因为需要额外的数组来存放数列的值,提交代码,时间减少了很多。
思路3:
思路2的时间复杂度应该是最小的了,但是空间复杂度有点大,需要O(N)的空间,那我们可以从减少空间复杂度的角度思考。
我们知道,斐波那契数列主要就是通过前两个数的值计算出当前的值,因而我们可以只记住前两个数而不必记住所有的数,通过pre来存储上一个数的值,prePre存储上上个数的值,当前数则等于pre + prePre,代码如下:
代码
public class Solution {
public int Fibonacci(int n) {
// 循环版本,空间复杂度为O(1)
// 当n小于2时,就不用申请数组空间了,直接返回n
if (n < 2)
return n;
// 标记上一个值,类似nums[n - 1]
int pre = 1;
// 标记上上一个值,类似nums[n - 2]
int prePre = 1;
// i从3开始,因为前两个元素已经确定,接下来要从第3个元素开始计算
int i = 3;
// 返回的结果
int result = 1;
while (i <= n) {
// result等于前两个数相加
result = pre + prePre;
// 此时的prePre,即上上个数等于上个数
prePre = pre;
// 上个数就等于result
pre = result;
i++;
}
return result;
}
}
代码讲解
pre变量用于记住上一个数的值,这里初始化为1,就类似思路2的nums[2]
int pre = 1;
prePre变量用于记住上上个数的值,这里同样初始化为1,类似思路2的nums[1]
int prePre = 1;
i跟思路2一样,初始化为3,也就是说直接从第3个元素开始计算就行了
int i = 3;
循环体的功能就是计算斐波那契数列,result就是用来返回结果的,result = pre + prePre,即等于前两个数的和。
由于result(第n个斐波那契数列的值)已经计算出来,上一个数跟上上个数都需要发生改变(改变pre和prePre的值是为了计算下一个result的值,若不发生改变的话,result一直等于1 + 1),上上个数此时就等于上一个数,上一个数就等于result。
while (i <= n) {
result = pre + prePre;
prePre = pre;
pre = result;
i++;
}
提交代码,时间跟思路2差不多,空间有所减少。
扩展
在【剑指offer】中,还有一些涉及到斐波那契数列的题型,比如跳台阶、变态跳台阶,若有同学做过这两道题的话,可以发现在跳台阶中,可能出现的跳法规律就是一个斐波那契数列,而变态跳台阶是斐波那契数列的变形,因此斐波那契数列掌握了之后,这两道题也就差不多可以解决了,思路都是差不多的。
总结
我觉得掌握某一道题的最好办法就是至少给出2种或者3种不同的解法,由时间复杂度最差的方法一步一步的改进,直到找到最优解。
就类似斐波那契数列,看起来是一道很简单的题,但是背后有很多需要注意的地方。