滑动窗口是什么
滑动窗口是数组 / 字符串中最经典的双指针优化算法 ,核心作用是:把暴力解法的 O (n²) 时间复杂度,直接降到 O (n) ,专门解决连续子数组 / 连续子串的最值、求和、匹配问题。
滑动窗口操作
想象一个固定 / 可变长度的窗户 ,在一条直线(数组 / 字符串)上从左向右滑动:
- 窗户左边 :左指针
left - 窗户右边 :右指针
right - 窗户内部 :
[left, right]区间的连续元素(就是我们要处理的目标)
滑动过程:
- 右指针向右移动,扩大窗口,把新元素加入窗口
- 满足条件时,左指针向右移动,缩小窗口,移除旧元素
- 全程只遍历一次数组,没有重复计算
本质 :滑动窗口 = 两个指针同向移动 +维护窗口内的状态 + 一次遍历解决问题
我们来解决一道题目,来分辨一下暴力解法到滑动窗口的丝滑过渡
滑动窗口题目
LCR 008. 长度最小的子数组 - 力扣(LeetCode)

方法一:暴力枚举

1.固定left,right移动
2.累加[left,right]范围内元素
3.如果sum>target,left++,如果sum<target,right--
4.如果sum==target则表示符合条件,我们定义len来计算范围大小
5.重复操作,直到找到最小数组和
这个方法是我们最普遍的想法
方法二:滑动窗口

如上图,我们可以注意到当sum>target时,left向右移动,此时[left,right]范围内的数组和必然小于或者等于target,因此我们不必要每次循环结束后,将right重置为left+1,所以right只需按兵不动即可。如图,这个操作类似于一个窗口在不断的移动以此来保证窗口内的元素符合条件
cpp
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int n = nums.size();
int len = INT_MAX;
int sum = 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;
}
};
滑动窗口最核心的四个步骤
1.入窗
2.判断
3.更新结果
4.出窗
其中出窗和更新结果的顺序需要就题目分析
模板如下:
cpp
int slidingWindowTemplate(vector<int>& nums) {
int left = 0;
int res = ...;
// 窗口状态:和、计数、map 等
int window = 0;
for (int right = 0; right < nums.size(); ++right) {
// 1. 把 nums[right] 加入窗口
window += nums[right];
// 2. 满足条件时,收缩左边界
while (condition) {
// 更新答案
res = min/max(res, right - left + 1);
// 移出左边界
window -= nums[left];
left++;
}
}
return res;
}
3. 无重复字符的最长子串 - 力扣(LeetCode)

在题目中我们发现"最长字串"这几个字符,不难想出利用 滑动窗口 来解决本题
我们之前知道在解决重复字符问题,我们通常会用 哈希表来记录字符出现次数
所以我们可以数组模拟实现哈希表
分四步走
1.入窗
2.判断
3.更新结果
4.出窗
但是本题跟上一题又不同,我们需要返回的是无重复最长字串,所以我们需要出窗之后再更新结果,所以第三步跟第四步对调
cpp
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int Hash[128] = {0};
int left = 0,right = 0,ret = 0;
int n = s.size();
while(right<n)
{
//入窗kou
Hash[s[right]]++;
while(Hash[s[right]]>1)
{
//出窗kou
Hash[s[left++]]--;
}
//更新结果
ret = max(ret,right-left+1);
right++;
}
return ret;
}
};
1004. 最大连续1的个数 III - 力扣(LeetCode)

