文章目录
1. 递归
我们这里重点在于递归思想和解题的重心,即利用递归解决一系列算法题。
1.1 什么是递归
我们在学习数据结构 / 做算法题时,如二叉树、快排,归并等,会使用递归写代码。
我们知道:递归就是函数调用自身的情况。
而我们在写一个递归函数时,分为两个部分:终止条件(函数出口)和递归体
1.2 何时使用递归
递归的本质 可以理解为:对于一个主问题,其可以分为多个相同的子问题,而每个子问题也可以继续分为相同的子问题(递归是一种通过将问题分解为更小规模的相同问题来解决主问题的方法),
因此在我们遇到这类问题时,可以尝试使用递归去解题。
1.3 如何上手理解递归
- 递归展开 :最直接的理解方法,我们画出递归过程的展开图,将跟着函数递归过程走
- 递归树 :我们可以将递归的执行过程表示为一棵递归树。每个节点代表一个子问题,递归的深度表示问题规模的减小,而叶子节点表示基本情况的出现,即递归的终止条件。
- 宏观理解 :
- 不在意递归展开图:忽略展开过程
- 将递归的函数看作是一个黑盒
- 该黑盒一定可以完成所需的功能
1.4 写递归代码的重点
- 问题分解 :我们知道:递归是将一个大问题不断地分解成更小的子问题来解决。通过将问题分解成相同的子问题,可以得到函数头(如何定义这个函数)
- 主要关注 子问题如何解决如何编写(函数体)
- 注意 递归函数的终止条件(函数出口)
2. 题目 - 汉诺塔
面试题 08.06. 汉诺塔问题
思路
- 根据题目,我们首先直接分析当n=1/2/3时的情况,我们有如下信息:
- 当n=2,将A柱上的盘子通过B柱放到C柱
- 当n=3,将A柱上n-1个盘子通过C放到B柱(然后将A上最后一个盘放到C上),将B柱上n个盘子通过A放到C柱
- 当n=4, 5… 会重复类似的步骤
我们可以将上面的子问题看作:将X柱的盘子通过Y柱放到Z柱
根据上面的思路,我们进行代码的编写:
代码
class Solution {
public:
void hanota(vector<int>& A, vector<int>& B, vector<int>& C) {
dfs(A, B, C, A.size());
}
void dfs(vector<int>& a, vector<int>& b, vector<int>& c, int n) {
if(n == 1) // 退出条件 : 将a上最后一个盘子加到c中
{
c.push_back(a.back());
a.pop_back();
return;
}
// 左柱借助右柱将n-1个盘子到中柱
dfs(a, c, b, n-1);
// 执行移动盘子
c.push_back(a.back());
a.pop_back();
// 中柱借助左柱将n-1盘子到右柱
dfs(b, a, c, n-1);
}
};
3. 利用递归解算法题
88.合并两个有序数组
思路
- 题意分析:题目要求返回合并后的两个有序链表的新头节点。而对于合并过程,实际上就是每次比较两链表的节点并加入到新链表的过程,即 重复子问题 。可以考虑使用递归解题:
- 解法:递归 + 宏观理解
- 函数头:dfs用于将两节点进行比较,并返回新节点Node* dfs(Node* l1, Node* l2)
- 函数体:比较两节点的值,并进一步dfs
- 如果l1 < l2,则对l1->next, l2 进行dfs,并返回新节点l1
- 如果l2 < l1,则对l1, l2->next进行dfs,并返回新节点l2
- 函数出口:
- 即递归出口(结束条件),当两链表任意一方结束时,由于是有序链表,直接返回另一个链表的节点
代码
class Solution {
public:
ListNode* dfs(ListNode* l1, ListNode* l2) {
if(l1 == nullptr) return l2; // 函数出口
if(l2 == nullptr) return l1;
// 每次取一个做头节点
if(l1->val < l2->val)
{
l1->next = dfs(l1->next, l2);
return l1;
}
else
{
l2->next = dfs(l1, l2->next);
return l2;
}
}
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
return dfs(list1, list2);
}
};
206. 反转链表
思路
- 题意分析:题目要求反转链表并返回新链表头节点,实际上可以转化为重复多次指针朝向更改。
- 解法:递归 + 宏观理解
- 宏观理解:reverseList本身就是反转链表,只需要进行reverseList(head->next)的操作,再完成指针的反转即可。
- 函数头:即reverse函数本身
- 函数体:递归找到最后一位,并反转指针
- 函数出口:即递归出口,当当前节点为空时向上返回。
- 宏观理解:reverseList本身就是反转链表,只需要进行reverseList(head->next)的操作,再完成指针的反转即可。
代码
ListNode* reverseList(ListNode* head) {
if(!head || !head->next) return head;
ListNode* newHead = reverseList(head->next); // 找到叶子节点前一位,开始逆置
// 将next节点指向该节点,该节点指向空
// 随后向上返回
head->next->next = head;
head->next = nullptr;
return newHead;
}
24.两两交换链表中的节点
思路
-
题意分析:即两两交换链表节点,并返回最终头节点。
-
根据图中思路,两两交换节点后,新头节点应该是两节点中的右侧节点
- 我们首先记录新头节点,后更改指针朝向,将新头节点的next指向下一位的新头节点。
- 函数的返回结果就是每次的新头节点,利用递归即
tmp = swapPairs(head->next->next)
,即交换后面的节点并每次链接新头节点
- 函数出口:当当前节点为空时
代码
ListNode* swapPairs(ListNode* head) {
if(head == nullptr || head->next == nullptr)
return head;
// 递归
auto tmp = swapPairs(head->next->next); // 翻转后的前一位节点
auto ret = head->next; // 新头节点
head->next->next = head; // 更改指针指向
head->next = tmp;
return ret;
}
50.Pow(x,n)
思路
-
题意分析:题目要求实现pow(x, n)用于实现x的n次幂
-
对 偶数与奇数次幂 进行分析:
-
通过上面的分析,我们知道:
- 如果是 偶数次幂 ,只需要将x^(n/2) 相乘即可
- 如果出现 奇数幂 ,需要将x^(n/2)相乘后再乘x
以此实现函数:
- 函数头:
double _myPow(double x, long long n)
,不用解释 - 函数体:
- 当n为负数时,手动处理,即归正n,并令x = 1/x
- 递归(x, n/2),在返回值时进行奇偶判断
- 函数出口:当n为0时,返回1。
代码
class Solution {
public:
double myPow(double x, int n) {
return _myPow(x, (long long)n);
}
double _myPow(double x, long long n) {
if(n == 0) return 1;
if(n < 0) // n为负数,负数指数的幂可以通过对正数指数的幂取倒数得到
{
n = -n;
x = 1 / x;
}
// 偶数:x^n = x^(n/2) * n^(n/2)
// 奇数:x^n = x^(n/2) * n^(n/2) * x
double tmp = _myPow(x, n / 2);
return n % 2 == 0 ? tmp * tmp : tmp * tmp * x;
}
};