递归+记忆化搜索
此文章为简单讲义,详情请移步至主播的主页算法合集:
樱茶喵的个人主页
🔴递归
一.什么是递归?
函数自己调用自己。
二.为什么要用递归?
优点:
-
代码简洁,可读性好
-
可用于某些排序算法(归并)和二叉树的遍历,大大简化代码。
不足:
调用函数的开销很大,每次调用都会在栈上为其分配空间,容易栈溢出(Stack Overflow),也就是我们俗称的爆栈。
例:(伪代码)
(1).斐波那契数
void Fib(int n)
{
if(n == 0 || n == 1)
return n;
else
return Fib(n-2) + Fib(n-1);
}
(2).归并排序
void Merge(int* nums,int left,int right)
{
if(left >= right)
return;
int mid = (left + right)/2;
Merge(nums,left,mid);
Merge(nums,mid+1,right);
合并左右的两个有序数组。
}
(3).二叉树的遍历…
三.怎么理解递归?
本质:
主问题->子问题
子问题->子子问题…
最终将主问题转换为最小子问题,再往上返回。
主问题和子问题、子问题和子问题之间的特点:
都有相同的映射关系f(可以用f来解决所有的子问题)
- 递归展开的细节图(时间复杂度:O(2^n)
- 举例说明(斐波那契数)
宏观看待递归的过程:
- 不要太纠结于递归的细节展开图。
- 把递归过程想成一个黑盒。
- 坚信这个黑盒一定能完成我们赋予他的任务。
四.如何写递归?
1.先找到相同的子问题(函数头)(映射关系)
2.在实现递归过程中只用关心其中一个子问题是如何解决的,因为所有子问题的映射关系是一样的。(函数体的设计)
3.注意递归结束的条件。
🔴记忆化搜索
一.什么是记忆化搜索
记忆化的解释:
就是带备忘录的递归。(容器、数组、哈希表…)
将出现过的子问题的答案存到一个“备忘录”里,之后在调用函数时如果发现该问题已经出现过,则可以在备忘录里找到该问题的答案,直接返回。
二.如何实现记忆化搜索?
1.添加一个备忘录
2.递归每次返回的时候,将结果放到备忘录里面
3.在每次进入递归的时候,往备忘录里面瞅一瞅
斐波那契数举例
class Solution {
public:
int memo[31];//创建备忘录
int fib(int n)
{
memset(memo,-1,sizeof memo);//初始化为-1,因为递归过程中的答案不可能为-1
return dfs(n);
}
int dfs(int n)
{
//"剪枝",判断该子问题是否已出现过,出现则直接返回答案,提升效率最关键的一步
if(memo[n] != -1)
return memo[n];
if(n == 0 || n == 1)
{
memo[n] = n;
return n;
}
memo[n] = dfs(n-1)+dfs(n-2);//将子问题答案存到备忘录中
return memo[n];
}
};
通过画递归的细节图可以发现,细节图的构成类似于二叉树,查找备忘录的过程就是剪枝的过程。在这一番操作之后,使时间复杂度:从O(2^n)转化为O(n),大大提高了运行效率。