斐波那契数列(动态规划)
题目描述
设斐波那契数列第 n 个数字为 f(n) 。根据数列定义,可得f(n)=f(n−1)+f(n−2) ,且第 0 , 1 个斐波那契数分别为 f(0)=0 , f(1)=1 。
作者:Krahets 链接:力扣 来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
方法一:暴力递归
class Solution {
public:
int fibonacci(int n) {
if(n==0) return 0;
if(n==1) return 1;
return fibonacci(n-1)+fibonacci(n-2);
}
};
如上图所示,为暴力递归求斐波那契数 f(5) 形成的二叉树,树中的每个节点代表着执行了一次 fibonacci() 函数,且有:
执行一次 fibonacci() 函数的时间复杂度为 O(1) ; 二叉树节点数为指数级 O(2^n) ; 因此,暴力递归的总体时间复杂度为 O(2^n ) 。此方法效率低下,随着 n 的增长产生指数级爆炸。
方法二:记忆化递归
观察发现,暴力递归中的子问题多数都是重叠子问题,即:
这些重叠子问题产生了大量的递归树节点,其不应被重复计算。实际上,可以在递归中第一次求解子问题时,就将它们保存;后续递归中再次遇到相同子问题时,直接访问内存赋值即可。记忆化递归的代码如下所示。
class Solution {
public:
int fibonacci(int n, vector<int> dp)) {
if(n==0) return 0;
if(n==1) return 1;
if(dp[n]!=0) return dp[n];
return fibonacci(n-1, dp))+fibonacci(n-2, dp));
}
int fibonacciMemoried(int n){
vector<int> dp(n+1,0);
return fibonacci(n, dp));
}
};
方法三:动态规划
递归本质上是基于分治思想的从顶至底的解法。借助记忆化递归思想,可应用动态规划从底至顶求取f(n) ,代码如下所示。
class Solution {
public:
int fibonacci(int n){
if(n==0) return 0;
vector<int> dp(n+1,0);
dp[1]=1;
for(int i=2;i<=n;i++){
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
};
上述动态规划解法借助了一个 dp 数组保存子问题的解,其空间复杂度为 O(N) 。而由于 f(n) 只与 f(n−1) 和 f(n−2) 有关,因此我们可以仅使用两个变量 a , b 交替前进计算即可。此时动态规划的空间复杂度降低至 O(1) ,代码如下所示。
class Solution {
public:
int fibonacci(int n){
int a=0,b=1,sum;
for(int i=0;i<n;i++){
sum=a+b;
a=b;
b=sum;
}
return a;
}
};
小结
记忆化递归和动态规划的本质思想是一致的,是对斐波那契数列定义的不同表现形式:
-
记忆化递归 — 从顶至低: 求 f(n) 需要 f(n−1) 和 f(n−2) ; ⋯ ;求 f(2) 需要 f(1) 和 f(0) ;而 f(1) 和 f(0) 已知;
-
动态规划 — 从底至顶: 将已知 f(0) 和 f(1) 组合得到 f(2) ;⋯ ;将 f(n−2) 和 f(n−1) 组合得到 f(n) ;