手撕数据结构算法-斐波那契数列-带备忘录的递归解法

901 阅读3分钟

明确了问题,其实就已经把问题解决了⼀半。即然耗时的原因是重复计算, 那么我们可以造⼀个「备忘录」,每次算出某个⼦问题的答案后别急着返 回,先记到「备忘录」⾥再返回;每次遇到⼀个⼦问题先去「备忘录」⾥查 ⼀查,如果发现之前已经解决过这个问题了,直接把答案拿出来⽤,不要再 耗时去计算了。 ⼀般使⽤⼀个数组充当这个「备忘录」,当然你也可以使⽤哈希表(字 典),思想都是⼀样的。

int fib(int N) { 
if (N < 1) return 0; 
// 备忘录全初始化为 0 vector<int> memo(N + 1, 0); 
// 初始化最简情况 return helper(memo, N); 
}
int helper(vector<int>& memo, int n) { 
// base case if (n == 1 || n == 2) return 1; 
// 已经计算过 if (memo[n] != 0) return memo[n]; 
memo[n] = helper(memo, n - 1) + 
       helper(memo, n - 2); return memo[n]; 
}

现在,画出递归树,你就知道「备忘录」到底做了什么。

实际上,带「备忘录」的递归算法,把⼀棵存在巨量冗余的递归树通过「剪 枝」,改造成了⼀幅不存在冗余的递归图,极⼤减少了⼦问题(即递归图中 节点)的个数。
如需获取完整数据结构算法、左神算法视频课程,请点击:数据结构算法课程

递归算法的时间复杂度怎么算?⼦问题个数乘以解决⼀个⼦问题需要的时间。

⼦问题个数,即图中节点的总数,由于本算法不存在冗余计算,⼦问题就是 f(1) , f(2) , f(3) ... f(20) ,数量和输⼊规模 n = 20 成正⽐,所以⼦问 题个数为 O(n)。

解决⼀个⼦问题的时间,同上,没有什么循环,时间为 O(1)。 所以,本算法的时间复杂度是 O(n)。⽐起暴⼒算法,是降维打击。 ⾄此,带备忘录的递归解法的效率已经和迭代的动态规划解法⼀样了。

实际 上,这种解法和迭代的动态规划已经差不多了,只不过这种⽅法叫做「⾃顶 向下」,动态规划叫做「⾃底向上」。 啥叫「⾃顶向下」?注意我们刚才画的递归树(或者说图),是从上向下延 伸,都是从⼀个规模较⼤的原问题⽐如说 f(20) ,向下逐渐分解规模,直 到 f(1) 和 f(2) 触底,然后逐层返回答案,这就叫「⾃顶向下」。

啥叫「⾃底向上」?反过来,我们直接从最底下,最简单,问题规模最⼩的 f(1) 和 f(2) 开始往上推,直到推到我们想要的答案 f(20) ,这就是动 态规划的思路,这也是为什么动态规划⼀般都脱离了递归,⽽是由循环迭代 完成计算。

今天就讲到这里,如需完整就Java数据结构左神算法课程,请点这里:java数据结构算法视频教程