目录
题目描述
写一个函数,输入 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)了。