文章目录
- 上期回顾
- 滑动窗口
-
- 1,长度最小的子数组⭐
- 2,无重复字符的最长子串⭐⭐
- [3,最大连续1的个数 III⭐⭐](#3,最大连续1的个数 III⭐⭐)
- [4,将 x 减到 0 的最小操作数⭐⭐](#4,将 x 减到 0 的最小操作数⭐⭐)
- 5,水果成篮⭐⭐⭐
- 6,找到字符串中所有字母异位词⭐⭐⭐
- 7,串联所有单词的子串⭐⭐⭐⭐
- 下期预告
- 结语

◆ 博主名称:此生决int
大家好,欢迎来到我的博客~
⭐ 个人专栏:快速复习系列
⭐ 热门专栏:算法基础到精通系列
大家好,我是此生决int,欢迎来到今天的算法专题 !
今天我们正式学习算法道路上的第一个经典算法------滑动窗口。双指针为什么能把原本 O(n²) 的暴力解法优化到 O(n)?它又为什么会成为面试中的高频考点?我们接着往下看吧!
上期回顾
上期我们主要介绍了左右指针""快慢指针 等经典的双指针算法。
文章概要
本文将系统讲解算法中的经典思想------滑动窗口算法。文章会通过 7 道 LeetCode 高频经典题,由浅入深地讲解滑动窗口的核心思想与常见套路。从最基础的最短子数组问题,到字符串哈希滑窗,再到固定步长窗口与分组滑窗,逐步带大家掌握滑动窗口的完整知识体系,无论你是算法初学者,还是正在准备 LeetCode、蓝桥杯、ICPC 等算法竞赛的同学,都能通过这篇文章快速建立对滑动窗口算法的理解与应用能力。
滑动窗口算法简介
什么是滑动窗口?
滑动窗口的本质其实就是双指针,即同向双指针!
如果让你暴力枚举所有区间,时间复杂度往往是 O(n²)。
但很多题目里,我们其实没必要"重新遍历"整个区间 。这时候,滑动窗口就登场了。
滑动窗口本质上是:
用 left 和 right 两个指针维护一个动态区间,让窗口像尺子一样在数组或字符串上滑动,从而在线性时间内解决问题。
它最大的魅力就在于:
很多看似需要两层循环的问题,都能被优化到 O(n)。
什么时候可以使用滑动窗口?
判断一个题目能不能用滑动窗口,关键看一个特征:
左右指针是否具有
"单调不回退"性质。
也就是说:
right指针不断向右扩展窗口left指针在需要时向右收缩窗口- 两个指针都只向前走,不会后退
因此,每个元素最多被访问两次,时间复杂度通常可以降到:O(n)
接下来,我们就从最经典的题型开始,彻底掌握滑动窗口的核心套路。
本章算法题的简单总结(建议最后看)
1,滑动窗口
-
- 长度最小的子数组
-
- 最大连续1的个数 III
-
- 将 x 减到 0 的最小操作数
2,滑动窗口+哈希
- 将 x 减到 0 的最小操作数
-
- 无重复字符的最长子串
-
- 水果成篮
-
- 找到字符串中所有字母异位词
-
- 串联所有单词的子串
滑动窗口
1,长度最小的子数组⭐
题目链接:
解题思路:
题目要求我们寻找"长度最小"的连续子数组,本质上就是在数组中维护一个动态区间,因此可以使用滑动窗口(双指针)来解决。
我们用 left 和 right 两个指针维护当前窗口,并记录窗口内元素之和 sum 。
由于数组中的元素全部为正数,因此窗口具有单调性:
当 sum < target 时,说明当前窗口不满足条件,需要扩大窗口,让 right 右移。
当 sum >= target 时,说明当前窗口已经满足条件,此时可以尝试缩小窗口,让 left 右移,从而寻找更短的满足条件的子数组。
在整个过程中,left 和 right 都只会向右移动一次,因此时间复杂度为 O(n)。
解题代码
cpp
/*
解题思路:
使用滑动窗口(双指针)维护一个连续区间[left, right]。
1. 当窗口内元素和 sum 小于 target 时:
说明当前窗口不满足条件,需要扩大窗口,right 右移。
2. 当 sum >= target 时:
说明当前窗口已经满足条件,此时尝试缩小窗口,
更新最小长度后让 left 右移,寻找更短的满足条件的子数组。
由于 left 和 right 都只会向右移动一次,时间复杂度为 O(n)。
*/
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int left = 0, right = 0;
// 当前窗口的元素和
int sum = nums[0];
// 记录最小长度
int ret = 0x3f3f3f3f;
// 滑动窗口
while (right < nums.size() && left <= right)
{
// 当前窗口和不足 target,扩大窗口
if (sum < target)
{
right++;
// 防止越界
if (right < nums.size())
sum += nums[right];
}
else
{
// 更新最小长度
ret = min(ret, right - left + 1);
// 缩小窗口
sum -= nums[left];
left++;
}
}
// 如果没有满足条件的子数组,返回 0
return ret == 0x3f3f3f3f ? 0 : ret;
}
};
没懂?看看大神的解题代码!!
大神解题代码
cpp
class Solution
{
public:
int minSubArrayLen(int target, vector<int>& nums)
{
int n = nums.size();
int sum = 0; // 窗口内元素和
int ret = INT_MAX; // 记录最小长度,初始为最大值
// 滑动窗口:left为窗口左边界,right为窗口右边界
for(int left = 0, right = 0; right < n; right++)
{
sum += nums[right]; // 右指针右移,元素加入窗口
// 当窗口和≥target时,收缩左边界,更新最小长度
while(sum >= target)
{
ret = min(ret, right - left + 1); // 更新当前窗口的长度
sum -= nums[left++]; // 移除左边界元素,左指针右移
}
}
// 若ret仍为INT_MAX,说明无有效子数组,返回0;否则返回ret
return ret == INT_MAX ? 0 : ret;
}
};
2,无重复字符的最长子串⭐⭐
题目链接:
解题思路:
题目要求我们寻找"最长的不含重复字符的子串",本质上也是在字符串中维护一个动态区间,因此可以使用滑动窗口(双指针)来解决。
我们用 left 和 right 两个指针维护当前窗口,并使用哈希表记录窗口中每个字符出现的次数。
当某个字符出现重复时,说明当前窗口已经不合法,此时需要不断移动 left 缩小窗口,直到窗口中不存在重复字符为止。
在窗口始终合法的过程中,我们不断更新窗口长度的最大值即可。
由于 left 和 right 都只会向右移动一次,因此时间复杂度为 O(n)。
解题代码
cpp
/*
解题思路:
使用滑动窗口(双指针)维护一个连续区间[left, right]。
1. right 指针不断向右扩展窗口,
并统计当前字符出现的次数。
2. 如果当前字符出现重复:
说明窗口不合法,此时移动 left 缩小窗口,
直到窗口重新变成无重复字符。
3. 在窗口合法的过程中,
不断更新最长子串长度。
由于 left 和 right 都只会向右移动一次,
所以时间复杂度为 O(n)。
*/
class Solution
{
public:
int lengthOfLongestSubstring(string s)
{
// 数组模拟哈希表,记录字符出现次数
int hash[128] = {0};
// 滑动窗口左右边界
int left = 0, right = 0;
int 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;
}
};
没懂?看看大神的解题代码!!
大神解题代码
cpp
class Solution
{
public:
int lengthOfLongestSubstring(string s)
{
int hash[128] = {0}; // 记录窗口内字符出现次数
int left = 0, right = 0;
int ret = 0;
while(right < s.size())
{
hash[s[right]]++; // 当前字符进入窗口
// 当前字符重复,缩小窗口
while(hash[s[right]] > 1)
{
hash[s[left]]--;
left++;
}
// 更新最长长度
ret = max(ret, right - left + 1);
right++; // 扩大窗口
}
return ret;
}
};
3,最大连续1的个数 III⭐⭐
题目链接:
解题思路:
题目要求我们求最长的连续 1 的个数,并且最多可以将 k 个 0 翻转成 1。
本质上就是在数组中维护一个合法区间,因此可以使用滑动窗口(双指针)来解决。
我们用 left 和 right 两个指针维护当前窗口,并用变量 zero 记录窗口中 0 的个数。
由于题目允许最多翻转 k 个 0,因此:
-
当窗口内 0 的个数
<= k时:当前窗口合法,可以更新答案。
-
当窗口内 0 的个数
> k时:当前窗口不合法,需要移动
left缩小窗口,直到窗口重新合法。
在整个过程中,left 和 right 都只会向右移动一次,因此时间复杂度为 O(n)。
解题代码
cpp
/*
解题思路:
使用滑动窗口(双指针)维护一个连续区间[left, right]。
1. right 指针不断向右扩展窗口,
并统计窗口内 0 的个数。
2. 当窗口内 0 的个数大于 k 时:
说明当前窗口不合法,
需要移动 left 缩小窗口。
3. 当窗口重新合法后,
更新窗口的最大长度。
由于 left 和 right 都只会向右移动一次,
所以时间复杂度为 O(n)。
*/
class Solution {
public:
int longestOnes(vector<int>& nums, int k) {
// left 和 right 为滑动窗口左右边界
int left = 0;
int right = 0;
// 记录窗口内 0 的个数
int zero = 0;
// 记录最长合法窗口长度
int retlen = 0;
// 滑动窗口
while(right < nums.size())
{
// 当前元素为 0
if(nums[right] == 0)
{
zero++;
}
// 窗口不合法,缩小窗口
while(zero > k)
{
// 左边界为 0,窗口内 0 的个数减少
if(nums[left] == 0)
{
zero--;
}
// 左边界右移
left++;
}
// 更新最大长度
retlen = max(retlen, right - left + 1);
// 扩大窗口
right++;
}
return retlen;
}
};
没懂?看看大神的解题代码!!
大神解题代码
cpp
class Solution
{
public:
int longestOnes(vector<int>& nums, int k)
{
int ret = 0; // 记录窗口最大长度
// 滑动窗口:left为左边界,right为右边界
// zero记录窗口内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;
}
};
4,将 x 减到 0 的最小操作数⭐⭐
题目链接:
解题思路:
题目要求我们每次从数组的最左边或者最右边删除元素,并让这些被删除元素的和等于 x。
直接模拟删除会比较复杂,因此我们可以换个角度思考:
- 删除左右两边元素的和为
x - 等价于:中间保留一段连续子数组,其和为
sum - x
并且:
- 删除元素个数最少
- 等价于:保留的子数组长度最长
因此问题就转换成:
求和为
sum - x的最长连续子数组长度。
由于数组中的元素都为正数,因此可以使用滑动窗口解决。
我们用 left 和 right 维护窗口,并记录窗口元素和:
-
当窗口和大于 target 时:
缩小窗口。
-
当窗口和等于 target 时:
更新最长子数组长度。
最后:
- 操作次数 = 数组总长度 - 最长子数组长度。
由于 left 和 right 都只会向右移动一次,因此时间复杂度为 O(n)。
解题代码
cpp
/*
解题思路:
题目要求从数组左右两边删除元素,
使删除元素之和等于 x。
可以转换思路:
删除左右元素之和为 x,
等价于保留中间一段连续子数组,
其和为 sum - x。
因此问题转换成:
求和为 target 的最长连续子数组长度。
最后:
操作次数 = 数组总长度 - 最长子数组长度。
由于数组元素全为正数,
可以使用滑动窗口解决。
*/
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
// 计算数组总和
int sum = 0;
for(auto it : nums)
{
sum += it;
}
// 目标窗口和
int target = sum - x;
// 总和小于 x,无法完成操作
if(target < 0)
return -1;
// 整个数组都需要删除
if(target == 0)
return nums.size();
// 滑动窗口左右边界
int left = 0, right = 0;
// 当前窗口和
int tmp = 0;
// 记录最长合法窗口长度
int len = 0;
// 滑动窗口
while(right < nums.size())
{
// 当前元素进入窗口
tmp += nums[right];
// 窗口和过大,缩小窗口
while(tmp > target)
{
tmp -= nums[left];
left++;
}
// 找到合法窗口
if(tmp == target)
{
len = max(len, right - left + 1);
}
// 扩大窗口
right++;
}
// 没有找到合法窗口
if(len == 0)
return -1;
// 最少操作次数
return nums.size() - len;
}
};
5,水果成篮⭐⭐⭐
题目链接:

