方法
1. 二分法
代码随想录对应题目链接
数组是有序数组,是使用二分查找的基础条件。
以后大家只要看到面试题里给出的数组是有序数组,都可以想一想是否可以使用二分法。或者说可以转换在一个有序数组中找一个数的题目。
同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的。
2. 双指针
代码随想录对应题目链接
双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
对于使用双指针的题目,需要明确双指针的含义,针对其含义去思考题解方式。
双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组、链表、字符串等操作的面试题,都使用双指针法。
3. 滑动窗口
代码随想录对应题目链接
关于滑动窗口的模板看这篇文章
针对题目类型:
在一个数组里(注意,一个字符串也是数组,而且滑动窗口大部分应用于字符串),寻找连续子数组满足某些要求。前面的两个链接很好的说明了这句话!!
难点:
参照模板,其难点是如何编写缩短窗口的逻辑,这是能不能完成这类题目的关键,这部分解决了,题目也就解决了。
本质:
- 和双指针题目类似,更像双指针的升级版,滑动窗口核心点是维护一个窗口集,根据窗口集来进行处理
- 核心步骤
- right 右移
- 收缩
- left 右移
- 求结果
4. 链表
代码随想录对应知识
绝大数题目用双指针求解;
很多时候,加虚拟头节点能带来很多方便。
5. 哈希表
代码随想录对应知识
用来记录是否出现和查找。只需要就元素用unorder_set;如果还要记录每个元素的其他信息,比如个数或者分类下的元素用unordered_map;如果,元素类型有限,可以用数组代替,节省时间和空间。
所以,很多题目需要留意,是否可以转化为寻找元素,如代码随想录里的这类题。
6. 栈与队列
[代码随想录对应知识]
针对数组和匹配问题!
还要会用:单调队列和优先级队列。关于优先级队列的一些问题,我还写了一篇文章解释。
所谓单调队列,即单调递减或单调递增的队列。C++中没有直接支持单调队列,需要我们自己来一个单调队列。
(所谓单调栈,通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。有专门的题型)。
7. 二叉树
遍历方式
让递归不再迷路
三个步骤:
- 明确函数的参数与返回值。
- 参数可能做不到一开始就确定下来。
- 对于需要搜索二叉树的整个范围的一般需要返回特定的值,用于告诉搜索的怎么样了;只是搜素二叉树的一条路径一般不需要返回值,只需要用一个变量记录结果,这个变量可能是全局变量也可能作为函数参数,当结果满足时就可以返回了。
- 代码随想录关于返回值的结论:
再来看返回值,递归函数什么时候需要返回值?什么时候不需要返回值?这里总结如下三点:
如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。(这种情况就是本文下半部分介绍的113.路径总和ii)
如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。 (这种情况我们在 236. 二叉树的最近公共祖先 中介绍)
如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。(112. 路径总和)
- 明确递归的结束条件。
- 这个必须一开始就想清楚,不然结果必不对,甚至死循环。
- 明确递归函数体处理逻辑。
- 是不是需要回溯、前中后序的哪个以及对应的处理方法,需要怎么处理返回值(如果有)、是在一棵树上处理还是两棵树(两个子树可以认为是两棵树)。
把三个步骤想明白,就代表理解题目要求解决的问题是什么以及解决方式是什么,对应的递归思路就清晰了,问题的结果也就知道了。
高度与深度
求高度用后序遍历;
求深度用前序遍历。
操作两棵树
迭代法中,一般一起操作两个树都是使用队列模拟类似层序遍历,同时处理两个树的节点,这种方式最好理解,如果用模拟递归的思路的话,要复杂一些。(代码随想录题目)
二叉搜索树
- 二叉搜索树的中序遍历是升序数组;
- 二叉搜索树的题解一般需要记录前一个节点;
- 二叉搜索树的迭代遍历特别简单:
class Solution { public: TreeNode* searchBST(TreeNode* root, int val) { while (root != NULL) { if (root->val > val) root = root->left; else if (root->val < val) root = root->right; else return root; } return NULL; } };
关于后序遍历与回溯
二叉树回溯的过程就是从低到上。后序遍历就是天然的回溯过程,最先处理的一定是叶子节点。
具体内容通过代码随想录这道题学习。
总结
8. 回溯
几个概念
- 回溯是一种搜索方式;
- 回溯的本质是穷举,穷举所有可能(可以优化搜索过程但本质思想不变,只是剪枝操作),选择符合的答案;
- 回溯函数是一个递归函数(函数递归调用);
- 解决的问题类型:
组合问题:N个数里面按一定规则找出k个数的集合
切割问题:一个字符串按一定规则有几种切割方式
子集问题:一个N个数的集合里有多少符合条件的子集
排列问题:N个数按一定规则全排列,有几种排列方式
棋盘问题:N皇后,解数独等等 - 回溯法解决的问题都可以抽象为树形结构:
这一点对于理解整个过程很关键。
因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度。
回溯函数模板
因为回溯函数是递归函数,而且回溯问题都可抽象为树形结构,所以基本上递归的思想与二叉树的递归处理一致,采用三个步骤确定回溯函数,其中不同点如下:
- 返回值一般为void;
- 参数的确定比二叉树的难一些,一般需要根据函数体逻辑确定参数;
- 什么时候达到了终止条件,树中就可以看出,一般来说搜到叶子节点了,也就找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归。
- 函数主体的for循环就是遍历集合区间,可以理解一个节点有多少个孩子,这个for循环就执行多少次。
回溯算法模板框架:
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
去重问题
关于去重,看代码随想录这篇文章。
- 同一树枝去重:使用一个全局的used数组,记录某一位是否访问过;
- 同一层树节点去重,又分子集为和排列:
-
子集问题一定要排列,一定要排列,一定要排列!!!有以下三种:
使用一个uset哈希表(有些可以用数组作为哈希表)去重:
unordered_set<int> uset; for (int i = startIndex; i < nums.size(); i++) { if (uset.find(nums[i]) != uset.end()) { continue; }
还可以用:
if (i > startIndex && candidates[i] == candidates[i - 1]) { continue; }
或者,使用全局的used数组(前面的uset的是局部的,只对单个for有效)
// used[i - 1] == true,说明同一树枝candidates[i - 1]使用过 // used[i - 1] == false,说明同一树层candidates[i - 1]使用过 // 要对同一树层使用过的元素进行跳过 if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) { continue; }
-
排列有以下两种:
使用一个uset哈希表(有些可以用数组作为哈希表)去重:
unordered_set<int> uset; for (int i = startIndex; i < nums.size(); i++) { if (uset.find(nums[i]) != uset.end()) { continue; }
对于有序数组,还可以用,
// used[i - 1] == true,说明同一树枝candidates[i - 1]使用过 // used[i - 1] == false,说明同一树层candidates[i - 1]使用过 // 要对同一树层使用过的元素进行跳过 if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) { continue; }
-
记忆化回溯
139.单词拆分 代码随想录题解
337.打家劫舍 III 代码随想录题解
9. 贪心算法
本质
贪心的本质是选择每一阶段的局部最优,从而达到全局最优。
一般解题步骤
首先明确:贪心算法并没有固定的套路。 最好用的策略就是举反例,如果想不到反例,那么就试一试贪心吧。
贪心算法一般分为如下四步:
- 将问题分解为若干个子问题
- 找出适合的贪心策略
- 求解每一个子问题的最优解
- 将局部最优解堆叠成全局最优解
其实这个分的有点细了,真正做题的时候很难分出这么详细的解题步骤,可能就是因为贪心的题目往往还和其他方面的知识混在一起。
题型总结
1. 数据上下浮动
- 考虑画折线图;
- 考虑用贪心;
- 考虑用动规;
题型:
贪心&&动规 376. 摆动序列 :代码随想录题解
贪心&&动规 122.买卖股票的最佳时机II:代码随想录题解
2. 子序列
回溯算法 491.递增子序列:代码随想录题解
贪心&&动规 376. 摆动序列:代码随想录题解
贪心&&动规 53. 最大子数组和:代码随想录题解(这道题也可回溯,也就是暴力解,但时间复杂度太高了(2n))
动规 300. 最长递增子序列:代码随想录题解
贪心&动规 674. 最长连续递增序列:代码随想录题解
动规 718. 最长重复子数组:代码随想录题解
动规 1143. 最长公共子序列 :代码随想录题解
动规 1035. 不相交的线:代码随想录题解
贪心(双指针)&&动规 392. 判断子序列:代码随想录题解:这道题与1143. 最长公共子序列是几乎一致的问题,区别在于,这道题的子序列指定为其中一个序列,这个的不同也让动规的递推关系有了小小的变化。
3. 一个位置两个状态
-
某些题,一个位置会有两个状态可选,考虑用动规分别考虑(也可以思考下贪心的方法);
-
定义dp[i][0]与dp[i][1]表示两种状态;
-
如果状态只跟前一个有关,状态转移直接通过前一个状态更新:
- 贪心&&动规 122.买卖股票的最佳时机II:代码随想录题解
-
如果状态跟前面的状态有关,且不知道那个是最优的,此时状态转移就需要从开始遍历到当前位置选取最优:
4. 一个位置确定两个维度
当一个数据存在两个维度需要满足条件时,两个维度可以是两种描述性要求(例题1),也可以是直接通过一个二维数组表达的数据(例题2),这时要想到先贪心确定一个维度后,再贪心确定另一个维度,如果同时考虑两个维度,会绕进去,顾此失彼。
一般来说,第一位维度很好确定,第二个维度需要结合题目要求确定方法。
- 贪心 135. 分发糖果:代码随想录题解:两个维度是题目的两个要求,一个是位置得满足左右两侧数量关系的两个维度。
- 贪心 406.根据身高重建队列:代码随想录题解:两个维度,一个是身高维度,一个是大于等于的数量维度。
- 贪心 452. 用最少数量的箭引爆气球:代码随想录题解:两个维度,,先确定直径的左值的维度要求,再确定右值满足同个区间尽可能包含多的气球的维度要求。
5. 编辑距离
编辑距离又称作Levenshtein距离,是指两个字符串之见由一个字符串转换成另一个字符串所需的最少操作次数.通常来说,比较两个字符串,将一个字符串修改成另一个字符串共有三个基本操作:增,删,改.其中增就是增加某个元素,删就是去掉某个元素,而改相当于将元素a替换成元素b.理论上,两个长度相等的字符串都可以通过"改"这个操作来实现,而长度不相等的时候就必须要有增或者删操作了.如果不考虑最少操作次数的话.对于任何一个字符串s1和s2,最多操作max{len(s1),
len(s2)}次.我们举个栗子.如果strs1 = “horse”, strs2 = “deer”,
我们不管三七二十一,直接做这样的操作:
可见此时做了五次操作,而这恰恰是字符串"horse"的长度,反过来也是一样的.但是题目要求的是最少的操作次数,我们再看一个例子,假如str1 = “love”, str2 = “life”.如果采用暴力转换,那么无疑要经过四次变换.但我们明显可以看出str1和str2有着相同位置的共同元素(“l"和"e”).我们可以保留这两个位置的元素而替换另外两个位置的元素.所以我们这里love到life的编辑距离就是2.
见代码随想录总结篇
6. 单调栈
单调栈有一个重要的性质:
约定顺序为从栈底到栈顶。
-
栈里元素为递增的单调递增栈:
当遇到一个元素比栈顶的元素小时,假设要求是以每个一元素为中心求最大面积时,此时大家应该可以发现其实就是栈顶和栈顶的上一个元素以及要入栈的三个元素组成了我们要求最大面积的高度和宽度。
参考题目:84.柱状图中最大的矩形,代码随想录题解。 -
栈里元素为递减的单调递减栈:
同样当遇到一个元素比栈顶的元素高时,此时就出现凹槽了,栈顶元素就是凹槽底部的柱子,栈顶的上一个元素就是凹槽左边的柱子,而添加的元素就是凹槽右边的柱子。
参考题目:42. 接雨水,代码随想录题解。