目录
- 题目描述
- 1、暴力递归法的重叠子问题
- 2、备忘录解法
- 3、dp数组迭代算法
- 4、滚动数组优化
- 5、参考链接
题目描述
写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项。斐波那契数列的定义如下:
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
1、暴力递归法的重叠子问题
暴力递归法最为常见,但是同时它的时间复杂度也是最高的,附带了许多重复计算。
class Solution {
public:int fib(int n) {if(n==0) return 0;else if(n == 1 || n == 2) return 1;else return (fib(n - 1) + fib(n - 2))%1000000007;}
};
画出递归树:
算法时间复杂度为递归二叉树结点总数,为O(2^n)。f(18)、f(17)被重复计算了,并且以f(18)为根节点的递归树体积也是十分巨大的,如果再算一遍会耗费大量的时间。
这个问题性质我们可以描述为“重叠子问题”。
2、备忘录解法
既然是重复计算的问题,我们就可以构造一个备忘录。
每次计算出某个子问题的答案先别着急返回,先记到备忘录中再返回;
每次遇到一个子问题,先去备忘录中查找,如果已经解决了这个问题,就直接把答案拿过来用,不再进行计算。
class Solution {
public:int search_helperTab(vector<int >& helperTab,int n){//n较小的直接返回if(n == 1 || n == 2) return 1;//如果已经计算过了,直接返回计算过的值if(helperTab[n] != 0) return helperTab[n];//如果没有计算过,则需要重新计算一遍else{helperTab[n] = (search_helperTab(helperTab,n-1) + search_helperTab(helperTab,n-2))%1000000007;}return helperTab[n];}int fib(int n) {if(n==0) return 0;//构建一个备忘录vector<int >helperTab(n+1,0);return search_helperTab(helperTab,n);}
};
带备忘录的递归算法,将一颗存在巨量冗余的递归树剪枝为没有冗余的递归图。
递归算法时间复杂度=子问题个数 * 解决子问题所需要的时间。
由于不存在冗余计算,所以子问题个数为O(n);解决一个子问题的时间是O(1);
所以本算法的时间复杂度是O(n)。
注意,我们刚刚画的递归树是从上向下延伸的,都是从一个规模较大的原问题,向下逐渐分解规模,直到触底(f(1)、f(2)),然后逐层返回答案,这就是自顶向下。
如果直接从最底下的最小规模的f(1)、f(2)开始往上推导,直到f(20),这就是动态规划的思路。
3、dp数组迭代算法
class Solution {
public:int fib(int n) {if(n==0) return 0;if(n == 1 || n == 2) return 1;//构建一个备忘录vector<int >dp(n+1,0);dp[1]=dp[2]=1;for(int i = 3;i <= n;i++)dp[i]=(dp[i-1]+dp[i-2])%1000000007;return dp[n];}
};
4、滚动数组优化
状态方程中的当前状态只由前两个状态决定,所以不需要一个数组进行存放。
class Solution {
public:int fib(int n) {if(n==0) return 0;if(n == 1 || n == 2) return 1;int pre=1,curr=1;for(int i = 3;i <= n;i++){int sum=(pre+curr)%1000000007;pre=curr;curr=sum;}return curr;}
};
这样空间复杂度就降到O(1)了。
5、参考链接
剑指 Offer 10- I. 斐波那契数列
labuladong:动态规划详解(修订版)