滑动窗口:解决连续区间问题的黄金模板(练习笔记)
滑动窗口:解决连续区间问题的黄金模板!
由于此问题分析的对象是「⼀段连续的区间」,因此可以考虑「滑动窗⼝」的思想来解决这道题。
滑动窗口其实就是双指针的升级版,但此时这两个指针的移动方向是一致的
长度最小的子数组
题⽬链接:
长度最小的子数组
要点:
- 寻找连续子数组使其和 ≥ target
- 滑动窗口维护当前窗口和,动态调整左右边界
老师代码:
class Solution
{
public:
int minSubArrayLen(int target, vector<int>& nums)
{
int n = nums.size(), sum = 0, len = INT_MAX;
for(int left = 0, right = 0; right < n; right++)
{
sum += nums[right]; // 进窗⼝
while(sum >= target) // 判断
{
len = min(len, right - left + 1); // 更新结果
sum -= nums[left++]; // 出窗⼝
}
}
return len == INT_MAX ? 0 : len;
}
};
老师思路:
-
由于此问题分析的对象是**「⼀段连续的区间」,因此可以考虑「滑动窗⼝」**的思想来解决这道题。
让滑动窗⼝满⾜:从 i 位置开始,窗⼝内所有元素的和⼩于 target (那么当窗⼝内元素之和第⼀次⼤于等于⽬标值的时候,就是 i 位置开始,满⾜条件的最⼩⻓度)。 做法:将右端元素划⼊窗⼝中,统计出此时窗⼝内元素的和
-
如果窗⼝内元素之和⼤于等于 target :更新结果,并且将左端元素划出去的同时继续判断是否满⾜条件并更新结果(因为左端元素可能很⼩,划出去之后依旧满⾜条件),如果窗⼝内元素之和不满⾜条件: right++ ,另下⼀个元素进⼊窗⼝
我的代码:
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int ret = 0, sum = 0;
int left = 0, right = 0;
//第一次情况
sum += nums[right];
while(sum < target)
{
right++;
if(right >= nums.size()) return 0;//边界问题
sum += nums[right];
}
ret = right - left + 1;
while(right < nums.size())
{
while(sum >= target)
{
ret = right - left + 1 < ret ? right - left + 1 : ret;
sum -= nums[left++];
}
right++;
if(right < nums.size()) sum += nums[right];
}
return ret;
}
};
我的思路:
- 初始化左右指针
left=0, right=0
,窗口和sum=0
right
右移扩大窗口,累加元素值到sum
- 当
sum ≥ target
时,记录窗口长度,并尝试左移left
缩小窗口 - 最终返回最小窗口长度
我的笔记:
-
len(ret)最开始时不能定义成0,要不然没有比0更小的数组了,老师的想法就是len = INT_MAX,我的想法就是先找到第一个满足条件的子数组,把这个数组的长度作为ret的初始值
-
这个数组的所有元素都是正整数,因此right++,sum += nums[right] 的值一定是增加的,left–,sum -= nums[left]的值一定是减小的,因此可以使用滑动窗口来解决
-
下标每一次变化,如果要使用下标对应的元素,都要判断一下边界问题
-
先找到第一个满足条件的窗口,作为初始结果
-
所有元素为正数,保证窗口和单调性,适合滑动窗口
-
边界处理:当数组总和小于
target
时返回0
无重复字符的最长子串
题⽬链接:
无重复字符的最长子串
要点:
- 寻找不含重复字符的最长连续子串
- 哈希表记录字符最后一次出现的位置
老师代码:
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++]]--; // 出窗⼝
ret = max(ret, right - left + 1); // 更新结果
right++; // 让下⼀个元素进⼊窗⼝
}
return ret;
}
};
老师思路:
-
研究的对象依旧是⼀段连续的区间,因此继续使⽤「滑动窗⼝」思想来优化。
-
让滑动窗⼝满⾜:窗⼝内所有元素都是不重复的。
-
做法:右端元素 ch 进⼊窗⼝的时候,哈希表统计这个字符的频次:
▪ 如果这个字符出现的频次超过 1 ,说明窗⼝内有重复元素,那么就从左侧开始划出窗⼝,直到 ch 这个元素的频次变为 1 ,然后再更新结果。
▪ 如果没有超过 1 ,说明当前窗⼝没有重复元素,可以直接更新结果

