目录
快慢指针
之前在链表总结部分,提及到了快慢指针,凡是牵扯到链表中的精准定位问题,选择使用快慢指针,保准没错。
环的个数/起点,中间位置,两条链表找相交处,看了题目描述,直接快慢指针,手到擒来。
LeetCode 链表总结:https://blog.csdn.net/qq_41605114/article/details/105385252
关于链表中的快慢指针使用,此处不再多嘴,详情见上方链接
好文分享:
滑动窗口
字符串或者数字之间求交集(在字符串中是子串),或者符合要求的连续子集或者子字符串,都可以使用滑动窗口进行解答。
滑动窗口的核心,就是在右侧区间不断扩大的同时,根据完成解的要求,右移左侧指针,依次找到最优解。
@powcai对滑动窗口的题目进行了汇总:
@labuladong关于滑动窗口,总结了一套模板:https://leetcode-cn.com/problems/minimum-window-substring/solution/hua-dong-chuang-kou-suan-fa-tong-yong-si-xiang-by-/%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7.md
模板如下:
/* 滑动窗口算法框架 */
void slidingWindow(string s, string t) {
//(1)
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
//(2)
int left = 0, right = 0;
int valid = 0;
while (right < s.size()) {
// c 是将移入窗口的字符
char c = s[right];
// 右移窗口
right++;
// 进行窗口内数据的一系列更新
...
/*** debug 输出的位置 ***/
printf("window: [%d, %d)\n", left, right);
/********************/
// 判断左侧窗口是否要收缩
while (window needs shrink) {
// d 是将移出窗口的字符
char d = s[left];
// 左移窗口
left++;
// 进行窗口内数据的一系列更新
...
}
}
}
作者:labuladong
链接:https://leetcode-cn.com/problems/minimum-window-substring/solution/hua-dong-chuang-kou-suan-fa-tong-yong-si-xiang-by-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
以下内容也主要是参考上面提到的多篇文章并加入自己的理解。
模板介绍:
(1)中,初始化两个Hash表,window代表我们的滑动窗口,needs代表我们需要找的目标值及个数
(2)中,初始化滑动窗口的左右指针,整个过程中滑动窗口都是一个左闭右开的区间,即[left,right),其中valid表示窗口中满足needs条件的字符个数。
此模板是左闭右开,务必注意right和left更新的位置,更新的位置不一样,那么我们得到的解也就不一样
滑动窗口的核心,就是先找到一个满足要求的解,然后不断的收缩空间,尝试得到最优解。
right不断右移,扩大窗口范围,在扩大的过程中,不断判断,是否得到了一个满足要求的解,一旦得到一个解,在更新解的同时
开始进行窗口left的右移,我们开始缩小窗口,直到在窗口中失去解,完成lef的移动,right继续右移
我们也可以这么认为:
不断找到解,不断失去解,再次找到解。
首先找到一个解,然后移动left指针,直到区间失去了解,失去解是为了在剩下的内容中找到另一组解
失去解后,right不断右移,直到我们再次找到解。以此循环,知道遍历所有要查找内容。
如果说二分查找的精华在区间中实在有解,滑动窗口的精髓在于舍弃解后,再次寻找解。
下面找一道题目作为依托,进一步阐述滑动窗口的原理
76. 最小覆盖子串
https://leetcode-cn.com/problems/minimum-window-substring/
class Solution {
public:
string minWindow(string s, string t) {
unordered_map<char,int> window,need;
for(auto item:t) need[item]++;//目标元素出现次数
int left = 0,right = 0,valid = 0;
int length = INT_MAX,begin = 0;
int size = need.size();
while(right < s.size())
{
char Rtemp = s[right];//当前字符
right++;
if(need[Rtemp])//一旦当前字符是目标值之一,那么我们进行记录,目前窗口包含目标值
{
window[Rtemp]++;//更新窗口包含目标值的情况
if(window[Rtemp] == need[Rtemp])
//一旦某个目标值元素在窗口中的个数满足要求,那么更新valid
valid++;
}
while( valid == size )//一旦条件成立,说明窗口中目前已经包含了解
{
//更新解
if(right - left<length)
{begin = left;length = right - left;}
//下面收缩左区间边界,尝试寻找最优解
char Ltemp = s[left];//当前字符
left++;
if(need[Ltemp])
{
window[Ltemp]--;
if(window[Ltemp] < need[Ltemp])//此处特别注意,判断符合应该是小于
valid--;
}
}
}
return length == INT_MAX?"":s.substr(begin,length);
//substr,begin规定起点,length规定了包括起点在内的元素长度
}
};
下面我们来图解一下整个过程:
right不断地右移,知道指向C,此时valid == 3,right照常自增1。第一次找到解,此时更新解,len = 6,begin = 0;
之后left右移,从解中删除A,left自动增1,valid == 2,区间内不包含解,那么直接break,right继续右移
知道right找到最后的A,right自增1,valid == 3,找到第二个解,但是不如第一个解短,不更新解,left开始右移
直到将C移除区间,right继续右移
当right在等于s.size()的时候,选择C,然后自增1,此时valid等于3,left开始右移
当left选中E的时候,left自增,到达了B的位置,此时进入新的一轮循环,更新解,排除B后,valid也不在等于3,跳出循环
此时right也不符合条件,跳出循环。
最优解已经被记录,完成。
最重要的细节部分,是对区间和最优解的更新位置,务必注意,因为左闭右开,right在更新解前自增,left在更新解后自增
567. 字符串的排列
https://leetcode-cn.com/problems/permutation-in-string/
class Solution {
public:
bool checkInclusion(string s1, string s2) {
unordered_map<char,int> window,need;
for(auto item : s1) need[item]++;
int left = 0, right = 0;
int len = INT_MAX;
int valid = 0;
int nsize = need.size(); //避免重复元素
while(right<s2.size())
{
char Rtemp = s2[right];
right++;
if(need[Rtemp])
{
window[Rtemp]++;
if(window[Rtemp] == need[Rtemp])
valid++;
}
while(valid == nsize)
{
if(right - left <len)
{
len = right - left;
if(len == s1.size()) return true;
}
char Ltemp = s2[left];
left++;
if(need[Ltemp])
{
window[Ltemp]--;
if(window[Ltemp]<need[Ltemp])
valid--;
}
}
}
return false;
}
};
架构完全一样,改都没有改,本题只需要增加一些判断即可,需要是子串,而且要求无视排列。
那么只要在s2中找到一个子串,均包含s1,且长度和s1相等,那一定就是找到了,否则就是没有找到。
438. 找到字符串中所有字母异位词
https://leetcode-cn.com/problems/find-all-anagrams-in-a-string/
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
unordered_map<char,int> window,need;
for(auto item:p) need[item]++;
int right = 0,left = 0;
int valid = 0,len = p.size();
int nsize = need.size();//避免重复元素
vector<int> Res;
while(right<s.size())
{
char rtemp = s[right];
right++;
if(need[rtemp])
{
window[rtemp]++;
if(window[rtemp] == need[rtemp])
valid++;
}
while(valid == nsize)
{
if((right - left) == len)//长度相等,元素都有,排列不相等
Res.push_back(left);
char ltemp = s[left];
left++;
if(need[ltemp])
{
window[ltemp]--;
if(window[ltemp] < need[ltemp])
valid--;
}
}
}
return Res;
}
};
此题和上一道题非常相似,都是子串,那么我们还是从长度入手进行解题。
但是有时我们需要的题目不一定如此复杂,下面的第3题和209题,就是滑动窗口的另外两种风格。
但是核心不变,就像二分查找的区间,不管左右区间如何迭代,解都是要在区间内的
滑动窗口也是如此,解一定要保持在窗口内,上面几道字符串类型的题目,都体现了滑动窗口的核心:
不断找到解,不断失去解,再次找到解。
首先找到一个解,然后移动left指针,直到区间失去了解,失去解是为了在剩下的内容中找到另一组解
失去解后,right不断右移,直到我们再次找到解。以此循环,知道遍历所有要查找内容。
如果说二分查找的精华在区间中实在有解,滑动窗口的精髓在于舍弃解后,再次寻找解。
下面的题目更是体现了这点。
3. 无重复字符的最长子串
https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/
还是左右指针,滑动窗口的套路,
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_map<char,int>window;
int right = 0,left = 0;
int len = 0;
while(right<s.size())
{
char temp = s[right];
right++;
window[temp]++;
while(window[temp]>1)//清空部分
{
char ltemp = s[left];
left++;
window[ltemp]--;
}
len = max(len,right - left);
}
return len;
}
};
window[temp]大于1的时候,说明有重复的了,那么我们把重复的排除出去即可,不断移动left指针,直到区间内再次恢复为只含
有单独元素的情况
滑动窗口的变形:
1248. 统计「优美子数组」
https://leetcode-cn.com/problems/count-number-of-nice-subarrays/
本题在某种意义上,甚至是有些违背滑动窗口的一般结题思路的
滑动窗口一般是找到解,然后将解抛出,继续寻找最优解。
此题我们在找到解的时候,需要特别注意,如果理解抛出解,会少算很多情况
比如示例3
以第一个2开头,就有四种情况,以第二个2开头也有四种,总共16种,所以我们的滑动窗口需要进行一下修改
本题我们需要在找到一组解后,计算这个解中两端奇数前后的偶数个数,算出排列组合的情况
class Solution {
public:
int numberOfSubarrays(vector<int>& nums, int k) {
//滑动窗口
int size = nums.size();
if(size<k) return 0;
int right = 0,left = 0;
int valid = 0,res = 0;
deque<int> target;
int leftnumber = 0,rightnumber = 0;
while(right<size)
{
int Rtemp = nums[right];
if(Rtemp%2 != 0)
{
target.push_back(right);//有效值的位置
valid++;
}//是奇数,增加
right++;
if(valid == k)
{
leftnumber = target.front() - left + 1;//第一个奇数到左边界的个数
while(right<size)//找到下一个奇数
{
int temp = nums[right];
if(temp%2 == 0) //是偶数
right++;
else break;
}
rightnumber = right - target.back();
while(valid == k)
{
int Ltemp = nums[left];
if(Ltemp%2 != 0)//奇数
valid--;
left++;
}
target.pop_front();
cout<<rightnumber<<" "<<leftnumber<<endl;
}
res+=rightnumber*leftnumber;//更新组合数
}
return res;
}
};
使用两个int变量,leftnumber 和 rightnumber,分别记录第一个奇数距离left的距离,和最后一个奇数距离right的距离
找到排列组合之后,我们要进行滑动窗口的老操作了,就是边界收缩,让left不断收缩,直到没有解
这是一道非常非常有趣的滑动窗口题目。反常规。
209. 长度最小的子数组
https://leetcode-cn.com/problems/minimum-size-subarray-sum/
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
int right = 0,left = 0;
int sum = 0,len = INT_MAX;
while(right<nums.size())
{
sum += nums[right];
right++;
while(sum>=s)
{
len = min(len,right - left);
sum -= nums[left];
left++;
}
}
return len == INT_MAX?0:len;
}
};
当和到达要求的时候,此时可以求长度了,我们得到了解,下面就要舍弃解,不断移动left,直到失去解
然后我们才能去找更优的解。
30. 串联所有单词的子串
https://leetcode-cn.com/problems/substring-with-concatenation-of-all-words/
参考内容:
https://leetcode-cn.com/problems/substring-with-concatenation-of-all-words/solution/30-by-ikaruga/
本题看似非常简单,不就是把找元素换找单词吗?步长从1改成单词长度,不就分分钟的事情吗?
但是此题的情况远比这复杂,因为题目可没有说,其他干扰项的长度也是步长
比如:
"lingmindraboofooowingdingbarrwingmonkeypoundcake"
["fooo","barr","wing","ding","wing"]
"aaaaaa"
["aaa","aaa"]
看了简直要死,第一个例子,第一个大的干扰项长度是13,但是平常的步长是4
那么本题该如何应对,我们在滑动窗口外侧加循环,让整个滑动窗口的起点,从0开,一直到步长4为止,起点分别是
0,1,2,3,按照一个步长的循环来进行,这样总能规避掉各种长度的干扰项目。
题目要求找到的解,必须长度和words所有元素的长度加起来一致,但是排列无所谓,所以求子串是否是我们想要的解,判断条件就是长度相等
本题细节非常多,可谓是滑动窗口之最了
先看核心代码
int left = i;
int right = i,valid = 0;
unordered_map<string, int> window;
while(right<s_size)
{
string Rtemp = s.substr(right,step);
right+=step;
if(need[Rtemp])
{
window[Rtemp]++;
if(window[Rtemp] == need[Rtemp])
valid++;
}
while(valid == validsize)
{
if(right - left == length)//①有要求,解必须是只包含所要的单词
Res.push_back(left);
string Ltemp = s.substr(left,step);
left+=step;
if(need[Ltemp])
{
window[Ltemp]--;
if(window[Ltemp] < need[Ltemp])//②此处
valid--;
}
}
①处,因为题目要求,不能含有其他多余字符,所以整个长度必须是匹配的
②处,这个地方写快了很容易出错,一旦少一个字符,我们就要将valid减去1
因为字典中每个字符都要出现,我们还是用Hash表进行记录,但是这样有时候会记录重复的内容
所以我们在加完temp后,立即比较,如果数量够了就将valid加1,之后有重复的也不管了
完整版本(有改进)
class Solution {
public:
vector<int> Res;
vector<int> findSubstring(string s, vector<string>& words) {
if(s.size() == 0 || words.empty()) return {};
unordered_map<string,int>M;
int len = 0,strlen = words[0].size();
for(auto item:words) {len++;M[item]++;}
for(int i = 0;i<strlen;++i) SubSolution(s,M,len,strlen,i);
return Res;
}
void SubSolution(string s, unordered_map<string,int>M,int len,int strlen,int begin){
int left = begin,right = begin;
int value = 0;
unordered_map<string,int>Temp;
while(right<s.size()){
string Rtemp = s.substr(right,strlen);
right += strlen;
if(M.find(Rtemp) != M.end()){
Temp[Rtemp]++;
if(Temp[Rtemp] == M[Rtemp]) value++;
}
while(value == M.size()){
if((right - left) == len*strlen) Res.push_back(left);
string Ltemp = s.substr(left,strlen);
left += strlen;
if(M.find(Ltemp) != M.end()){
Temp[Ltemp]--;
if(Temp[Ltemp] < M[Ltemp]) value--;
}
cout<<left<<" "<<right<<endl;
}
}
return;
}
};
最后一题,借滑动窗口之名,而无滑动窗口之实。
239. 滑动窗口最大值
https://leetcode-cn.com/problems/sliding-window-maximum/
https://leetcode-cn.com/problems/hua-dong-chuang-kou-de-zui-da-zhi-lcof/
要求线性时间:我们可以这么做,维护一个栈,每次都选择滑动窗口中的最大值压入,之后每次窗口移动一次,只把串口right处的值和栈顶元素比较即可,线性时间复杂度,需要注意的是这个最值,一定要在滑动窗口的范围内:我们尝试实现一下代码:
我们先处理第一个滑动窗口:
//先处理第一个滑动窗口
int MAXnum = 0,index = 0;
for(int i = 0;i<k;++i)
{
if(MAXnum<nums[i]) {MAXnum = nums[i];index = i;}
}
MAXIndex.push(index);//压入第一个滑动窗口的最大值索引
Res.push_back(nums[MAXIndex.top()]);
cout<<nums[MAXIndex.top()]<<endl;
比较了k次,选出最值,那么我们下面从第二个滑动窗口开始:
int left = 1,right = k;//我们从第二个滑动窗口开始
while(right<size)
{
//栈中要有数字
if(MAXIndex.size())
{
//该最值元素不在窗口内,那么没有办法,我们只能把这个窗口中的所有值全部比一遍
if(MAXIndex.top()<left||MAXIndex.top()>right)
{
MAXIndex.pop();
MAXnum = nums[left],index = left;
//只比较left到right-1这个区间内的值,下面还有一个if比较right的值
for(int i = left + 1;i<right;++i)
{
if(nums[i]>MAXnum) {MAXnum = nums[i],index = i;}
}
MAXIndex.push(index);
}
//一般情况下:只比较栈中的最值和right指针的位置
if(nums[right]>nums[MAXIndex.top()]) {MAXIndex.pop();MAXIndex.push(right);}
}
else MAXIndex.push(right);
//选择最值并添加到结果中,整个滑动窗口右移
cout<<nums[MAXIndex.top()]<<endl;
Res.push_back(nums[MAXIndex.top()]);
right++;
left++;
}
完成代码如下:
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
if(nums.empty()) return {};
vector<int>Res;
stack<int>MAXIndex;
int size = nums.size();
//先处理第一个滑动窗口
int MAXnum = 0,index = 0;
for(int i = 0;i<k;++i)
{
if(MAXnum<nums[i]) {MAXnum = nums[i];index = i;}
}
MAXIndex.push(index);//压入第一个滑动窗口的最大值索引
Res.push_back(nums[MAXIndex.top()]);
cout<<nums[MAXIndex.top()]<<endl;
int left = 1,right = k;//我们从第二个滑动窗口开始
while(right<size)
{
if(MAXIndex.size())
{
//不在窗口内
if(MAXIndex.top()<left||MAXIndex.top()>right)
{
MAXIndex.pop();
MAXnum = nums[left],index = left;
for(int i = left + 1;i<right;++i)
{
if(nums[i]>MAXnum) {MAXnum = nums[i],index = i;}
}
MAXIndex.push(index);
}
if(nums[right]>nums[MAXIndex.top()]) {MAXIndex.pop();MAXIndex.push(right);}
}
else
MAXIndex.push(right);
cout<<nums[MAXIndex.top()]<<endl;
Res.push_back(nums[MAXIndex.top()]);
right++;
left++;
}
return Res;
}
};
程序一定要注意,最值要在范围内:
下面是官方解法:
(C++版本:https://leetcode-cn.com/problems/sliding-window-maximum/solution/dan-diao-dui-lie-by-labuladong/)
本题总的来说,不算是滑动窗口,只是名字一样,实际方法是双项队列deque
为了降低时间复杂度,我们观察一下窗口移动的过程类似于队列出队入队的过程,每次队尾出一个元素,然后队头插入一个元素,求该队列中的最大值
每次的值都和队尾元素比较,将小的弹出,大的暂时放入,队首一直都是最大值(在滑动窗口范围内)
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
if(nums.empty()) return {};
vector<int>Res;
deque<int>MAXindex;
//处理第一个窗口
for(int i = 0;i<k;++i)
{
while(MAXindex.size()&&nums[MAXindex.back()]<nums[i])//注意细节,是和最后的部分比
MAXindex.pop_back();//弹出尾部
MAXindex.push_back(i);
}
Res.push_back(nums[MAXindex.front()]);
// cout<<MAXindex.size()<<endl;
int left = 1,right = k,size = nums.size();
while(right<size)
{
//提出不在范围内的值
while(MAXindex.size()&&(MAXindex.front()>right||MAXindex.front()<left)) MAXindex.pop_front();//弹出头部
//最值比较
while(MAXindex.size()&&nums[MAXindex.back()]<nums[right])//注意细节,是和最后的部分比
MAXindex.pop_back();//弹出尾部
MAXindex.push_back(right);
Res.push_back(nums[MAXindex.front()]);
left++,right++;
// cout<<MAXindex.size()<<endl;
}
return Res;
}
};
以上情况,正好是线性的时间复杂度
面试题57 - II. 和为s的连续正数序列
https://leetcode-cn.com/problems/he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof/
从头开始滑动窗口,找到解后,因为以这个为开头的解只可能有一个,所以左侧窗口边界收缩
这个方法非常巧妙,但是要注意细节,注意循环的break条件,否则会找不全内容
class Solution {
public:
vector<vector<int>> findContinuousSequence(int target) {
int Uplimit = target/2;
//滑动窗口
vector<vector<int>> Res;
int left = 1,right = 1;
int Sum = 0;
while(left<=Uplimit)//小细节
{
if(Sum>target)
{
Sum -= left;
left++;
}
else if(Sum<target)
{
Sum += right;
right++;
}
else if(Sum == target)
{
vector<int>Temp;
for(int i = left;i<right;++i) Temp.push_back(i);
Res.push_back(Temp);
Sum -= left;
left++;
}
}
return Res;
}
};
Hash + 滑动窗口
下面三道题,滑动窗口的判断和普通判断稍有不同,需要注意!
424. 替换后的最长重复字符
https://leetcode-cn.com/problems/longest-repeating-character-replacement/
本题思路:统计滑动窗口之间,出现次数最多的元素,那么剩下的元素,就是要被替换的内容,当被替换的内容长度大于k的时候,就要开始收拾左侧边界了,收缩左边界的时候,要统计收缩后的出现最多元素的个数,此时需要遍历Hash table
//本题整体思路比较不好想,难在判断合格条件上
class Solution {
public:
int characterReplacement(string s, int k) {
if(s.empty()) return 0;
unordered_map<char,int>M;
int left = 0,right = 0,maxlen = 0,MAX = 0;
while(left<=right&&right<s.size()){
//左右指针之间,统计最多元素的个数,剩下的就是要替换的内容
char rstr = s[right++];
M[rstr]++;
maxlen = max(maxlen,M[rstr]);//时刻去确定right和left之间最长重复元素的个数
//left 到 right 之间,最多重复元素的个数
if((right - left - maxlen)<=k) MAX = max(MAX,right - left);
//在不超范围的情况下去计算最长距离
while((right - left - maxlen)>k){
//循环条件是,left和right之间,可替换的数量已经大于k了,需要减少
char lstr = s[left++];
M[lstr]--;
for(auto item:M) maxlen = max(maxlen,item.second);//这个时候要从头计算
}
}
return MAX;
}
};
最大连续1的个数系列问题
标准的动态规划
class Solution {
public:
int findMaxConsecutiveOnes(vector<int>& nums) {
if(nums.empty()) return 0;
int size = nums.size();
vector<int>dp(size+1,0);
int MAX = 0;
for(int i = 1;i<=size;++i){
if(nums[i-1] == 1){
dp[i] = dp[i-1] + 1;
}
else dp[i] = 0;
MAX = max(MAX,dp[i]);
}
return MAX;
}
};
487. 最大连续1的个数 II https://leetcode-cn.com/problems/max-consecutive-ones-ii/
滑动窗口:统计1和0的个数,当0的个数为1时,我们进行左右边界的移动,先移动右侧窗口,直到下一个不是1的位置,然后收缩左侧窗口,直到排除0。
class Solution {
public:
int findMaxConsecutiveOnes(vector<int>& nums) {
if(nums.empty()) return 0;
int left = 0,right = 0;
int Zero = 0,One = 0,res = 0;
while(left<=right&&right<nums.size()){
int rtemp = nums[right++];
if(rtemp == 1) One++;
else Zero++;
while(Zero){
//right再往右移动,直到下一个0
while(right<nums.size()&&nums[right] == 1) {right++;One++;}
res = max(res,One + Zero);
int rtemp = nums[left++];
if(rtemp == 1) One--;
else Zero--;
}
}
return res == 0?nums.size():res;
}
};
1004. 最大连续1的个数 III https://leetcode-cn.com/problems/max-consecutive-ones-iii/
class Solution {
public:
int longestOnes(vector<int>& A, int K) {
if(A.empty()) return 0;
int left = 0,right = 0;
int Zero = 0,One = 0,res = 0;
while(left<=right&&right<A.size()){
int rtemp = A[right++];
if(rtemp == 0) Zero++;
else One++;
if(Zero<=K) res = max(res,One + Zero);//判断位置要对,可以省去很多繁琐的操作
while(Zero>K){
int ltemp = A[left++];
if(ltemp == 0) Zero--;
else One--;
}
}
return res;
}
};
剑指 Offer 48. 最长不含重复字符的子字符串
https://leetcode-cn.com/problems/zui-chang-bu-han-zhong-fu-zi-fu-de-zi-zi-fu-chuan-lcof/
class Solution {
public:
int lengthOfLongestSubstringTwoDistinct(string s) {
//最多包含两个不同元素的子串
unordered_map<char,int>M;//数组做hash map
if(s.empty()) return 0;
int left = 0,right = 0;
int diff = 0,MAX = 0;
while(left<=right&&right<s.size()){
char rstr = s[right++];
if(M[rstr] == 0) {//初见这个元素
diff++;//统计不同元素的个数
}
M[rstr]++;
if(diff <= 2) MAX = max(MAX,right - left);
while(diff > 2){
char lstr = s[left++];
M[lstr]--;
if(M[lstr] == 0) {
diff--;//统计不同元素的个数
}
}
}
return MAX;
}
};
双指针
有下面一道题目,有一个排序数组,现在需要在线性的时间复杂度下,找出目标和,我们应该怎么做,Hash表是很好的方法
1. 两数之和
https://leetcode-cn.com/problems/two-sum/
leetcode天字一号题目,两数之和,又回到了梦开始的地方
Hash表:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int,int> m;
for(int i = 0;i<nums.size();++i)
{
if(m.find(target - nums[i])!=m.end())
return{m[target - nums[i]],i};
else
m[nums[i]] = i;
}
return{};
}
};
但是稍稍改变题目,返回的是数组的元素而不是下标,双指针法也毫不逊色:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int size = nums.size();
sort(nums.begin(),nums.end());//排序
vector<int> Res;
int left = 0,right = size-1;
while(left<right)
{
if((nums[left]+nums[right])==target)
{
Res.push_back(left);
Res.push_back(right);
while(left<size-1&&nums[left] == nums[left+1])left++;
while(right>0&&nums[right] == nums[right-1])right--;
left++,right--;
}
else if((nums[left]+nums[right])>target) right--;
else left++;
}
return Res;
}
};
以上是一个典型的双指针技巧。
从排序数组的两端,不断向中间逼近,下面我们看图解:
如果说快慢指针是数学问题,滑动窗口的一个非常傲娇的过程,先找到解,再将解移除区间,然后再次寻找解
双指针的核心,就是根据数列的性质(从小到大排序),让两个指针移动。
这些性质也就省去了很多冗余不必要的计算。
https://leetcode-cn.com/problems/3sum/solution/three-sum-ti-jie-by-wonderful611/
双指针一般出现在数组问题中,数组问题是个非常庞大的系列,其中使用到的方法也是各式各样。
15. 三数之和
https://leetcode-cn.com/problems/3sum/
初见此题,在没有任何准备的情况下,暴力解法就是一种解法,在暴力解法的基础上,Hash表也是一个优化的选择。
但是面对数组问题,可以尝试排序后使用双指针的方法进行解决。
下面收录了一些非常好的解法和解题思路讲解:
https://leetcode-cn.com/problems/3sum/solution/san-shu-zhi-he-cshi-xian-shuang-zhi-zhen-fa-tu-shi/
https://leetcode-cn.com/problems/3sum/solution/man-hua-jue-bu-wu-ren-zi-di-xiang-kuai-su-kan-dong/
https://leetcode-cn.com/problems/3sum/solution/three-sum-ti-jie-by-wonderful611/
以上三篇文章,都是从不同的角度对本问题提出了分析的方法
变化太多的情况,逻辑上我们就要把他变成变化少的情况,固定一个数的位置,去找剩下两个数字。
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> Res;
int size = nums.size();
if(size <= 2) return Res;
sort(nums.begin(),nums.end());
int pre = 0,right = size-1,left = pre+1;
while(pre<=size-3)
{
int sum = 0;
while(left<right)
{
vector<int> Temp;
sum = nums[pre]+nums[left]+nums[right];
if(sum == 0)
{
Temp.push_back(nums[pre]);
Temp.push_back(nums[left]);
Temp.push_back(nums[right]);
left++,right--;
}
else if(sum>0) right--;
else left++;
if(!Temp.empty()) Res.push_back(Temp);
}
pre++,right = size-1,left = pre+1;
}
return Res;
}
};
会发现,计算重复了,那么,我们需要剔除重复的结果
如果pre重复了,那么我们继续加,直到遇到一个新的pre
对于right和left也是一样的,既然重复,那么就不断循环,直到找到一个区别于刚才一组解的值
代码如下:
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> Res;
int size = nums.size();
if(size <= 2) return Res;
sort(nums.begin(),nums.end());
int pre = 0,right = size-1,left = pre+1;
while(nums[0]<=0&&pre<=size-3)
{
int sum = 0;
while(left<right)
{
vector<int> Temp;
sum = nums[pre]+nums[left]+nums[right];
if(sum == 0)
{
Temp.push_back(nums[pre]);
Temp.push_back(nums[left]);
Temp.push_back(nums[right]);
//位置一定要放对了,一定要是在等于0之后,剔除重复的内容
while(right>0&&nums[right] == nums[right-1]) right--;
while(left<size-1&&nums[left] == nums[left+1]) left++;
left++,right--;
}
else if(sum>0) right--;
else left++;
if(!Temp.empty()) Res.push_back(Temp);
}
while(pre<size-1&&nums[pre] == nums[pre+1]) pre++;
pre++,right = size-1,left = pre+1;
}
return Res;
}
};
版本二:
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
if(nums.empty()) return {};
int size = nums.size();
vector<vector<int>>Res;
sort(nums.begin(),nums.end());//排序
int PCur = 0,left = 1,right = size-1;
while(PCur<=size-2)//倒数第二个
{
if(PCur-1>=0&&nums[PCur] == nums[PCur-1]) {PCur++;continue;}
//和自己的前一个比较,如果一样,那么我们就要跳过这个值
left = PCur+1,right = size-1;
while(left<right)
{
int temp = nums[PCur]+nums[left]+nums[right];
// cout<<temp<<endl;
if(temp<0) left++;
else if(temp>0)right--;
else if(temp == 0)
{
vector<int> Vtemp;
Vtemp.push_back(nums[PCur]);
Vtemp.push_back(nums[left]);
Vtemp.push_back(nums[right]);
Res.push_back(Vtemp);
right--;left++;
//放置于正确的位置,当等于零时,移动左右指针,知道遇到不一样的内容为止,放置重复
while(right>=0&&nums[right] == nums[right+1]) right--;
while(left<=size-1&&nums[left] == nums[left-1]) left++;
}
}
PCur++;
}
return Res;
}
};
下面的解中,包含了各种个数的求和总结:
https://leetcode-cn.com/problems/3sum/solution/man-hua-jue-bu-wu-ren-zi-di-xiang-kuai-su-kan-dong/
那么将上面的题目稍作改变:
16. 最接近的三数之和
https://leetcode-cn.com/problems/3sum-closest/solution/
类比上一道题目。本题可以选择重复内容,只要求接近,那么我们不断去更新最小值,再根据目前三个数的和与target的关系,移动指针,不断去逼近正确答案。
class Solution {
public:
int threeSumClosest(vector<int>& nums, int target) {
int size = nums.size();
sort(nums.begin(),nums.end());
if(size < 3) return 0;
int Min = INT_MAX,pre = 0,right = size-1,left = pre+1,Res = 0;
while(pre<size-2)
{
while(left<right)
{
int sum = (nums[pre]+nums[right]+nums[left]);
int delta = abs(sum-target);
if(Min>delta)
{
Min = delta;
Res = sum;
}
if(sum<target) left++;
else right--;
}
pre++,right = size-1,left = pre+1;
}
return Res;
}
};
上面的题目是一个类型,那么当我们需要不能排序的情况,或者整体大小情况未知的时候,该怎么办?
9. 回文数
https://leetcode-cn.com/problems/palindrome-number/
灵活机动,将数字变成字符串,这样就可以诸位进行访问,然后双指针操作,一个从左一个从右开始,向中间收缩,一旦不相等,break;
class Solution {
public:
bool isPalindrome(int x) {
string num = to_string(x);
int size = num.size();
int right = size-1,left = 0;
while(left<right)
{
if(num[left] != num[right]) return false;
left++;
right--;
}
return true;
}
};
11. 盛最多水的容器
https://leetcode-cn.com/problems/container-with-most-water/
暴力解法:
class Solution {
public:
int maxArea(vector<int>& height) {
int size = height.size();
if(size == 0) return 0;
if(size == 2)
{
return min(height[0],height[1]);
}
int pre = 0,right = size-1,left = pre+1,MAX = 0;
while(pre<size-2)
{
while(left<right)
{
int sum1 = min(height[left],height[pre])*(left-pre);
int sum2 = min(height[right],height[pre])*(right-pre);
int tempMAX = max(sum1,sum2);//计算最大范围
MAX = max(MAX,tempMAX);
left++;
}
pre++,right = size-1,left = pre+1;
}
return MAX;
}
};
显然是超时,那么我们应该怎样解决这个问题呢?简化双指针
就两个指针,一左一右,计算面积,然后收缩,怎么收缩呢,收缩牵扯到长方形长边的缩短,所以要找出最大值,我们要固定right和left中的较大值,移动较小的值。
class Solution {
public:
int maxArea(vector<int>& height) {
int left = 0, right = height.size() - 1;
int Res = 0;
while (left < right) {
int sum = min(height[left], height[right]) * (right - left);
Res = max(Res, sum);
//移动小的边界
if (height[left] <= height[right]) ++left;
else right--;
}
return Res;
}
};
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
面试题 16.06. 最小差
https://leetcode-cn.com/problems/smallest-difference-lcci/solution/
此题和上面的题目如出一辙,上面是没有办法排序,本次是没有办法知道两个数组彼此之间的大小情况
那么还是老办法,让他们自己排序,然后两个指针,各自指向各自的首地址,算完差值后,二者比较,大的肯定是动不了,大的动了二者差距越来越大,小的动,以此类推不断循环。
class Solution {
public:
int smallestDifference(vector<int>& a, vector<int>& b) {
int sizea = a.size(),sizeb = b.size();
if(sizea == 0||sizeb == 0) return 0;
sort(a.begin(),a.end());
sort(b.begin(),b.end());
long p1 = 0,p2 = 0,res = INT_MAX;
while(p1<sizea&&p2<sizeb)
{
long temp = abs(a[p1]-b[p2]);
res = min(res,temp);
if(a[p1]<b[p2])//现在,大的增大,差距更大,让小的增大
p1++;
else p2++;
cout<<"temp:"<<temp<<"res:"<<res<<endl;
}
return res;
}
};
240. 搜索二维矩阵 II
https://leetcode-cn.com/problems/search-a-2d-matrix-ii/
本题最大的技巧,是从右上角开始,因为如果从左上角开始,上面右面都是大值,这怎么移动?
反而是右上角a点为起始,比target大了,左移(因为a的值本行最大),比target小了,下移(因为a的值本行最小)
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
if(matrix.empty()||matrix[0].empty()) return false;
int m = matrix.size(),n = matrix[0].size();
;
int Hbegin = 0,Lbegin = n-1;
while(Hbegin<m&&Lbegin>=0)
{
if(matrix[Hbegin][Lbegin] == target) return true;
if(matrix[Hbegin][Lbegin] > target) Lbegin--;
else Hbegin++;
}
return false;
}
};
面试题21. 调整数组顺序使奇数位于偶数前面
https://leetcode-cn.com/problems/diao-zheng-shu-zu-shun-xu-shi-qi-shu-wei-yu-ou-shu-qian-mian-lcof/
优秀代码:
class Solution {
public:
vector<int> exchange(vector<int>& nums) {
if(nums.empty()) return {};
int size = nums.size();
int left = 0,right = size - 1;
while(left<right)
{
while(left<size&&nums[left]%2!=0) left++;//找第一个偶数
cout<<"left"<<left<<endl;
while(right>=0&&nums[right]%2==0) right--;//找最后一个奇数
cout<<"right"<<right<<endl;
if(right<0||left>=size||left>right) break;
swap(nums[left],nums[right]);
left++,right--;
}
return nums;
}
};
垃圾(但是实在是太容易想到了):
class Solution {
public:
vector<int> exchange(vector<int>& nums) {
if(nums.empty()) return {};
int size = nums.size();
vector<int>Res;
unordered_map<int,int>M;
for(int i = 0;i<nums.size();++i)
{
if(nums[i]%2 != 0)
{Res.push_back(nums[i]);M[i]++;}
}
for(int i = 0;i<nums.size();++i)
{
if(M[i]!=1)
Res.push_back(nums[i]);
}
return Res;
}
};
1471. 数组中的 k 个最强值
https://leetcode-cn.com/problems/the-k-strongest-values-in-an-array/
周赛第二题:
本题叙述还是比较复杂的,我们整理一下内容:
- 排序
- 找中位数
- 寻找满足条件的数字
前两点非常好满足
第三点:我们看表达式一的样子,就是在求和中位数的差值(绝对值),越大越好,那么对于一个排序数组来说,首尾必然是差值最大的地方,但是具体差多少,是首差值大还是尾差值大,我们不知道
再看表达式二,当二者差值相等,本身值谁大选择谁,因为是排序数组,显然是序号越大,越靠近尾部的值大。
综上,直接双指针法,left = 0,right = size - 1
对比 abs(arr[right] - arr[mid]) 和 abs(arr[mid] - arr[left])的关系,大于等于,其实都是选择right值
只有当小于的时候,才会选择left,我们让while跳出条件为left = right即可。
class Solution {
public:
vector<int> getStrongest(vector<int>& arr, int k) {
if(arr.empty()) return {};
sort(arr.begin(),arr.end());//排序
//找中位数
int size = arr.size();
int mid = arr[size/2];
//找到合适的内容
vector<int>Res;
int right = size-1,left = 0;
mid = left + (right - left)/2;
while(left<=right)
{
if(k == 0) break;
if(abs(arr[right] - arr[mid]) > abs(arr[mid] - arr[left])) //第一条规则
{Res.push_back(arr[right]);right--;k--;}
else if(abs(arr[right] - arr[mid]) == abs(arr[mid] - arr[left]))//第二条规则
{Res.push_back(arr[right]);right--;k--;}//必然是后面的大
else {Res.push_back(arr[left]);left++;k--;}
}
return Res;
}
};
581. 最短无序连续子数组
https://leetcode-cn.com/problems/shortest-unsorted-continuous-subarray/
本题我们要的是双指针,从头开始,扫描,找单调递增中的异常,也就是下降沿
找到下降沿后,需要移动左指针,找到下降沿的起点,同时移动右指针,找到下降沿的终点
class Solution {
public:
int findUnsortedSubarray(vector<int>& nums) {
if(nums.empty()) return 0;
int size = nums.size();
int right = 1,left = 0,begin = 0;
int NewB = size,NewE = 0;
while(right<size)
{
if(nums[left]<=nums[right]) {right++;left++;}
else if(nums[left]>nums[right])
{
int Ltemp = left;
while(Ltemp>=0)//检查左侧,有没有高的
{
if(nums[Ltemp]>nums[right]) Ltemp--;
else break;
}
int Rtemp = right;
while(Rtemp<size)//检查右侧,有没有低的
{
if(nums[left]>nums[Rtemp]) Rtemp++;
else break;
}
NewB = min(NewB,Ltemp);NewE = max(NewE,Rtemp);//记录起点和终点
right++;left++;
}
}
return NewE==0?0:NewE-NewB-1;
}
};
283. 移动零
https://leetcode-cn.com/problems/move-zeroes/
双指针原地交换,并不难
class Solution {
public:
void moveZeroes(vector<int>& nums) {
if(nums.empty()) return;
int right = 1,left = 0;
while(right<nums.size())
{
if(nums[left] == 0&&nums[right] == 0) {right++;continue;}//都为0
if(nums[left] != 0&&nums[right] != 0) {right += 2;left += 2;continue;}
if(nums[left] == 0&&nums[right] != 0)
{
swap(nums[left],nums[right]);
if(right-left>1) left++;
else {right++;left++;}
}
else {right++;left++;}
}
return;
}
};
75. 颜色分类
https://leetcode-cn.com/problems/sort-colors/
三指针分类解题:
三个指针,C2探索2的左边界,C0探索0的右边界,PCur保存正常遍历
当PCur指向0时,将C0和PCur交换,二者同时增加步长
当PCur指向2时,将C2和PCur交换,此时收缩2的边界指针C2,也就是将C2--,但是此时PCur不能动,因为你不知道现在PCur指向的是谁,需要在下一轮循环中判断。
比如下面的情况
错误程序:
class Solution {
public:
void sortColors(vector<int>& nums) {
//原地排序
int C2 = nums.size()-1,C0 = 0,PCur = 0;
while(PCur<C2)//错误地点!!!!!!!!!!!!!!!!!!!
{
if(nums[PCur]==0)//等于0
{
swap(nums[PCur],nums[C0]);
C0++,PCur++;
}
else if(nums[PCur]==2)//等于2
{
swap(nums[PCur],nums[C2]);
C2--,PCur++;//错误地点!!!!!!!!!!!!!!!!!!!
}
else PCur++;
}
}
};
本题要特别的注意细节问题:
class Solution {
public:
void sortColors(vector<int>& nums) {
//原地排序
int C2 = nums.size()-1,C0 = 0,PCur = 0;
while(PCur<=C2)
{
if(nums[PCur]==0)//等于0
{
swap(nums[PCur],nums[C0]);
C0++,PCur++;//交换完后,都移动
}
else if(nums[PCur]==2)//等于2
{
swap(nums[PCur],nums[C2]);
C2--;//只减少2的边界
}
else PCur++;
}
}
};
更加巧妙的双指针(从后往前遍历)
88. 合并两个有序数组 https://leetcode-cn.com/problems/merge-sorted-array/
class Solution {
public:
// m+n 方法1:新建数组,从头开始合并
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
int size1 = nums1.size(),size2 = nums2.size();
vector<int>Res;
int i = 0,j = 0;
while(i<m&&j<n){
if(nums1[i]<nums2[j]) Res.push_back(nums1[i++]);
else Res.push_back(nums2[j++]);
}
for(int k = i;k<m;++k) Res.push_back(nums1[k]);
for(int k = j;k<n;++k) Res.push_back(nums2[k]);
copy(Res.begin(),Res.end(),nums1.begin());
}
// size1logsize1 方法2:将nums2插入到nums1尾部,然后整体排序
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
int size1 = nums1.size(),size2 = nums2.size();
for(int i = m,j = 0;j<n&&i<size1;++i,++j) nums1[i] = nums2[j];
sort(nums1.begin(),nums1.end());
}
//m+n 方法3:反着来,从后往前遍历
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
int size1 = nums1.size(),size2 = nums2.size();
int end1 = m - 1,end2 = n-1,insert = size1-1;
while(end1>=0&&end2>=0){
if(nums1[end1]>nums2[end2]) nums1[insert--] = nums1[end1--];
else nums1[insert--] = nums2[end2--];
}
// cout<<end1<<" "<<end2<<endl;
for(int k = end1;k>=0;--k) nums1[insert--] = nums1[k];
for(int k = end2;k>=0;--k) nums1[insert--] = nums2[k];
}
};