文章目录
- 递归算法(上)
- 前言:算法思路
- 一、力扣[21. 合并两个有序链表](https://leetcode.cn/problems/merge-two-sorted-lists/)
- 二、力扣[206. 反转链表](https://leetcode.cn/problems/reverse-linked-list/)
- 三、力扣[24. 两两交换链表中的节点](https://leetcode.cn/problems/swap-nodes-in-pairs/)
- 四、力扣[50. Pow(x, n)](https://leetcode.cn/problems/powx-n/)(快速幂)
- 五、力扣[面试题 08.06. 汉诺塔问题](https://leetcode.cn/problems/hanota-lcci/)
递归算法(上)
前言:算法思路
-
重复子问题->函数头的设计
-
只关心某一个子问题在做什么事情->函数体的事情
-
递归的出口
**【注意】:**无条件相信自己的函数体一定能成功,不要深究
思考1:什么时候循环舒服,什么时候递归舒服?
- 如果递归的展开是单分支,就用循环,如果是多叉树,就用递归
思考2:递归vs深搜
- 递归的展开图就是对一棵树做一次深度优先遍历(dfs)。
一、力扣21. 合并两个有序链表
解法:
- 基本情况处理:
- 如果链表
l1
为空(l1 == nullptr
),则直接返回链表l2
作为合并后的链表。 - 如果链表
l2
为空(l2 == nullptr
),则直接返回链表l1
作为合并后的链表。
- 如果链表
- 递归调用:
- 比较
l1
和l2
当前节点的值。 - 如果
l1->val <= l2->val
,说明l1
当前节点的值应该在新链表的前面。此时,递归地调用mergeTwoLists
函数,将l1->next
和l2
作为参数,并将返回的节点设置为l1->next
。然后返回l1
,因为l1
当前节点是新链表的头节点或某个子链表的头节点。 - 如果
l1->val > l2->val
,则执行与上述相反的操作,即将l2
当前节点放在新链表的前面,并递归地处理剩余部分。
- 比较
- 合并过程:
- 在每次递归调用中,都会确定当前两个链表中哪个节点的值应该在新链表的前面。
- 通过递归,这个过程会一直进行到两个链表中的至少一个为空。
- 递归返回时,每个节点的
next
指针都会被正确设置,指向合并后链表的下一个节点。
- 返回结果:
- 最终,当递归调用达到基本情况时,会返回一个非空的链表头节点,这个节点是合并后链表的头节点。
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
if(l1 == nullptr) return l2;
if(l2 == nullptr) return l1;
if(l1->val <= l2->val){
l1->next = mergeTwoLists(l1->next, l2);
return l1;
}
else{
l2->next = mergeTwoLists(l1, l2->next);
return l2;
}
}
};
二、力扣206. 反转链表
视角1:从宏观角度看待问题
- 让当前结点后面的链表先逆置,并且把头结点返回
- 让当前结点添加到逆置后的链表后面即可
视角2:将链表看成一棵树
- 仅需做一次dfs即可
- 后序遍历找到叶子结点为止,然后结合视角1再思考
解法:
- 基本情况处理:
- 如果链表为空(
head == nullptr
)或者链表中只有一个节点(head->next == nullptr
),则无需进行反转,直接返回原链表头节点head
。
- 如果链表为空(
- 递归调用:
- 递归地处理链表的剩余部分,即从
head->next
开始的链表。这里,reverseList(head->next)
会返回反转后链表的头节点,我们将其存储在newnode
变量中。
- 递归地处理链表的剩余部分,即从
- 节点反转:
- 在当前递归层级,我们需要将
head
节点与head->next
节点(现在已经是反转后链表的一部分)进行“局部”反转。 - 将
head->next->next
设置为head
,这样head
节点就被插入到了反转后链表的头部(但实际上,由于递归还未返回,这部分链表在递归栈中还未完全构建完成)。 - 将
head->next
设置为nullptr
,因为head
现在是反转后链表的最后一个节点。
- 在当前递归层级,我们需要将
- 返回结果:
- 返回
newnode
,即反转后链表的头节点。这个节点是递归调用返回的,代表了除当前head
节点外,已经反转完成的链表部分。
- 返回
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(head == nullptr || head->next == nullptr) return head;
ListNode* newnode = reverseList(head->next);
head->next->next = head;
head->next = nullptr;
return newnode;
}
};
三、力扣24. 两两交换链表中的节点
解法:
- 基本情况处理:
- 如果链表为空(
head == nullptr
)或者链表中只有一个节点(head->next == nullptr
),则无需进行任何交换,直接返回原链表头节点head
。
- 如果链表为空(
- 递归调用:
- 递归地处理从链表的第三个节点开始的剩余部分。这里使用
head->next->next
作为递归调用的参数,因为前两个节点(head
和head->next
)将在当前递归层级中被交换。 - 递归调用的结果存储在
tmp
变量中,它代表了交换后的剩余链表的头节点。
- 递归地处理从链表的第三个节点开始的剩余部分。这里使用
- 节点交换:
ret
变量被设置为head->next
,即交换后的新链表的头节点(因为head
和head->next
交换位置后,head->next
将成为新的头节点)。- 接下来,将
head->next->next
指向head
,完成两个节点的交换。 - 最后,将
head->next
设置为tmp
,即将交换后的剩余链表连接到已经交换好的前两个节点之后。
- 返回结果:
- 返回
ret
,即交换后的新链表的头节点。
- 返回
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
if(head == nullptr || head->next == nullptr) return head;
ListNode* tmp = swapPairs(head->next->next);
ListNode* ret = head->next;
head->next->next = head;
head->next = tmp;
return ret;
}
};
四、力扣50. Pow(x, n)(快速幂)
细节问题:
- 递归:
pow
函数通过递归的方式实现了幂的计算,每次递归都将问题的规模减半(计算n/2
次幂),从而提高了效率。 - 负指数处理:
myPow
函数通过检查n
的符号,并相应地调整计算方式,来处理负指数的情况。 - 类型转换:在处理负指数时,将
n
转换为long long
类型,以避免在取负时可能发生的整数溢出。
解法:
myPow
函数
- 如果
n
大于 0,直接调用pow
函数计算x
的n
次幂。 - 如果
n
小于或等于 0,将n
转换为正数(通过取负并转换为long long
类型以避免整数溢出),然后计算1
除以x
的-n
次幂,以此处理负指数的情况。
pow
函数
- 如果
n
等于 0,根据幂的定义,任何数的 0 次幂都是 1,所以直接返回 1。 - 使用递归的方式计算幂。首先计算x的n/2次幂,存储在
tmp
中。- 如果
n
是偶数,那么x^n = (x^(n/2))^2
,即tmp * tmp
。 - 如果
n
是奇数,那么x^n = x * (x^(n/2))^2
,即tmp * tmp * x
。
- 如果
代码:
class Solution {
public:
double myPow(double x, int n) {
return n > 0? pow(x, n): 1/pow(x, -(long long)n);
}
double pow(double x, long long n){
if(n == 0) return 1;
double tmp = pow(x, n/2);
return n % 2 == 0? tmp * tmp: tmp * tmp * x;
}
};
五、力扣面试题 08.06. 汉诺塔问题
解法:
-
重复子问题->函数头
- 将x柱子上的n个盘子,借助y柱子,转移到z柱子上去——
void dfs(x, y, z, n);
- 将x柱子上的n个盘子,借助y柱子,转移到z柱子上去——
-
只关心某一个子问题在做什么事情->函数体的事情
-
-
将x上的n-1个盘子,借助z柱子,转移到y柱子上去——
dfs(x, z, y, n-1);
-
将x上剩余的最大的盘子转移到z柱子上去
-
再将y柱子上的n-1个盘子,借助x柱子,转移到z柱子上去——
dfs(y, x, z, n-1);
-
-
-
递归的出口
- 当n = 1时,将x的最后一个盘子移到z柱子上去即可
代码:
class Solution {
public:
void dfs(vector<int>& x, vector<int>& y, vector<int>& z, int n){
if(n == 1){
z.push_back(x.back());
x.pop_back(); // 不要忘记清除x柱子上已经转移的盘子
return;
}
dfs(x, z, y, n - 1);
z.push_back(x.back());
x.pop_back();
dfs(y, x, z, n - 1);
}
void hanota(vector<int>& x, vector<int>& y, vector<int>& z) {
int n = x.size();
dfs(x, y, z, n);
}
};