欢迎来到 s a y − f a l l 的文章 欢迎来到say-fall的文章 欢迎来到say−fall的文章

🌈 say-fall:个人主页 🚀 专栏:《手把手教你学会C++》 | 《C语言从零开始到精通》 | 《数据结构与算法》 | 《小游戏与项目》 💪 格言:做好你自己,才能吸引更多人,与他们共赢,这才是最好的成长方式。
前言:
本文系统讲解双指针与滑动窗口算法,从暴力枚举出发,逐步推导出滑动窗口的核心思想,并配合多道 LeetCode 例题加深理解。掌握这一技巧,能将大量 O(n²) 的问题优化到 O(n)。
文章目录
- 前言:
- 正文:
-
- 算法思想
- 长度最小的子数组
- 无重复字符的最长字串
- 最大连续1的个数Ⅲ
-
- [解法一:暴力枚举 和 zero计数器](#解法一:暴力枚举 和 zero计数器)
- 解法二:滑动指针优化
- 将x减到零的最小操作数
- 水果成篮
- 找到字符串中所有字符异位词
- 串联所有单词的字串
- 总结
正文:
算法思想
滑动窗口算法,实际上其本质就是同向双指针 ,同向双指针之间夹有 有用区间 或者 无用区间 ,这个时候区间的某种数据就是结果。
双指针按形态可以分为三类:
| 类型 | 方向 | 典型场景 |
|---|---|---|
| 对撞指针 | 相向 | 有序数组两端夹逼 |
| 快慢指针 | 同向不同速 | 链表环、中点 |
| 滑动窗口 | 同向同速,窗口伸缩 | 连续子数组/子串 |
滑动窗口的核心步骤:
- 进窗口:右指针右移,新元素加入窗口
- 判断:检查窗口是否满足/违反条件
- 出窗口:左指针右移,旧元素移出窗口,更新结果
- 重复上述过程直到右指针越界
长度最小的子数组

题意 :给定正整数数组 nums 和目标值 target,找长度最小的连续子数组,使其元素之和 ≥ target,返回其长度,不存在则返回 0。
解法一:多层循环暴力枚举
本题可以直接双层循环+sum记录大小
cpp
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int sum = 0,ret = INT_MAX;
for(int left = 0;left < nums.size();left++)
{
sum = 0;
for(int right = left;right < nums.size();right++)
{
sum += nums[right];
if(sum >= target)
{
ret = min(right - left + 1,ret);
break;
}
}
}
if(ret == INT_MAX) return 0;
return ret;
}
};
而很显然,这种方法太慢了,既然已经每增加一个数字sum都是变大的,利用单调性,我们能不能不让right每次都重复移动,而是需要时候向后移动:这就是滑动窗口算法的核心。
解法二:滑动窗口
- 进窗口
- 判断
- ---->出窗口,更新结果
- ---->继续进窗口
cpp
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int left = 0,right = 0,sum = 0, ret = 0;
while(right < nums.size())
{
sum += nums[right];
while(sum >= target)
{//判断,出窗口
if(ret == 0 || ret > right - left + 1) ret = right - left + 1;
sum -= nums[left];
left++;
}
//进窗口
right++;
}
return ret;
}
};
💡 为什么正确? 数组全为正整数,sum 具有单调性:right 右移 sum 只增,left 右移 sum 只减。因此每个元素最多被 left 和 right 各访问一次,时间复杂度 O(n)。
无重复字符的最长字串

题意 :给定字符串 s,找其中不含重复字符的最长子串的长度。
这题的思路明显和上一题是类似的,只不过没有 target 作为出窗口的判断条件,而是判断字串中有没有重复字符,我们可以使用一个类哈希表来处理。
滑动窗口
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;
}
};
💡 关键点 :用
hash[128]充当字符频次表(ASCII 共 128 个字符),hash[s[right]] > 1说明s[right]在窗口内重复出现,此时持续收缩左边直到不重复为止。
最大连续1的个数Ⅲ

