笔者目前正在刷力扣平台算法题,本文总结一下笔者在刷题过程中自己悟出来的代码模板,持续更新中…
二分算法-红蓝染色法:
(1)二分查找模板:
//开区间写法最为简洁,最不易出错,体现在以下几个方面:
//1.无需担心死循环的问题(如果真的出现死循环,说明是思路有问题,请不要怀疑是开区间的问题)
//2.染色时,不需要考虑mid+1,-1的问题,大大简化了染色过程需要思考的逻辑,只需把握好循环不变量即可,不必再考虑区间的开闭
//3.循环不变量的边界对称而简洁,易于记忆
class Solution {
public:
int binarySearch(vector<int>& nums, int target){
//注意1:开区间设定初始边界的时候,要注意左边界-1,右边界+1
int left = 左边界-1, right = 右边界+1;
//注意2:开区间的循环条件:left + 1 < right
while(left + 1 < right){
int mid = left + (right - left) / 2;
//注意3:把握好循环不变量将很有助于分析最终的二分答案
//循环不变量:
//未确定区间为(left, right)
//nums[left] 以及left左侧元素满足某一条件
//nums[right] 以及rihgt右侧元素满足上述条件的对立面
//上述的循环不变量将直接影响最终结果,循环结束,left+1 == right,且满足上述循环不变量
//注意4:根据判断结果染色,这里left和right的位置需根据实际问题来染色
(/*填写关于mid与目标值的关系*/ ? left : right) = mid;
}
return right;
}
};
(2)二分答案模板:
class Solution {
public:
long long binaryAns(vector<int>& ranks, int cars) {
//注意1:使用局部函数(lamabda表达式)来定义check函数。返回值为bool类型
//这样比重新定义一个check函数需要敲的代码量少
auto check = [&](long long ans) {
/*填写判断ans是否为答案的逻辑,返回值bool类型*/
};
//注意2:开区间设定初始边界的时候,要注意左边界-1,右边界+1
int left = 左边界-1, right = 右边界+1;
//注意3:二分。可以看出,二分答案的开区间二分非常简单,唯一需要注意的地方是
//check检查了mid之后,应该染色左边界还是右边界?其他的写法一成不变,很有模板的韵味
while(left + 1 < right){
long long mid = left + (right - left) / 2;
//注意4:根据判断结果染色,这里left和right的位置需根据实际问题来染色
(check(mid) ? left : right) = mid;
}
return right;
}
};
定长滑动窗口:
//假设滑动窗口固定长度为: n ,则代码模板如下:
class Solution {
public:
int SlideWindow(vector<int>& nums, int k) {
int length = nums.size();
//在进入循环之前,必须先初始化好窗口为最左侧位置的情况
//并且维护好这种情况下的相关变量
//这里要首先判断一下初始化的结果是否满足题意,然后下面的第一次循环就不必
//遍历第一种情况了,这么做是也是为了满足循环不变量[i - n, i)
//循环不变量:滑动窗口[i - n, i),窗口长度固定为 n
for (int i = n; i < length; i++) {
//此时i位置为窗口本次循环的末位置下标,由于是开区间i,所以接下来要维护nums[i]的状态
//而i-n位置为上一次循环的首位下标,我们通常也需要关注维护它的状态,使窗口左边界向右移动一位
//以上操作进行完毕之后,此时窗口区间就变为闭区间[i - n + 1, i]了,长度还是n
//下一次循环之前i++,区间再次变为半开半闭状态-[i - n, i)
}
return ...;
}
};
不定长滑动窗口:
//不定长滑动窗口伪代码
class Solution {
public:
long long slideWindow(vector<int>& nums, long long k) {
int 需要维护的变量..., left = 0, ans = 0;
for (int right = 0; right<nums.size(); right++){
代码:在这里进行将nums[right]加入到窗口之后需要维护
窗口的相关状态变量的操作,使其达到 "缩小左边界的条件"
ans可能需要在这里维护
while(满足缩小左边界的条件){
代码:在这里进行缩小左边界所需维护窗口相关变量状态的操作
ans可能需要在这里维护
left++;
}
ans可能需要再这里维护
}
return ans;//返回答案
}
};
单调栈:
(1)寻找左侧第一个小的元素:
- 栈中存放的是数组的下标,而不是数组的值
- 此模板适用于以下四种情况:
- 寻找数组中所有元素左侧第一个比它小的元素
- 寻找数组中所有元素左侧第一个比它大的元素
- 寻找数组中所有元素右侧第一个比它小的元素
- 寻找数组中所有元素右侧第一个比它大的元素
vector<int> ans(n, -1);
stack<int> st;
for (int i = 0; i < n; i++) {
//应该递增栈,还是递减栈?这里可以这样思考:从右往左找,栈中存放的是答案
//则栈一定要为目标量身定制一个栈,要找的是小的,则保证从栈顶到栈底递减即可,即
//为 “寻找左侧第一个小的元素这个目标服务”, 因为这样子的话,即使栈顶元素不合适
//(大于当前遍历元素),那栈的下一个元素仍然可能满足小于当前遍历的元素
//解决了递增递减问题,下面讨论如何写出while这些代码:栈里面存放的是可能有比当前遍历
//元素小的目标元素,也可能没有。我们要找的是比当前元素小的栈中的第一个元素(从栈顶向
//下看)即,nums[i] > nums[st.top()])。那么就反过来,如果nums[i] <= nums[st.top()])
//(到这里其实已经推出了while括号里面的条件,下文可以不看,也可以看看加深理解)
//则说明当前栈顶元素不满足条件,并且后续也不可能再满足了,因为此时的nums[i]比它更
//符合条件(这里是小),还更靠左,所所以栈顶元素就被淘汰了,直接出栈,再比较新的栈顶
//元素,重复上述过程,直到找到符合条件的栈顶元素(这里是比nums[i]小)
while (!st.empty() && nums[i] <= nums[st.top()]) {
st.pop();
}
//while之后,代码走到这里有两种结果:
//1.nums[i]在栈中找到了自己的真命天子-此时的栈顶元素 (栈中有比nums[i]还
// 优秀的元素,这里的优秀指的是更小),且栈顶就是真命天子,此时栈非空
//2.nums[i]把栈弹空了,仍然找不到自己的真命天子,此时栈空了
//如果栈非空,说明是情况1.此时说明找到nums[i]左边第一个比它小
//的元素了,这个元素就是栈顶元素:nums[st.top()],接下根据实际问题处理
//维护nums[i]与nums[st.top()]的相关变量状态即可
if (!st.empty()) {
在这里维护nums[i]与st.top(),的关系,注意st.top()是下标
例如我想将nums[i]左侧第一个比它大元素下标记录一下:
ans[i] = st.top(); //ans[i]的值表示:nums[i]左侧第一个比它大的元素的下标
}
//不管最后nums[i]找到没找到自己的真命天子,她都被栈给感动了,因为对于情
//况1.找到了,那自然很感动;而对于情况2,虽然没有找到,但是栈已经为此把自
//己弹空了,这更令nums[i]感动,因此nums[i]毅然决定入栈来感谢栈的付出
st.push(i);
}
注意事项:如果某个元素根本就不存在要找的关系,比如上述例子nums[3]左
侧的nums[0],nums[1],nums[2]都比它大,那么上述模板的做法是忽略不处理,
例如上述模板的nums[2]找不到符合条件的元素,那么ans[2]将等于其初始值-1
回溯算法:
//自己画树形图分析的时候,要时刻思考回溯三问:
//回溯三问:当前操作? 子问题? 下一个子问题?
//解释:回溯三问主要用于解决代码模板里面的 "当前操作" ,"子问题" 这两个代码块
//至于" 回溯" 这个代码块,是当前操作的逆过程,无需多言
//而刚开始的 "终止" 的相关逻辑也比较好想
//因此难点是剩下的两步:"当前操作?" , "子问题?"
//回溯的问题都是树形结构的,每一个节点都是一个子问题,如何通过当前操作,使当前问题
//变成规模更小的子问题?想清楚这一点,就可以很容易的画出树形图,然后当前操作和子问题的代码
//也会一目了然
void backtracking(参数){
if(终止条件){
存放结果;
return;
}
for(选择: 本层集合中的元素(树中节点孩子的数量就是集合大小)){
处理节点; //当前操作
backtracking(路径,选择列表); //子问题
回溯,撤销处理结果; //回溯
}
}
748

被折叠的 条评论
为什么被折叠?



