目录
滑动窗口其实也是用两个指针进行操作,只不过两个指针移动的方向是一致的,我们把两个指针中间的部分叫做滑动窗口,根据题目要求进行出窗口和入窗口的操作
1.长度最小的子数组

题目要求是找出一段子数组,子数组的和大于等于目标值,并且该子数组是所有满足条件中长度最短的子数组
因为所有数据都是正整数,所以窗口中多一个元素总和会变大,少一个元素总和会减小,所以滑动窗口中的和具有单调性,也就是入窗口和变大,出窗口和变小
例如一段数组,它符合条件的最短子数组在蓝色部分,此时左右指针如果移动一定会导致某一种情况,要么依旧符合条件但是长度不是最小,要么不符合条件

那么假设我们将滑动窗口从最左侧开始,当子数组的和小于target时,右指针向右移动,当满足条件时,记录该子数组的长度,然后开始移动左指针,看看左侧有没有多余元素,因为可能右指针移动时,突然加了一个很大的值,只需要最右侧一两个元素就可以满足条件,所以要移动左指针,并减去不在窗口中的值,并且每次移动都要更新一下最短长度
然后当子数组的和小于目标值时,右指针继续向右移动,为什么这样一定能找到该子数组呢,因为这个算法的左右指针都在往右侧移动,假设右指针一直移动到该子数组最右侧之前的和都还没有达到目标值,一旦右指针继续移动,移动到该子数组的右侧时,此时左指针在开头,左右指针之间的和肯定达到了目标,并且左指针一直向右移动并更新最短长度,直到左指针移动到子数组左侧,此时找到了最短子数组
这是一种极端情况,其他情况左右指针会慢慢向右移动,不过这样可以直观感受出怎么找到最短子数组
代码部分
将最短长度设置为整型最大值,如果找不到最短的子数组,minlen就没有改变,还是整型最大值,那么此时返回0
cpp
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
long long sum = 0;
int left = 0;
int right = 0;
int len = nums.size();
int minlen = INT_MAX;
while (right < len) {
sum += nums[right];
if (sum >= target) {
while (sum >= target && left <= right) {
minlen = min(minlen, right - left + 1);
sum -= nums[left++];
}
}
right++;
}
return minlen == INT_MAX ? 0 : minlen;
}
};
2.无重复字符的最长字串

这道题可以使用哈希表的方法来判断,对于滑动窗口中的元素,入窗口时,如果该元素没有在哈希表中,那么继续入窗口,如果已经在哈希表中了,那么此时更新最长字串的结果,并开始出窗口,直到没有重复字符,继续入窗口
哈希表可以使用unordered_map,就是一个建立key和value的映射关系的容器,这里我们要记录字符是否重复,那么就使用char作为key,int作为value记录出现次数,超过1就说明重复了
那么原理就很简单了,当新加入的元素没出现过,就入窗口,当重复了就更新结果,并移动左指针,因为不能重复,所以新加入了什么元素,我们就要在滑动窗口中找到重复了的那个元素,然后移动到这个元素的下一个位置,途中出窗口的元素对应的次数均需要在哈希表中减一
代码部分
当传入空字符直接返回0,剩下的按照逻辑写即可
注意有可能右指针越界之后,但是还没有更新长度,也就是从左指针的位置一直到末尾都符合,要记得更新一下长度
cpp
class Solution {
public:
int lengthOfLongestSubstring(string s) {
if(s.size()==0)return 0;
unordered_map<char,int> HashMap;
int left=0;
int right=0;
int len=s.size();
int maxlen=-1;
while(right<len){
if(HashMap[s[right]]==0){
HashMap[s[right]]++;
}
else if(HashMap[s[right]]==1){
maxlen=max(maxlen,right-left);
//找到重复的字符
while(s[left]!=s[right]){
HashMap[s[left]]--;
left++;
}
left++;
}
right++;
}
maxlen=max(maxlen,right-left);
return maxlen;
}
};
3.最大连续1的个数
1004. 最大连续1的个数 III - 力扣(LeetCode)

对于这道题目,最多可以反转k个0,也就是一段字串中最多包含k个0,找到符合这样条件的最长字串即可
所以每次入窗口时,如果是1就继续入窗口,如果是0就将0的计数加一,然后判断是否小于等于k,不超过k就是合理的,如果超过了k个,那么要先更新最大长度,然后移动左指针,移动到0的计数等于k的时候,这样子数组中还有k个0,符合条件,然后右指针继续往右移动,继续查找
代码部分
当0的个数超过k就要更新,并移动左指针,cnt用于计数0,移动左指针说明0个数超了,所以cnt稳定要减一,不然不符合条件
cpp
class Solution {
public:
int longestOnes(vector<int>& nums, int k) {
int left=0;
int right=0;
int cnt=0;
int len=nums.size();
int maxn=-1;
while(right<len){
if(nums[right]==1);
else{
cnt++;
if(cnt>k){
maxn=max(right-left,maxn);
while(nums[left++]!=0);
cnt--;
}
}
right++;
}
maxn=max(right-left,maxn);
return maxn;
}
};
4.将x减到0的最小操作数
1658. 将 x 减到 0 的最小操作数 - 力扣(LeetCode)