题意 :给定二进制数组 nums 和整数 k,最多可以将 k 个 0 翻转为 1,返回最长连续 1 的子数组长度。
转化:子数组中 0 的个数 ≤ k,求满足条件的最长子数组。取消翻转操作,因为题目没有要求修改原数组。
解法一:暴力枚举 和 zero计数器
cpp
// 伪代码:
for(int left = 0;left < n;left++)
{
for(right = left;right < n;right++)
{
// 统计 [left, right] 内 0 的个数
// 若 zero <= k,更新结果
}
}
时间复杂度 O(n²),数据量大时超时。
解法二:滑动指针优化
cpp
class Solution {
public:
int longestOnes(vector<int>& nums, int k) {
int left = 0,right = 0,ret = 0,n = nums.size(),zero = 0;
for(left = 0,right = 0;right < n;right++)//进窗口
{
if(nums[right] == 0)
{
zero++;
}
while(zero > k)//判断:0 的个数超出 k
{
if(nums[left] == 0) zero--;
left++;//出窗口
}
ret = max(ret,right - left + 1);//更新结果
}
return ret;
}
};
💡 思路 :窗口内维护 0 的计数器
zero,一旦zero > k就收缩左边。每次 right 右移后更新最大长度。
将x减到零的最小操作数
题意:每次操作可以从数组最左或最右取一个数,使 x 减去该数,求使 x 恰好为 0 的最少操作数。
转化 :从两端取数使和为 x,等价于找中间连续子数组使和为 sum - x,且该子数组长度最大(操作数 = n - 子数组长度)。
cpp
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
int left = 0, right = 0, n = nums.size();
int sum = 0;
for (auto e : nums)
{
sum += e;
}
int t = sum - x;
if (t < 0) return -1; // 总和都不够,无解
if (t == 0) return n; // 整个数组就是答案
int ret = -1;
sum = 0;
while (right < n)
{
sum += nums[right]; // 进窗口
while (sum > t) sum -= nums[left++]; // 出窗口
if (sum == t) ret = max(right - left + 1, ret); // 更新结果
right++;
}
if (ret == -1) return -1;
return n - ret;
}
};
💡 正难则反:直接模拟从两端取数很复杂,转化为求中间最长子数组是滑动窗口的经典套路。
水果成篮
题意:给定水果类型数组,有两个篮子,每个篮子只能装一种水果,从某棵树开始连续摘,求最多能摘多少水果。
转化:找最长子数组,使其中水果种类 ≤ 2。
cpp
class Solution {
public:
int totalFruit(vector<int>& fruits) {
int left = 0, right = 0, ret = 0, kinds = 0;
int n = fruits.size();
int hash[100001] = { 0 };
for (right = 0;right < n;right++)
{
if (hash[fruits[right]] == 0) kinds++; // 新种类进入
hash[fruits[right]]++; // 进窗口
while (kinds > 2) // 判断:超过2种
{
hash[fruits[left]]--;
if (hash[fruits[left]] == 0) kinds--; // 种类消失
left++; // 出窗口
}
ret = max(ret, right - left + 1);
}
return ret;
}
};
💡 哈希表维护种类数 :
kinds记录窗口内水果种类,hash记录每种水果数量。种类超过 2 时收缩左边,直到某种水果数量归零(种类减少)。
找到字符串中所有字符异位词
题意 :给定字符串 s 和 p,找 s 中所有 p 的异位词的起始索引。
思路 :固定窗口大小为 p.size(),用 count 记录窗口内有效字符数(频次不超过 p 中对应字符数的字符),count == p.size() 时即为异位词。
cpp
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
int left = 0, right = 0;
vector<int> ans;
int count = 0;
int hash1[26] = {0}; // p 的字符频次
int hash2[26] = {0}; // 窗口内字符频次
int len = p.size();
for (int i = 0; i < len; i++)
hash1[p[i] - 'a']++;
for (right = 0; right < s.size(); right++)
{
int in = s[right] - 'a';
// 进窗口:遇到 p 中没有的字符,重置窗口
if (hash1[in] == 0)
{
left = right + 1;
count = 0;
memset(hash2, 0, sizeof(hash2));
continue;
}
hash2[in]++;
if (hash2[in] <= hash1[in]) count++; // 有效字符数+1
// 出窗口:窗口超过 len
if (right - left + 1 > len)
{
int out = s[left] - 'a';
hash2[out]--;
left++;
if (hash2[out] < hash1[out]) count--; // 有效字符数-1
}
// 更新结果
if (count == len)
ans.push_back(left);
}
return ans;
}
};
💡 count 的含义 :
count统计窗口内"有效"字符数,即频次不超过 p 中对应字符频次的字符总数。count == len说明窗口恰好是 p 的一个异位词。
串联所有单词的字串
题意 :给定字符串 s 和单词列表 words(每个单词等长),找 s 中所有由 words 中所有单词串联而成的子串的起始位置。
思路 :单词长度为 len,以 0, 1, ..., len-1 为起点分别做滑动窗口,步长为 len,窗口大小固定为 n * len。
cpp
class Solution {
public:
vector<int> findSubstring(string s, vector<string>& words) {
vector<int> ans;
int count = 0;
unordered_map<string, int> hash1;
int n = words.size();
for (auto s : words) hash1[s]++;
unordered_map<string, int> hash2;
int len = words[0].size();
for (int i = 0; i < len; i++) // 枚举起点,共 len 种
{
hash2.clear();
count = 0;
for (int left = i, right = i + len; right <= (int)s.size(); right += len)
{
// 进窗口
string in = s.substr(right - len, len);
hash2[in]++;
if (hash1.count(in) && hash2[in] <= hash1[in]) count++;
// 出窗口:窗口超过 n*len
if (right - left > n * len)
{
string out = s.substr(left, len);
left += len;
hash2[out]--;
if (hash1.count(out) && hash2[out] < hash1[out]) count--;
}
// 更新结果
if (count == n) ans.push_back(left);
}
count = 0;
hash2.clear();
}
return ans;
}
};
💡 为什么枚举 len 个起点? 因为单词长度固定为
len,所有可能的起点只有0 ~ len-1这len种,每种起点独立做一次滑动窗口即可覆盖所有情况。
总结
双指针
├── 对撞指针 → 有序数组,两端夹逼(两数之和、三数之和)
├── 快慢指针 → 链表,环/中点/倒数第k个
└── 滑动窗口 → 连续子数组/子串,窗口伸缩
├── 可变窗口 → 最小子数组、最长无重复子串、最大连续1
└── 固定窗口 → 字母异位词、串联所有单词的子串
滑动窗口的本质 :利用数据的单调性 ,避免重复枚举。每个元素最多被 left 和 right 各访问一次,时间复杂度 O(n) ,空间复杂度 O(1) 或 O(k)(k 为字符集大小)。
套路口诀:
进窗口 → 判断 → 出窗口 → 更新结果
- 本节完...