递归啊递归,说简单简单,说难难。
首先我们要知道
一、什么是递归?
我们再C语言和数据结构里都用了不少递归,这里就不多详细介绍。
递归简单来说就是函数自己调用自己的情况
二、为什么要用递归呢?
本质来说其实就是我们在解决一个问题后出现相同的问题,解决这个问题后会再出现相同的问题。我们解决这些问题的方式一样,所以就出现了函数自己调用自己。
三、如何加深理解递归?
以下的步骤由浅入深
- 首先,你需要会画递归展开图
- 当你会画递归展开图时,下面要刷刷题,先从简单的二叉树中的题目来刷,这种一般比较容易
- 当上面都完成后,下面我们要做到的就是能够宏观的去看递归的过程,宏观的去看递归的过程需要:
- 不要一味的在意递归的过程
- 尽量把递归函数当作一个黑盒
- 相信这个黑盒的实力
四、如何写好一个递归
- 找到相同的问题(把主问题分解成若干个子问题)--->函数头的设计
- 只需要关心某一个子问题是如何解决得---->函数体的书写
- 注意递归函数的出口,避免死循环
五、题目实战
1,经典汉诺塔问题
面试题 08.06. 汉诺塔问题 - 力扣(LeetCode)
这道题属于是我们接触递归后解决的第一道问题,很多人做完汉诺塔后,知道了汉诺塔要用递归解决,那么你知道为什么汉诺塔要用递归吗?
1)如何解决汉诺塔问题
我们可以先假设,三个柱子为A、B、C,盘子数量问n,盘子开始在A上,我们需要把它放到C上。
当n=1时 我们只需要一次操作,把A上的盘子转移到C上。
当n=2时
我们需要把盘子全部转移到C上,所以我们需要先把大的盘子转移到C上,所以我们需要先把上面的盘子转移到B上,把A上最大的盘子转移到C上
当n=3时,此时A上右三个盘子,
我们要把A上的盘子全部移到C上,所以我们需要把A上最大的盘子先移动到C,所以我们需要把A上较小的两个盘子移到B上,这时我们可以把它转化成n=2来解,此时的C是B,B是C。
当我们想出这个思路时,我们的递归思路是不是来了呢?
2)为什么呢用递归
所以我们为什么要用递归呢?
因为我们解决这个汉诺塔问题时,我们可以把它分解成若干的子问题。
3)如何编写递归代码?
1.重复子问题->函数头
将x柱上的n个盘子,借助y柱,转移到z柱上------>void dfs(x,y,z,n)
2.只关心某一个子问题在做什么->函数体
我们只需要先移动上n-1个,即调用dfs(x,z,y,n-1),然后x------>z,然后dfs(y,x,z,n-1)
3.递归的出口
n=1时,x------ >z
4) 代码解决
cpp
class Solution {
public:
void hanota(vector<int>& A, vector<int>& B, vector<int>& C)
{
int n = A.size();
dfs(n, A, B, C);
}
void dfs(int n, vector<int>& A, vector<int>& B, vector<int>& C)
{
if (n == 1)
{
C.push_back(A.back());
A.pop_back();
return;
}
dfs(n-1, A, C, B); // 将A上面n-1个通过C移到B
C.push_back(A.back()); // 将A最后一个移到C
A.pop_back(); // 这时,A空了
dfs(n-1, B, A, C); // 将B上面n-1个通过空的A移到C
}
};
如果对于代码有疑问的可以自己画画递归展开图
2.合并两个有序链表
1)题意解析
2)算法原理
解法:递归------>重复子问题
1)重复子问题------>函数头的设计
这个很明显,就是合并两个有序链表------>Node*dfs(l1,l2)
2)只关心某一个子问题在做什么->函数体
1.比大小
2.(假设l1较小)l1->next=dfs(l1->next,l2)
3.return l1
3)递归的出口
3)代码解决
cpp
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
if (l1 == nullptr) {
return l2;
} else if (l2 == nullptr) {
return l1;
} else if (l1->val < l2->val) {
l1->next = mergeTwoLists(l1->next, l2);
return l1;
} else {
l2->next = mergeTwoLists(l1, l2->next);
return l2;
}
}
};
本题结束。
4)小总结
1)迭代(循环)和递归
它们都能解决重复的子问题,所以我们一般的代码都能递归改循环,循环改递归。
但是为什么有些代码递归改循环麻烦,有些代码循环改递归麻烦呢?
这个问题我们可以像看一下这个例子,比如汉诺塔问题吧,我们不是画过递归展开图吗?不少人肯定发现了,我们没的递归展开图和树的深度优先搜索极为相似。
可以这么说,递归展开图,其实就是对一棵树做一次深度优先遍历(dfs)
所以,我们可以得出当递归展开图,如上图,比较复杂时,这个时候递归改循环就比较困难!
那么什么时候递归改循环简单呢?我们根据上面的结论可以反推出来,当递归展开图简单时,递归改循环简单!如下图:
我们也可以举一个实例,比如遍历数组
假设我们有一个num数组
我们用循环写一个遍历并打印
cpp
for(int i=0;i<num.size();++i)
{
cout<<num[i[<<" ";
}
我们用递归写个函数呢?
cpp
void dfs(vector<int>& num,int i)
{
if(i==num.size())
return;
cout<<num[i]<<" ";
dfs(num,i+1)
}
3.反转链表
1)题意解析
2)算法原理
解法:递归------>重复子问题
本题我们可以从两个视角进行解决:
视角一:从宏观看待