题目描述:
斐波那契数,通常用 F(n) 表示,形成的序列称为斐波那契数列。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0, F(1) = 1 F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
给定 N,计算 F(N)。
示例 1:
输入:2 输出:1 解释:F(2) = F(1) + F(0) = 1 + 0 = 1.
示例 2:
输入:3 输出:2 解释:F(3) = F(2) + F(1) = 1 + 1 = 2.
示例 3:
输入:4 输出:3 解释:F(4) = F(3) + F(2) = 2 + 1 = 3.
提示:
- 0 ≤ N ≤ 30
解题思路:
(1)斐波那契数是一个典型的递归解决问题的模板,所以最简短的代码就是使用递归。
class Solution {
public int fib(int N) {
if(N <= 1)
return N;
return fib(N-1)+fib(N-2);
}
}
接下来计算递归解法的时间复杂度和空间复杂度。
首先来波概念:
递归算法的时间复杂度:递归的总次数*每次递归的数量。
递归算法的空间复杂度:递归的深度*每次递归创建变量的个数。
在递归调用过程中Fib(4)被计算了2次,Fib(3)被计算了3次。Fib(1)被调用了7次,Fib(0)中被调用了4次。所以,递归的效率低下,但优点是代码简单,容易理解。
递归算法时间复杂度为(二叉树的节点个数):O()=(2^h)-1=2^n。空间复杂度为树的高度:h即o(n)。
(2)可用尾递归方法来求,尾递归若优化,空间复杂度可达到o(1),但时间复杂度是o(n);
尾递归是什么呢?
尾递归解决了递归重复计算的问题。
"尾递归的前提是递归"
(1)定义:在一个程序中,执行的最后一条语句是对自己的调用,而且没有别的运算
(2)尾递归的实现:是在编译器优化的条件下实现的
编译器优化:
递归的第一次调用时会开辟一份空间,此后的递归调用不会再开辟空间,而是在刚才开辟的空间上做一些修改,实现此次递归,例如在本题中求Fib(10),编译器会给Fib(10)的调用开辟栈帧,调用Fib(9)的时候不会再重新开辟栈帧,而是在刚开辟的栈帧上做一些修改,因为递归的每一次调用都是一样的流程,只是会有一些数据不同,所以不会再开辟空间。
注:vs一般都支持优化,Debug下编译器不会优化哦,一定要在Release模式下。
class Solution {
public int fib(int N) {
int res = f(1,1,N);
return res;
}
public int f(int first, int second, int N){
if(N == 0)
return 0;
if(N < 3)
return 1;
if(N == 3)
return first + second;
return f(second, first+second, N-1);
}
}
此种方法是尾递归,很大程度的减小了第一种方法(递归实现斐波那契数列)的时间复杂度
时间复杂度:O(N-2)约等于0(N)
空间复杂度:O(N-2)约等于0(N)(编译器如果优化的话是O(1))
(3)采用循环结构来实现,此时时间复杂度为O(n),空间复杂度为O(1)
循环结构的实现有几种方式,第一种是采用两个辅助变量来分别记住Fib[i-1]和Fib[i-2]。
class Solution {
public int fib(int N) {
if(N == 0)
return 0;
if(N == 1)
return 1;
int temp1 = 0;
int temp2 = 1;
int res = 0;
for(int i=2;i<=N;i++){
res = temp1 + temp2;
temp1 = temp2;
temp2 = res;
}
return res;
}
}
也可以只使用一个辅助变量
class Solution {
public int fib(int N) {
if(N == 0)
return 0;
if(N == 1)
return 1;
int temp = 0;
int res = 1;
for(int i=2;i<=N;i++){
res += temp;
temp = res - temp;
}
return res;
}
}
还可以使用辅助数组,将每次计算的结果存储在数组里面,这样空间复杂度也变为O(n)
class Solution {
public int fib(int N) {
int f[] = new int[N+1];
f[0] = 0;
if (N > 0) {
f[1] = 1;
}
for (int i = 2; i <= N; i++) {
f[i] = f[i-1] + f[i-2];
}
return f[N];
}
}
从效率的角度考虑,采用循环结构是最优的。优点是时间复杂度和空间复杂度最低,而且可读性高。
最后,常用时间复杂度所耗费的时间从小到大依次是o(1)<o(log2n)<o(n)<o(nlog2n)<o(n^2)<o(n^3)<o(2^n)<o(n!)<o(n^n)。
最后,复习一下主定理计算递归的时间复杂度。
https://baike.baidu.com/item/%E4%B8%BB%E5%AE%9A%E7%90%86/3463232?fr=aladdin