这道题正向操作不是很方便,我们可以采取反方向的操作,也就是找到最长的子数组,使得子数组的和等于数组总和减去x,因为题目要求是从左或者右减去元素使得这些元素和为x,那么剩下的元素和应该就是数组总和减去x
这样就可以使用滑动窗口的方法来解决,当窗口元素小于目标,继续入窗口,大于目标就出窗口,找到符合条件的数组就更新长度,然后右指针继续移动,继续上述操作,注意入窗口每次入一个,但是出窗口要循环遍历,因为下一次大循环如果发现大于目标值还是要移动左指针,不如一次性移动到位
代码部分
先遍历一遍数组求总和,然后得到目标值t,注意这里的sum,n,x要使用longlong类型的整型,我在写完代码的时候,使用的int,然后有一个提交报错是超出整型范围了,也就是题目给了一个int的边界值,然后进行加减,这时候就超范围了,所以用longlong来避免范围问题
还有注意一下别忘记返回值,操作数应该是数组长度减去子数组的长度
cpp
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
long long sum=0;
long long n=0;
int maxn=-1;
int len=nums.size();
for(auto e:nums){
n+=e;
}
long long t=n-x;
int left=0;
int right=0;
while(right<len){
sum+=nums[right];
while(left<=right&&sum>t){
sum-=nums[left];
left++;
}
if(sum==t){
maxn=max(maxn,right-left+1);
}
right++;
}
return maxn==-1?-1:len-maxn;
}
};
5.水果成篮

这道题目的意思就是找到最长的一段子数组,该子数组中最多包含两种数字
同样可以用哈希表,采用int映射int的方式,每次入窗口,如果没见过,那么种类计数加一,如果见过继续移动,当种类计数加一后判断是否超出两种,如果超出就需要更新结果,然后出窗口,直到窗口中只存在两种水果,一种水果不在窗口的判断条件是value等于0,也就是左指针移动时,要将对应元素的计数减一,直到某个元素计数为0,此时种类减一,然后重复操作
代码部分
注意最后多加一次判断
cpp
class Solution {
public:
int totalFruit(vector<int>& fruits) {
unordered_map<int,int> HashMap;
int left=0;
int right=0;
int len=fruits.size();
int cnt=0;
int maxlen=0;
while(right<len){
if(HashMap[fruits[right]]==0){
cnt++;
HashMap[fruits[right]]++;
if(cnt>2)maxlen=max(maxlen,right-left);
while(cnt>2){
if(--HashMap[fruits[left]]==0)cnt--;
left++;
}
}
else HashMap[fruits[right]]++;
right++;
}
maxlen=max(maxlen,right-left);
return maxlen;
}
};
6.找到字符串串中所有字母异位词
438. 找到字符串中所有字母异位词 - 力扣(LeetCode)

这道题目可以采用哈希表记录p中的字母,只要滑动窗口中的元素都在哈希表中就成立,就不用考虑字母次序了,只要是这几个字母即可
并且这道题的滑动窗口长度是固定的,长度为p的长度,不然肯定不符合条件
我们采取两个哈希表对照的方式进行遍历,字符串s为hash2,字符串p为hash1,先遍历字符串p然后将hash1写好,然后使用滑动窗口以固定长度滑动,每次入窗口时记录有效字符个数
什么是有效字符个数呢,首先这个字符存在于p中,然后hash2和hash1对应字符的个数,hash2中的个数要小于等于hash1,说明p中字符还没有全部取到,这就是有效字符,如果hash2中对应的字符个数大于hash1,那么就是无效字符,例如p中有两个a,此时滑动窗口中包含了3个a,多出来的a就不计入有效字符个数,直到有效字符个数等于p的长度,也就是p中所有字符均被包含到了,此时就记录下窗口最左侧的下标,放入vector中,然后继续移动窗口
注意出窗口时也要判断是否删除了一个有效字符
代码部分
因为数据只包含小写字母,直接采用数组作为哈希表就可以,开销比较小
入窗口时我这里是判断有没有空位,也就是加上新加入的字符之后有没有超过hash1中的个数,所以是小于而不是小于等于
因为一开始左右指针均在最左侧,所以加了一个长度判断,直到窗口长度等于p的长度,开始整体滑动,然后其余代码就按照逻辑来,如果是有效字符那么cnt++,cnt==p.size()时说明符合条件,然后出窗口也判断一下出的字符是否是有效字符,继续调整cnt
注意移动窗口时,不管是否有效,入窗口的字符在哈希表中计数一定++,出窗口的字符计数一定--
cpp
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
vector<int> v;
int hash1[26] = {0};
int hash2[26] = {0};
int len = s.size();
int num = p.size();
int cnt = 0;
for (auto e : p) {
hash1[e - 'a'] += 1;
}
int left = 0;
int right = 0;
while (right < len) {
if (hash1[s[right] - 'a'] &&
hash2[s[right] - 'a'] < hash1[s[right] - 'a']) {
cnt++;
}
hash2[s[right] - 'a']++;
if (right - left + 1 == num) {
if (cnt == num)
v.push_back(left);
if (hash1[s[left] - 'a'] &&
hash2[s[left] - 'a'] <= hash1[s[left] - 'a']) {
cnt--;
}
hash2[s[left] - 'a']--;
left++;
}
right++;
}
return v;
}
};
7.串联所有单词的字串