题目的理解可能有些曲折,但是通过示例还是轻松明了的,就是0翻转为1,让我们求连续1的最大个数
高中数学有个重要的思考方式是发散思维,本题恰好可以应用到,让我们求连续1的最大个数,可以转换为 在区间内最多不超过k个0,有多少个连续的1
cpp
class Solution {
public:
int longestOnes(vector<int>& nums, int k) {
//本题我们转换一下思路
//即为在区间内最多不超过k个0
int ret = 0,zero = 0;
for(int left = 0,right = 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;
}
};
438. 找到字符串中所有字母异位词 - 力扣(LeetCode)

通过示例,很明了的可以知道就是在字符串S中找到字符串P中所有字母
那么查找字母这个问题,很简单就能想到用哈希表来存储数据,但是哈希表因为存在哈希冲突,实际使用起来内存消耗和时间成本是比较大的,所以在算法阶段,我们通常是数组模拟哈希表,因为查找字母嘛,大不了开128个数组空间
在这里我们还是使用哈希表来操作,相对比较好迁移算法思路
方法一:哈希表+暴力
这个方法的思路就是,构建两个哈希表,哈希表1用来存储字符串P中字母的数据(key:字母名 value:出现频次),unordered_map<char,int>hash1; 哈希表二用来控制字符串S,哈希表2一插入元素,就比较一次,直到size跟hash1一致,一致之后,left右移,right回到left位置重新遍历
方法二:哈希表+滑动窗口
在前一个思路中,我们注意到其实right没必要回到left的位置,因为只要left右移之后不再满足条件,right只要继续向右移动即可,这样大大减少了对于中间已经遍历过元素消耗的时间,我们只需要对方法一做一些优化即可实现方法二 ,听起来棒极了
cpp
class Solution {
public:
vector<int>findAnagrams(string s, string p)
{
unordered_map<char, int>hash1;
vector<int>res;
//hash1用来存储字符串P中的数据
for (auto& ch : p)
{
hash1[ch]++;
}
//hash2用来操作滑动窗口
unordered_map<char, int>hash2;
//处理特殊情况,如果字符串P长度大于字符串S,直接返回res
int m = p.size();
int n = s.size();
if (m > n) return res;
for (int left = 0, right = 0; right < m; right++)
{
//入窗口
char in = s[right];
hash2[in]++;
//出窗口 如果滑动窗口的长度大于字符串P的长度,就需要出窗口
if (right - left + 1 > m)
{
char out = s[left];
hash2[out]--;
//如果刚好left此处的值的出现频率降低为0,那么直接删除该元素,降低对哈希表影响
if (hash[out] == 0) hash2.erase(out);
}
left++:
}
//更新结果 长度刚好相等的时候且hash1与hash2相等,此时更新结果
if (right - left + 1 == m)
{
if (hash2 == hash1)
{
res.push(left);
}
}
return res
}
};
76. 最小覆盖子串 - 力扣(LeetCode)

观察题目发现其实跟上一个题目有很大的相似之处
方法一:哈希表+暴力
不做过多赘述,简而言之就是right不用回到left位置,实现一个滑动窗口
方法二:哈希表+滑动窗口
构建两个哈希表,用途与上题一致,但是这道题需要我们返回的是子串,所以我们需要知道该子串的索引
cpp
class Solution {
public:
string minWindow(string s, string t)
{
if (t.size() > s.size()) return "";
}
unordered_map<char, int>need,window;
//need 用来记录需要的字符 window用来操作滑动窗口
for (auto& ch : need)
{
need[ch]++;
}
//索引我们用start来指代
int start = 0, minlen = INT_MAX;
//1.滑动窗口 count用来记录有效字符个数
for (int left = 0, right = 0, count = 0; right < s.size(); right++)
{
//入窗口
char in = s[right];
if (need.count(in)) //如果是t中的字符,更新窗口
{
window[in]++;
if (window[in] == need[in]) count++;
}
while (count == need.size())
{
//更新结果
if(right-left+1<minlen)
{
minlen = right - left + 1;
start = left;
}
//出窗口
char out = s[left];
//如果移除元素是t中的字符,更新window
if (need.count(out))
{
if (window[out] == need[out]) count--;
window[out]--;
}
}
}
return minlen == INT_MAX ? "" : s.substr(start, minlen);
};