我的代码:
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int arr[128] = {0};//相当于状态寄存器
int left = 0, right = 0, ret = 0;
while(right < s.size() )
{
if(arr[(int)s[right]] == 0)//如果这个字符没有出现过
{
arr[(int)s[right]]++;//对应标志位置1
right++;//进窗口
}
else//如果出现过,开始出窗口
{
while(left < right && s[left] != s[right])//循环删除
{
arr[(int)s[left]] = 0;//标志位重新置0
left++;//出窗口
}
//到了相等的位置,出窗口
arr[(int)s[left]] = 0;//标志位重新置0
left++;//出窗口
}
//更新结果
ret = max(ret, right - left);
}
return ret;
}
};
我的思路:
- 使用数组
hash[128]
模拟哈希表,记录字符是否在窗口内 right
右移时检查字符是否重复- 若重复,移动
left
到重复字符的下一个位置,并更新哈希表
我的笔记:
- 哈希表直接存储字符的存在状态,时间复杂度O(n)
- 左指针跳跃式移动,避免无效检查
- 关键代码:
ret = max(ret, right - left)
最大连续1的个数
题⽬链接:
最大连续1的个数
要点:
- 允许翻转k个0为1,求最长连续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;
}
}
老师思路:
不要去想怎么翻转,不要把问题想的很复杂,这道题的结果⽆⾮就是⼀段连续的 1 中间塞了 k 个 0 嘛。 因此,我们可以把问题转化成:求数组中⼀段最⻓的连续区间,要求这段区间内 0 的个数不超过 k 个。
我的代码:
class Solution {
public:
int longestOnes(vector<int>& nums, int k) {
int num = 0, left = 0, right = 0;//right是下一个要判断的数字
int ret = 0;
while(right < nums.size())
{
if(nums[right] == 0)//如果为0,判断是否还有翻转名额
{
if(num >= k)//没有翻转名额
{
//出窗口
while(left < right && nums[left] != 0) left++;//left找到最近的0位置
left++;//left来到最近的0位置的后一个位置
num--;//释放一个名额
}
else//名额还有
{
//进窗口
num++;
right++;
}
}
else//如果为1
{
//进窗口
right++;
}
//更新结果
ret = max(ret, right - left);
}
return ret;
}
};
我的思路:
- 维护窗口内0的计数
zero
- 当
zero > k
时,左移left
直到zero ≤ k
- 窗口大小即为当前有效区间长度
我的笔记:
- 贪心思想:尽可能保留更多1,通过翻转0扩展窗口
- 时间复杂度O(n),空间复杂度O(1)
- 关键优化:直接统计0的个数,避免复杂逻辑
将x减到0的最小操作数
题⽬链接:
将x减到0的最小操作数
要点:
- 从数组两端删除元素,使得删除总和等于x
- 转化为寻找中间连续子数组,其和等于
total_sum - x
老师代码:
class Solution
{
public:
int minOperations(vector<int>& nums, int x)
{
int sum = 0;
for(int a : nums) sum += a;
int target = sum - x;
// 细节问题
if(target < 0) return -1;
int ret = -1;
for(int left = 0, right = 0, tmp = 0; right < nums.size(); right++)
{
tmp += nums[right]; // 进窗⼝
while(tmp > target) // 判断
tmp -= nums[left++]; // 出窗⼝
if(tmp == target) // 更新结果
ret = max(ret, right - left + 1);
}
if(ret == -1) return ret;
else return nums.size() - ret;
}
}
老师思路:
题⽬要求的是数组「左端+右端」两段连续的、和为 x 的最短数组,信息量稍微多⼀些,不易理清思路;我们可以转化成求数组内⼀段连续的、和为 sum(nums) - x 的最⻓数组。此时,就是熟悉的「滑动窗⼝」问题了。
我的代码:
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
//求目标值
int target = 0;
for(int i = 0; i < nums.size(); i++)
{
target += nums[i];
}
target -= x;
//计算
int left = 0, right = 0, sum = 0, ret = -1;
while(right < nums.size())
{
//进窗口
sum += nums[right++];
while(left < right && sum > target)//判断是否要出窗口
{
//出窗口
sum -= nums[left];
left++;
}
//结果更新判断
if(sum == target)
{
ret = max(ret, right - left);
}
}
if(ret == -1) return -1;
else return nums.size() - ret;
}
};
第二版
class Solution {
private:
vector<int> ret;
public:
vector<int> findAnagrams(string s, string p) {
int hash1[26] = { 0 };
for(auto e : p) hash1[e - 'a']++;
int hash2[26] = { 0 };
int left = 0, right = 0, count = 0;
while(right < s.size())
{
hash2[s[right] - 'a']++;
if(hash2[s[right] - 'a'] <= hash1[s[right] - 'a']) count++;//维护count
//这里不需要while循环,因为窗口大小是固定的,如果进入if语句只能是大了一个数
if(right - left + 1 > p.size())
{
if(hash2[s[left] - 'a'] <= hash1[s[left] - 'a']) count--;
hash2[s[left] - 'a']--;
left++;
}
if(count == p.size()) ret.push_back(left);
right++;
}
return ret;
}
};
我的思路:
- 计算目标值
target = sum(nums) - x
- 滑动窗口寻找和为
target
的最长子数组 - 最小操作数为
总长度 - 最长子数组长度
我的笔记:
-
虽然题目中没有说这个数组的数都是正整数,但我们可以从题目下面的提示可以看到,其实这个数组的数都是正整数,因此我们就会想到单调性;又由于相当于找连续子数组,所以就可以用滑动窗口的方法
做题时一定要看完题目
-
正难则反:如果一个问题从正面难解决,我们就可以反过来看一下
-
逆向思维:将两端删除问题转化为中间子数组问题
-
边界处理:当
target < 0
时直接返回-1 -
关键代码:
ret = max(ret, right - left)
找到字符串中所有字母异位词
题⽬链接:
找到字符串中所有字母异位词
要点:
- 寻找s中包含p所有字符排列的子串
- 固定窗口大小为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++)//count用于计算滑动窗口中的元素个数
{
char in = s[right];
// 进窗⼝ + 维护 count
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--;
}
// 更新结果
if(count == m) ret.push_back(left);//当滑动窗口的元素个数与p元素个数一致时
}
return ret;
}
}
老师思路:老师的方法
- 因为字符串 p 的异位词的⻓度⼀定与字符串 p 的⻓度相同,所以我们可以在字符串 s 中构造⼀个⻓度为与字符串 p 的⻓度相同的滑动窗⼝,并在滑动中维护窗⼝中每种字⺟的数量;
- 当窗⼝中每种字⺟的数量与字符串 p 中每种字⺟的数量相同时,则说明当前窗⼝为字符串 p 的异位词;
- 因此可以⽤两个⼤⼩为 26 的数组来模拟哈希表,⼀个来保存 s 中的⼦串每个字符出现的个数,另⼀个来保存 p 中每⼀个字符出现的个数。这样就能判断两个串是否是异位词。
- 在更新结果时我们需要比较两个哈希表是否相同,可以用我的代码中的硬比较方法(遍历),但那种方法不能应对哈希表比较长的情况,老师的方法是利用count来统计窗口中有效字符个数,如果有效字符个数与p元素个数一致时 这里我不好描述,一定要去看视频
我的代码:
class Solution {
private:
vector<int> ret;
public:
//用于比较两个数组是否相同
bool compare(int* arrs, int* arrp)
{
for(int i = 0; i < 26; i++)
{
if(arrp[i] != arrs[i])
{
return false;
}
}
return true;
}
//用于判断是否应该执行出窗口
bool compare2(int* arrs, int* arrp)
{
for(int i = 0; i < 26; i++)
{
//如果p中没有但s中有,或s中的某个字符多余p中的数量
//返回真,执行出窗口
if((arrp[i] == 0 && arrs[i] != 0) ||
arrp[i] < arrs[i])
{
return true;
}
}
return false;
}
vector<int> findAnagrams(string s, string p) {
int arrp[26] = { 0 };
int arrs[26] = { 0 };
for(int i = 0; i < p.size(); i++)
{
arrp[p[i] - 'a']++;
}
int left = 0, right = 0;
while(right < s.size())
{
arrs[s[right++] - 'a']++;//进窗口
while(left < right && compare2(arrs, arrp))//判断出窗口条件
{
arrs[s[left++] - 'a']--;//出窗口
}
if(compare(arrs, arrp))//判断更新结果的条件
{
ret.push_back(left);
}
}
return ret;
}
};
我的思路:
- 用哈希表
hash1
记录p的字符频率 - 滑动窗口维护
hash2
记录当前窗口字符频率 - 通过
count
变量统计有效字符个数,避免全表比较
我的笔记:
- 一定要注意字符串中的字符的ASCII码表对应的数组下标的值,对于数组下标要减去一个’a’才是从0开始的对应位置
- 我的思路是利用滑动窗口,每进窗口一个字符判断一下两个数组(哈希表)的元素关系,------硬比较
- 如果p中没有而s中有,则s的滑动窗口中进入了p中没有的字母,我们需要出窗口
- 如果arrs[i] > arrp[i] 则说明i这个位置的字母多了,我们需要出窗口,
- 当这两个数组的元素个数一致时说明找到了,就返回left;
- 老师的方法更容易理解一些,因为这次的窗口大小是固定的 要学会利用这个特性
- 固定窗口大小:
right - left + 1 == p.size()
- 哈希表优化:仅当字符频率匹配时更新
count
- 关键代码:
if(++hash2[in] <= hash1[in]) count++
注意事项总结
C++语法相关:
- 数组初始化:
int hash[128] = {0}
需显式初始化 - 字符处理:
s[right] - 'a'
将字符映射到0-25的索引 - 边界检查:
right < nums.size()
避免越界
算法思路相关:
- 单调性利用:元素为正数时,窗口和具有单调性,适合滑动窗口
- 逆向思维:如“将x减到0”转化为求中间子数组问题
- 哈希表优化:用
count
替代全表比较,时间复杂度从O(26n)降至O(n) - 固定窗口:处理异位词时,窗口大小固定为p的长度,简化逻辑
- 去重技巧:通过跳跃移动左指针(如无重复字符问题),减少无效计算
位置的字母多了,我们需要出窗口,
- 当这两个数组的元素个数一致时说明找到了,就返回left;
- 老师的方法更容易理解一些,因为这次的窗口大小是固定的 要学会利用这个特性
- 固定窗口大小:
right - left + 1 == p.size()
- 哈希表优化:仅当字符频率匹配时更新
count
- 关键代码:
if(++hash2[in] <= hash1[in]) count++
注意事项总结
C++语法相关:
- 数组初始化:
int hash[128] = {0}
需显式初始化 - 字符处理:
s[right] - 'a'
将字符映射到0-25的索引 - 边界检查:
right < nums.size()
避免越界
算法思路相关:
- 单调性利用:元素为正数时,窗口和具有单调性,适合滑动窗口
- 逆向思维:如“将x减到0”转化为求中间子数组问题
- 哈希表优化:用
count
替代全表比较,时间复杂度从O(26n)降至O(n) - 固定窗口:处理异位词时,窗口大小固定为p的长度,简化逻辑
- 去重技巧:通过跳跃移动左指针(如无重复字符问题),减少无效计算