解题思路:
题目要求我们寻找一个最长的连续子数组,并且这个子数组中最多只能包含两种水果。
本质上就是:
求"至多包含两种元素"的最长连续区间。
因此可以使用滑动窗口(双指针)解决。
我们用 left 和 right 维护当前窗口,并使用哈希表记录窗口中每种水果出现的次数。
由于窗口中最多只能有两种水果:
-
当窗口内水果种类数
<= 2时:当前窗口合法,可以更新答案。
-
当窗口内水果种类数
> 2时:当前窗口不合法,需要移动
left缩小窗口。
在整个过程中,left 和 right 都只会向右移动一次,因此时间复杂度为 O(n)。
解题代码
cpp
/*
解题思路:
使用滑动窗口(双指针)维护一个连续区间[left, right]。
1. right 指针不断向右扩展窗口,
并统计窗口内水果出现次数。
2. 当窗口内水果种类超过 2 种时:
说明当前窗口不合法,
需要移动 left 缩小窗口。
3. 当窗口重新合法后,
更新最长窗口长度。
由于 left 和 right 都只会向右移动一次,
所以时间复杂度为 O(n)。
*/
class Solution {
public:
int totalFruit(vector<int>& fruits) {
// 滑动窗口左右边界
int left = 0, right = 0;
// 哈希表记录水果出现次数
unordered_map<int, int> mp;
// 记录最长合法窗口长度
int len = 0;
// 滑动窗口
while(right < fruits.size())
{
// 当前水果进入窗口
mp[fruits[right]]++;
// 水果种类超过 2 种,缩小窗口
while(mp.size() > 2)
{
// 左边界水果移出窗口
mp[fruits[left]]--;
// 数量为 0,移除该水果
if(mp[fruits[left]] == 0)
{
mp.erase(fruits[left]);
}
// 左边界右移
left++;
}
// 更新最长长度
len = max(len, right - left + 1);
// 扩大窗口
right++;
}
return len;
}
};
没懂?看看大神的解题代码!!
大神解题代码
cpp
class Solution
{
public:
int totalFruit(vector<int>& f)
{
unordered_map<int, int> hash; // 统计窗口内水果数量
int ret = 0; // 记录最长合法窗口长度
// 滑动窗口:
// left 为左边界
// right 为右边界
for(int left = 0, right = 0; right < f.size(); right++)
{
// 当前水果进入窗口
hash[f[right]]++;
// 水果种类超过 2 种,缩小窗口
while(hash.size() > 2)
{
hash[f[left]]--;
// 当前水果数量为 0,移除
if(hash[f[left]] == 0)
{
hash.erase(f[left]);
}
left++;
}
// 更新最长长度
ret = max(ret, right - left + 1);
}
return ret;
}
};
6,找到字符串中所有字母异位词⭐⭐⭐
题目链接:

