剑指 Offer 10- I. 斐波那契数列 (从重叠子问题到备忘录到dp数组迭代解法)

191 阅读3分钟

目录

题目描述

写一个函数,输入 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:动态规划详解(修订版)