有了第六题的铺垫,其实和第六题本质是一样的,只不过将字符映射改为字符串映射即可,代码逻辑是一样的
注意几点细节,每次移动指针要按照words中字符串长度进行移动,以及边界的判断和窗口的长度都和words中字符串长度挂钩
代码部分
因为每次移动都是按照words字符串长度移动的,所以会漏掉几种情况,假设长度为p,那么从下标0~p-1都是不同的情况,所以外层要套一层循环,遍历p中情况
注意这里的right的位置是一段字符串的开始位置,如图,如果是蓝色箭头的位置,应该是前两块红色区域表示的字符串,如果是绿色箭头的位置,表示前三块红色区域的字符串,但是很明显黄色部分超出了边界,所以在判断边界时,要满足right+p<=s.size()
对于取字符串的操作,我们可以用substr这个函数,取pos位置开始总共n个字符,返回一个字符串,字符串长度的判断就用这一行代码:right - left == p * (num - 1)

cpp
class Solution {
public:
vector<int> findSubstring(string s, vector<string>& words) {
unordered_map<string, int> HashMap1;
for (auto e : words) {
HashMap1[e] += 1;
}
vector<int> v;
int len = s.size();
int num = words.size();
int p = words[0].size();
int k = 0;
while (k < p) {
unordered_map<string, int> HashMap2;
int cnt = 0;
int left = k;
int right = k;
while (right + p <= len) {
if (HashMap2[s.substr(right, p)] <
HashMap1[s.substr(right, p)]) cnt++;
HashMap2[s.substr(right, p)]++;
if (right - left == p * (num - 1)) {
if (cnt == num)v.push_back(left);
if (HashMap2[s.substr(left, p)] <=
HashMap1[s.substr(left, p)])cnt--;
HashMap2[s.substr(left, p)]--;
left += p;
}
right += p;
}
k++;
}
return v;
}
};
8.最小覆盖字串

这道题同样是通过有效字符加哈希表的方式
s对应hash2,t对应hash1,先遍历t字符串,然后在字符对应的位置计数,对于有效字符可以换一种方式,也就是有效字符的种类,例如ABBC,有效字符种类是3,也就是ABC三个字符,然后入窗口时,入窗口的字符对应的个数和hash1中相等时,有效字符种类加一,直到有效字符种类和t中相等,更新结果
然后开始出窗口,每次更新长度,同时要控制好有效字符的种类,如果出窗口后该元素个数小于hash1,那么有效字符种类要减少,此时停止出窗口,继续向右入窗口
代码部分
cpp
class Solution {
public:
string minWindow(string s, string t) {
unordered_map<char, int> hash1;
unordered_map<char, int> hash2;
int kinds = 0;
for (auto e : t) {
if (hash1[e]++ == 0) {
kinds++;
}
}
int len = s.size();
string ret;
int left = 0;
int right = 0;
int pos = 0;
int minlen = INT_MAX;
int cnt=0;
while (right < len) {
if (++hash2[s[right]] == hash1[s[right]])
cnt++;
while (cnt == kinds) {
if (minlen > right - left + 1) {
pos = left;
minlen = right - left + 1;
}
if (hash2[s[left]]-- == hash1[s[left]])
cnt--;
left++;
}
right++;
}
if (minlen == INT_MAX)
minlen = 0;
ret = s.substr(pos, minlen);
return ret;
}