解题思路:
题目要求我们找到字符串 s 中所有与字符串 p 的字母异位词。
所谓字母异位词,就是:
两个字符串包含的字符种类和数量完全相同,只是顺序不同。
由于我们需要寻找的是:
长度固定为
p.size()的连续子串。
因此可以使用固定长度的滑动窗口来解决。
我们使用两个哈希数组:
hash1:记录字符串p中每个字符出现的次数。hash2:记录当前窗口中每个字符出现的次数。
同时使用 count 记录当前窗口中"有效字符"的数量。
滑动窗口过程:
-
右边界进入窗口时:
更新字符出现次数。
-
当窗口长度超过
p.size()时:左边界出窗口,维持窗口长度固定。
-
当有效字符数量等于
p.size()时:说明当前窗口就是一个字母异位词。
由于每个字符最多进出窗口一次,因此时间复杂度为 O(n)。
解题代码
cpp
/*
解题思路:
使用固定长度滑动窗口维护长度为 p.size() 的区间。
1. hash1 记录字符串 p 中字符出现次数。
2. hash2 记录当前窗口中字符出现次数。
3. count 记录窗口内有效字符数量。
4. 当窗口长度超过 p.size() 时:
移动 left 缩小窗口。
5. 当 count == p.size() 时:
说明当前窗口为字母异位词。
时间复杂度为 O(n)。
*/
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
// 记录 p 中字符出现次数
int hash1[26] = {0};
for(auto it : p)
{
hash1[it - 'a']++;
}
// 保存答案
vector<int> ret;
// 记录窗口中字符出现次数
int hash2[26] = {0};
// count 记录有效字符数量
for(int left = 0, right = 0, count = 0; right < s.size(); right++)
{
// 当前字符进入窗口
hash2[s[right] - 'a']++;
// 当前字符属于有效字符
if(hash2[s[right] - 'a'] <= hash1[s[right] - 'a'])
{
count++;
}
// 窗口长度超过 p.size()
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);
}
}
return ret;
}
};
没懂?看看大神的解题代码!!
大神解题代码
cpp
class Solution
{
public:
vector<int> findAnagrams(string s, string p)
{
// cnt1 记录 p 中字符出现次数
int cnt1[26] = {0};
// cnt2 记录窗口中字符出现次数
int cnt2[26] = {0};
// 统计 p 中字符频率
for(char ch : p)
{
cnt1[ch - 'a']++;
}
vector<int> ret;
// 固定长度滑动窗口
for(int left = 0, right = 0; right < s.size(); right++)
{
// 当前字符进入窗口
cnt2[s[right] - 'a']++;
// 窗口长度超过 p.size()
if(right - left + 1 > p.size())
{
// 左边界字符移出窗口
cnt2[s[left] - 'a']--;
left++;
}
// 判断当前窗口是否为字母异位词
if(cnt1 == cnt2)
{
ret.push_back(left);
}
}
return ret;
}
};
7,串联所有单词的子串⭐⭐⭐⭐
题目链接:
解题思路:
题目要求我们找到:
由
words中所有单词按任意顺序拼接形成的子串。
并且:
- 每个单词长度相同
- 每个单词必须全部使用一次
因此我们可以使用:在这里插入代码片
"固定步长滑动窗口"
来解决。
假设:
- 每个单词长度为
n - 单词总数为
m
那么合法窗口长度一定为:
m * n
cpp
/*
解题思路:
由于所有单词长度相同,
因此可以使用固定步长滑动窗口。
1. 每次窗口移动一个单词长度。
2. mp1 记录 words 中单词出现次数。
3. mp2 记录当前窗口中单词出现次数。
4. count 记录当前窗口中有效单词数量。
5. 当窗口长度超过总长度时:
移动 left 缩小窗口。
6. 当 count == words.size() 时:
当前窗口为合法答案。
由于需要处理不同起点,
因此需要枚举 0 ~ n-1 的所有起点。
*/
class Solution {
// 获取从 j 开始长度为 n 的字符串
string gets(int j, int n, string s)
{
string tmp = "";
while(n--)
{
tmp += s[j++];
}
return tmp;
}
public:
vector<int> findSubstring(string s, vector<string>& words) {
// 单词长度
int n = words[0].size();
// 统计 words 中单词出现次数
unordered_map<string, int> mp1;
for(auto it : words)
{
mp1[it]++;
}
vector<int> ret;
// 枚举不同起点
for(int m = 0; m < n; m++)
{
// 当前窗口单词统计
unordered_map<string, int> mp2;
// 滑动窗口
for(int left = m, right = m, count = 0;
right + n <= s.size();
right += n)
{
// 当前单词进入窗口
string tmp = s.substr(right, n);
mp2[tmp]++;
// 当前单词有效
if(mp2[tmp] <= mp1[tmp])
{
count++;
}
// 窗口长度超过限制
if((right - left + n) > (words.size() * n))
{
// 左边界单词移出窗口
string tmp2 = gets(left, n, s);
// 当前单词属于有效单词
if(mp2[tmp2] <= mp1[tmp2])
{
count--;
}
mp2[tmp2]--;
// 数量为 0 时移除
if(mp2[tmp2] == 0)
{
mp2.erase(tmp2);
}
// 缩小窗口
left += n;
}
// 找到合法窗口
if(count == words.size())
{
ret.push_back(left);
}
}
}
return ret;
}
};
没懂?看看大神的解题代码!!
大神解题代码
cpp
class Solution
{
public:
vector<int> findSubstring(string s, vector<string>& words)
{
int n = words[0].size(); // 每个单词长度
int m = words.size(); // 单词数量
// 统计 words 中每个单词出现次数
unordered_map<string, int> target;
for(auto& word : words)
{
target[word]++;
}
vector<int> ret;
// 枚举不同起点
for(int i = 0; i < n; i++)
{
unordered_map<string, int> window;
// 滑动窗口:
// left 为左边界
// right 为右边界
// count 为有效单词数量
for(int left = i, right = i, count = 0;
right + n <= s.size();
right += n)
{
// 当前单词进入窗口
string word = s.substr(right, n);
window[word]++;
// 当前单词有效
if(window[word] <= target[word])
{
count++;
}
// 窗口长度超过要求
while(right - left + n > m * n)
{
// 左边界单词
string del = s.substr(left, n);
// 当前单词有效
if(window[del] <= target[del])
{
count--;
}
// 左边界单词移出窗口
window[del]--;
if(window[del] == 0)
{
window.erase(del);
}
left += n;
}
// 找到合法窗口
if(count == m)
{
ret.push_back(left);
}
}
}
return ret;
}
};
下期预告
今天的分享就到这里,下一期我们将一起学习二分算法!!!
结语
本期内容就到这里啦,欢迎大家在评论区一起交流讨论
如果你也在为蓝桥杯/ACM备赛头疼,或是准备算法面试找不到系统学习路径,欢迎订阅我的「算法从入门到精通」专栏!
这里没有枯燥的理论堆砌,只有完整的算法学习路线 ,
搭配精选梯度习题+清晰思路解析,帮你把每个算法学透、练熟。包教包会的!
我们一起在算法路上稳步进阶!




