滑动窗口大致分为两类:一类是窗口长度固定的,即left和right可以一起移动;另一种是窗口的长度变化(例如前五道题),即right疯狂移动,left没怎么动,这类题需要观察单调性(即指针)等各方面因素综合思考
长度最小的子数组
但随着right向后移动,虽然sum可以一直增长满足条件,但是len已经在不断增长,不符合题意,所以right在移动到2的时候就可以停下来了
3. **之后left继续移动到下一个数字3,此时我们会让right重新回到3的位置,但此时我们发现,在left处于2时,随着right的移动图示中红框的部分我们其实已经知道了,即用当时所求的sum减2即8-2=6即可,这样也剩去了接下来的遍历枚举,并且right也不需要回到原来的位置。**
![image.png](https://cdn.nlark.com/yuque/0/2023/png/29339358/1701163390629-a702e58c-ea0e-4106-bcd3-34a573227d5f.png#averageHue=%23fefefe&clientId=ub4e97224-715f-4&from=paste&height=410&id=uce25b86b&originHeight=512&originWidth=582&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=62156&status=done&style=none&taskId=u1ffc1c3d-5bce-4e4c-9caf-6d75274133d&title=&width=465.6)
滑动窗口一般是用来维护信息,即本题中我们通过[left,right]这个区间维护这个区间的sum。当做题用暴力解法时发现两个指针同向移动不回退时,可以用滑动窗口。
进窗口
判断是否出窗口
更新结果这一步,需要结合实际题目具体分析,有时候需要进窗口的时候更新结果,有的时候需要出窗口时更新结果,此题出窗口前更新结果。
滑动窗口的正确性:利用单调性,规避掉了很多没有必要的枚举
时间复杂度:从代码角度看,好像是两层循环嵌套,时间复杂度似乎也是O(n2),但是实际情况我们对窗口进行操作时,left、right每次只移动了一步(即我们的两个指针不回退),最多两个一共移动n+n次,即时间复杂度为O(n)
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums)
{
int n=nums.size(),sum=0,len=INT_MAX;//因为len最终是要取最小值的,如果初始化为0,会影响。
for(int left = 0,right = 0;right<n;right++)
{
sum += nums[right]; //进窗口
while(sum >= target) //判断
{
len = min(len,right-left+1);
sum -= nums[left]; //出窗口
left++;
}
}
return len == INT_MAX?0:len; //如果测试用例没有结果,就返回0
}
};
无重复字符的最长子串
暴力枚举+哈希表(判断字符是否重复出现)开始遍历使,将每个字母都存入哈希表里,开始移动并记录长度,对比表里是否有重复的字母。时间复杂度O(n2)
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int hash[128] = {0}; //用数组模拟哈希表
int left = 0,right = 0,n=s.size();
int ret = 0;
while(right<n)
{
hash[s[right]]++; //进入窗口
while(hash[s[right]] > 1) //判断
{
hash[s[left++]]--; //出窗口,哈希表--,然后移动left, ++
}
ret = max(ret,right-left+1); //判断
right++; //让下一个元素进入窗口
}
return ret;
}
};
最大连续1的个数
题目中翻转指的是可以把0变为1。
转化:找出最长的子数组,0的个数不超过k个
class Solution {
public:
int longestOnes(vector<int>& nums, int k) {
int ret = 0;
for( int left = 0,right = 0,zero = 0;right < nums.size(); right++)
{
if(nums[right] == 0) zero++; //进窗口
while(zero > k) //判断
if(nums[left++] == 0) zero--; //出窗口
ret = max(ret,right-left+1); //更新结果
}
return ret;
}
};
将x减到0的最小操作数
- 正难则反,正常情况下是在左边选一个区间,右边选一个区间,让这两个区间的和等于x,就可以满足。但是正面作战不利,我们可以反过来,在中间部分找一个连续的区间,使他们和为sum-x也行。(这里的sum是整个数组的和)。
- 题目还要求操作次数最短,即找的两边的区间长度需要最短,那换言之就是使中间的区间长度最长即可。
暴力枚举:固定left和right,用sum标记[left,right]所指的这段区域的和,判断sum和target之间的关系,如果sum
滑动窗口
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
int sum = 0;
for(int a:nums) sum += a;
int target = sum-x;
//细节问题:如果有小于0的元素,单调性不存在
if(target < 0) return -1;
int ret = -1; //防止有小于0的元素
for(int left = 0,right = 0,temp = 0;right < nums.size();right++) //temp代替sum
{
temp+=nums[right]; //进窗口
while(temp > target ) //判断
temp -= nums[left++]; //出窗口
if(temp == target)
ret = max(ret,right-left+1);
}
if(ret == -1) return ret;
else return nums.size()-ret;
}
};
水果成篮
转化:找出一个最长的子数组,且子数组中的水果种类不能超过两种
class Solution {
public:
int totalFruit(vector<int>& fruits) {
unordered_map<int,int>hash; //统计窗口内出现的水果数量
int ret = 0;
for(int left = 0,right = 0;right<fruits.size();right++)
{
hash[fruits[right]]++; //进窗口
while(hash.size() > 2)
{
//出窗口
hash[fruits[left]]--; //left位置对应的水果数量--
if(hash[fruits[left]] == 0)
hash.erase(fruits[left]);
left++;
}
ret = max(ret,right-left+1);
}
return ret;
}
};
这里因为我们频繁的向哈希表中插入数据,所以时间复杂度不是很好。观察题目要求的数据后(数据范围有限),我们可以用数组模拟哈希表。
class Solution
{
public:
int totalFruit(vector<int>& f)
{
int hash[100001] = { 0 }; // 统计窗⼝内出现了多少种⽔果
int ret = 0;
for(int left = 0, right = 0, kinds = 0; right < f.size(); right++)
{
if(hash[f[right]] == 0) kinds++; // 维护⽔果的种类
hash[f[right]]++; // 进窗⼝
while(kinds > 2) // 判断
{
// 出窗⼝
hash[f[left]]--;
if(hash[f[left]] == 0) kinds--;
left++;
}
ret = max(ret, right - left + 1);
}
return ret;
}
};
找到字符串中所有字母的异位词
这里也可以利用数组模拟哈希表,因为题目中说都是小写字母,那么我们可以设置一个大小为26的数组,在数组为0的位置放a,为1的位置放b…让里面存储的值表示出现的次数,这样我们在判断hash1和hash2时只需要比较26次。这里的时间复杂度为O(n)
进窗口维护:当right进入窗口,统计第一个字符为c,此时记录c的次数为1,P中的c也出现了一次,(当窗口中的c出现次数小于等于P中,此时能相互匹配,这时c为有效字符),令count++,right移动下一个位置c,这时c出现了两次,但是P中只出现了一次,即不是有效字符,count不变,right继续往下移动到a…此时count等于3(P的长度),则此时窗口中全是字母的异位词,返回left的位置即可。
出窗口维护:当窗口大于P的长度时,需要移动left即出窗口删掉字符,left移动到第二个c之前我们可以观察此时窗口中c的次数为2大于P中的,即为无效字符,所以count不需要变化。left移动到第二个c之后,c变为一次,此时是P的异位词,输出起始位置。同理left移动到字符b时(此时c仅一次,小于等于P),c为有效字符,改变count的值为2,这时只需要判断count是否等于P的长度就能从而决定我们是否要更新结果。
class Solution {
public:
vector<int> findAnagrams(string s, string p)
{
vector<int> ret;
int hash1[26] = { 0 }; // 统计字符串 p 中每个字符出现的个数
for(auto ch : p) hash1[ch - 'a']++;
int hash2[26] = { 0 }; // 统计窗⼝⾥⾯的每⼀个字符出现的个数
int m = p.size();
for(int left = 0, right = 0, count = 0; right < s.size(); right++)
{
char in = s[right];
// 进窗⼝ + 维护 count
++hash2[in - 'a'];
if(hash2[in - 'a'] <= hash1[in - 'a']) count++;
if(right - left + 1 > m) // 判断
{
char out = s[left++];
// 出窗⼝ + 维护 count
if(hash2[out - 'a'] <= hash1[out - 'a']) count--;
hash2[out - 'a']--;
}
// 更新结果
if(count == m) ret.push_back(left);
}
return ret;
}
};
串联所有单词的子串
先将s的字符串按照w中长度的进行划分:
class Solution {
public:
vector<int> findSubstring(string s, vector<string>& words) {
vector<int>ret;
unordered_map<string,int> hash1; //保存world单词出现的次数
for(auto&s :words) hash1[s]++;
int len = words[0].size(),m = words.size();
for(int i = 0; i < len; i++) //执行len次
{
unordered_map<string,int> hash2; //维护窗口内单词的频次
for(int left = i,right = i,count = 0; right+len <= s.size();right+=len)
{
//进窗口+维护count
string in = s.substr(right,len); //将要进窗口的字符串裁出来,从right位置开始,长度为len的子串
hash2[in]++;
if(hash1.count(in) && hash2[in] <= hash1[in]) count++;
/*这里hash1中不一定有in,hash[]的特性是如果没有in会再重新创建一个,
较消耗时间复杂度。所以可以先加一个判断条件hash1.count(in)*/
//判断
if(right-left+1 > len*m) //当大于words的总长度时,移动窗口
{
//出窗口+维护count
string out = s.substr(left,len);
//这里同理
if(hash1.count(out) && hash2[out] <= hash1[out]) count--;
hash2[out]--; //出窗口,即哈希表里的字符数--
left+=len; //left移动
}
//更新结果
if(count == m) ret.push_back(left);
}
}
return ret;
}
};
最小覆盖子串
因为这里都是字符,所以我们可以定义数组来模拟hash
class Solution {
public:
string minWindow(string s, string t) {
int hash1[128] = {0}; //统计t中出线的频次
int kind = 0;//统计有效字符种类
for(auto ch:t)
{
if(hash1[ch] == 0) kind++;
hash1[ch]++;
}
int hash2[128] = {0}; //统计窗口内每个字符出现的频次
int minlen = INT_MAX, begin = -1;
for(int left = 0,right = 0,count = 0;right<s.size();right++)
{
char in = s[right];
hash2[in]++;
if(hash2[in] == hash1[in]) count++; //进窗口+维护
while(count == kind) //判断条件
{
if(right-left+1 < minlen) //更新结果
{
minlen = right - left + 1;
begin = left;
}
char out = s[left];
left++;
if(hash2[out] == hash1[out]) count--;
hash2[out]--;
}
}
if(begin == -1) return "";
else return s.substr(begin,minlen);
}
};