当前位置: 首页 > news >正文

滑动窗口专题总结:从懵逼到掌握valid计数器

学习记录

  • 时间: 3天(10.5 - 10.7)
  • 题量: 8题
  • 完成度: 8/8(全部AC,第3题一次AC!)

我的学习过程

这是我算法学习的第5-7天,继续深化滑动窗口。

Day04-05(10.5-10.6): 滑动窗口基础(4题)

  • 和≥target最短子数组、无重复字符、字母异位词、最大连续1
  • 前两题犯了"索引vs值"错误,第3题一次AC了!

Day06(10.7): 滑动窗口进阶(4题)

  • 字符串排列、水果成篮、将x减到0、最小覆盖子串
  • 第1题数组越界,第4题是BOSS题,完全不会,学了valid计数器

滑动窗口的核心概念(我的理解)

什么是滑动窗口?

我的理解就是:用两个指针(left和right)维护一个"窗口",窗口在数组上滑动,每次滑动都更新窗口内的数据。

关键是:

  • 什么时候扩大窗口?(right++)
  • 什么时候缩小窗口?(left++)
  • 什么时候更新结果?

滑动窗口 vs 双指针

相同点: 都是两个指针

不同点:

  • 双指针:两个指针各自独立移动
  • 滑动窗口:两个指针维护一个区间,right一直往右,left按条件移动

滑动窗口的两种类型

通过这8道题,我发现滑动窗口主要有2种:

1. 可变窗口(求最长/最短)

特点: 窗口大小动态变化

适用: 找最长/最短满足某条件的子数组/子串

模板:

for(int left = 0, right = 0; right < n; right++) {// 1. 进窗口进窗口操作(right);// 2. 判断 + 出窗口(条件不满足时收缩)while(/* 条件不满足 */) {出窗口操作(left);left++;}// 3. 更新结果ret = max(ret, right - left + 1);  // 求最长
}

我做过的题:

  • 和≥target最短子数组(LeetCode 209)
  • 无重复字符的最长子串(LeetCode 3)⭐
  • 最大连续1的个数III(LeetCode 1004)
  • 水果成篮(LeetCode 904)
  • 将x减到0(LeetCode 1658)
  • 最小覆盖子串(LeetCode 76)⭐⭐⭐
2. 固定窗口(异位词问题)

特点: 窗口大小固定不变

适用: 找固定长度的满足某条件的子数组/子串

模板:

// 1. 建立第一个窗口
for(int i = 0; i < len; i++) {window[s[i]]++;
}
if(window == target) ret.push_back(0);// 2. 滑动窗口
for(int right = len; right < n; right++) {int left = right - len;// 出窗口window[s[left]]--;if(window[s[left]] == 0) {window.erase(s[left]);  // ⚠️ erase很重要!}// 进窗口window[s[right]]++;// 判断if(window == target) {ret.push_back(left + 1);}
}

我做过的题:

  • 找到字符串中所有字母异位词(LeetCode 438)
  • 字符串的排列(LeetCode 567)

典型题目分类

类型1:可变窗口(求最长)

LeetCode 3. 无重复字符的最长子串 ⭐⭐⭐

我的第一次错误:又是索引vs值!

while(hash[right] > 1) {  // ❌ right是索引,不是字符!hash[s[left++]]--;
}

错在哪: right 是索引(0,1,2…),s[right] 才是字符(‘a’,‘b’,‘c’…)

正确写法:

while(hash[s[right]] > 1) {  // ✅ s[right]是字符hash[s[left++]]--;
}

又是第5次犯这个错误了! 这次我终于意识到:写代码前先问自己,要索引还是值?


完整正确代码:

int left = 0, right = 0;
int hash[128] = {0};  // 字符频次
int ret = 0;while(right < s.size()) {hash[s[right]]++;  // 进窗口while(hash[s[right]] > 1) {  // 有重复,收缩窗口hash[s[left++]]--;}ret = max(ret, right - left + 1);  // 更新结果right++;
}

关键点:

  • 用hash数组记录字符出现次数
  • 当某个字符出现2次时,收缩左边界
  • 求最长,所以在while外更新结果

LeetCode 1004. 最大连续1的个数III

这题是我第一次一次AC!✨

思路: 最多替换k个0 → 找一个窗口,窗口内最多有k个0

int ret = 0;
for(int left = 0, right = 0, zero = 0; right < nums.size(); right++) {if(nums[right] == 0) zero++;  // 进窗口while(zero > k) {  // 0太多了,收缩if(nums[left++] == 0) zero--;}ret = max(ret, right - left + 1);  // 更新结果
}

为什么一次AC?

  • 因为前面做了3题可变窗口,模板已经熟了
  • 而且这次没犯"索引vs值"错误!nums[right] == 0 写对了!

类型2:可变窗口(求最短)

LeetCode 76. 最小覆盖子串 ⭐⭐⭐⭐⭐

这题是滑动窗口的BOSS题!我一开始完全不会。

我的第一次尝试(完全错了):

for(auto c: target) {  // ❌ 应该是 for(auto c: t)target[t[c]]++;
}int len = t.size();
for(int i = 0; i < len; i++) {  // ❌ 以为是固定窗口windows[s[i]]++;
}if(windows >= target) {  // ❌ map不能直接比较// ...
}

错误点:

  1. 统计target时写错了
  2. 以为是固定窗口,其实是可变窗口
  3. 不知道如何判断窗口是否满足条件

核心难点:如何判断窗口包含了所有目标字符?

答案:用 valid 计数器!

int valid = 0;  // 满足条件的字符种类数// 进窗口时:
if(window[c] == target[c]) {  // 刚好满足valid++;
}// 判断满足条件:
if(valid == target.size()) {// 所有字符都满足了!
}// 出窗口时:
if(window[d] == target[d]) {  // 出窗口前刚好满足valid--;  // 出窗口后就不满足了
}

为什么用valid?

如果不用valid,需要每次都遍历map比较:

// ❌ 这样太慢了(每次O(k))
bool check() {for (auto& [ch, cnt] : target) {if (window[ch] < cnt) return false;}return true;
}

用valid只需要O(1):

// ✅ 只需要判断一个变量
if (valid == target.size()) {// 满足条件
}

完整正确代码:

unordered_map<char, int> target, window;
for (char c : t) target[c]++;int left = 0, right = 0, valid = 0;
int start = 0, len = INT_MAX;while (right < s.size()) {// 进窗口char c = s[right++];if (target.count(c)) {window[c]++;if (window[c] == target[c]) valid++;}// 收缩窗口(满足条件时尝试缩小)while (valid == target.size()) {// 更新结果(求最短,在while内更新)if (right - left < len) {start = left;len = right - left;}// 出窗口char d = s[left++];if (target.count(d)) {if (window[d] == target[d]) valid--;window[d]--;}}
}return len == INT_MAX ? "" : s.substr(start, len);

关键点:

  1. valid计数器:记录满足要求的字符种类数(不是字符个数!)
  2. 求最短:更新结果在while内(和求最长相反)
  3. 返回字符串:要记录start和len,不能只记录长度
  4. 窗口移动:right一直往右,left只在满足条件时才往右收缩

类型3:固定窗口(异位词问题)

LeetCode 438. 找到字符串中所有字母异位词

我的第一次错误:

for(int j = left; j < right; j++) {  // ❌ 应该是 j <= rightd[s[j]]++;
}

问题1: 初始窗口统计不完整,漏掉了最后一个字符

问题2: 每次都重新统计整个窗口,应该是增量更新

问题3: 窗口移动顺序错了


正确思路(固定窗口):

  1. 先统计p的字符频次
  2. 初始化第一个完整窗口 [0, len-1]
  3. 循环:判断 → 出窗口 → 进窗口
// 1. 统计p
for (char c : p) target[c]++;// 2. 初始化第一个窗口
for (int i = 0; i < len; i++) {window[s[i]]++;
}
if (window == target) ret.push_back(0);// 3. 滑动窗口
for (int right = len; right < n; right++) {int left = right - len;// 出窗口window[s[left]]--;if (window[s[left]] == 0) {window.erase(s[left]);  // 必须erase!}// 进窗口window[s[right]]++;// 判断if (window == target) {ret.push_back(left + 1);}
}

为什么必须erase?

如果不erase,值为0的key还在map里:

target = {'a':1, 'b':1, 'c':1}  // 3个key
window = {'a':1, 'b':1, 'c':1, 'd':0}  // 4个keywindow == target?false// key数量不同(3 vs 4)

C++的map比较会比较"key的数量",即使value=0,key存在就算不同!


LeetCode 567. 字符串的排列

这题和上一题几乎一样,只是返回bool。

我的错误:数组越界

int len = s1.size();
for(int i = 0; i < len; i++) {  // ❌ 如果s2比s1短,越界!windows[s2[i]]++;
}

教训:固定窗口的第0步:先判断边界!

if(s2.size() < s1.size()) return false;  // 必须先判断

类型4:思维转换(难点)

LeetCode 1658. 将x减到0的最小操作数

这题的关键是思维转换!

原问题: 从左边或右边移除元素,使得移除的和 = x,求最小操作数

转换后: 找中间最长连续子数组,使得和 = sum - x


为什么可以转换?

数学推导:

[左边移除] + [中间保留] + [右边移除] = nums设:
- 左边移除的和 = L
- 中间保留的和 = M
- 右边移除的和 = R
- 总和 = sum = L + M + R原问题要求:L + R = x
→ sum - M = x
→ M = sum - x目标转换:
最小化移除的元素个数
= 最大化保留的元素个数
= 找最长的中间连续子数组,和 = sum - x

正确代码:

int sum = 0;
for(int c : nums) sum += c;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);}
}return ret == -1 ? -1 : nums.size() - ret;

关键点:

  • 转换后变成标准的可变窗口求最长子数组
  • 答案 = 总长度 - 保留的长度

我踩的坑(总结)

坑1:固定窗口的边界判断(第1次遇到)

int len = s1.size();
for(int i = 0; i < len; i++) {window[s2[i]]++;  // ❌ 如果s2比s1短,越界!
}

教训: 固定窗口第0步永远是:先判断边界!

if(s2.size() < s1.size()) return false;

坑2:固定窗口必须erase

window[s[left]]--;
// ❌ 如果不判断,值为0的key还在

必须判断:

if(window[s[left]] == 0) {window.erase(s[left]);
}

原因: map比较时会比较key的数量,值为0的key也算一个key。


坑3:可变窗口的更新位置

求最长: 更新结果在while外

while(条件不满足) {出窗口;
}
ret = max(ret, right - left + 1);  // 在while外

求最短: 更新结果在while内

while(条件满足) {ret = min(ret, right - left);  // 在while内出窗口;
}

原因:

  • 求最长:窗口越大越好,收缩后更新(取收缩前的最大值)
  • 求最短:窗口越小越好,收缩前更新(取收缩中的最小值)

坑4:valid计数器的维护(最难)

进窗口:

if(window[c] == target[c]) {  // 刚好满足时valid++;
}

出窗口:

if(window[d] == target[d]) {  // 出窗口前刚好满足valid--;
}
window[d]--;  // 注意顺序!先判断再减

如果顺序错了:

window[d]--;  // ❌ 先减
if(window[d] == target[d]) {valid--;  // 判断的是减之后的值,错了!
}

坑5:窗口移动的理解

我一开始的疑惑: 满足条件就收缩,不满足就不收缩?还是一定会往右走?

答案: 两个指针独立移动!

  • right: 一直往右走(每次外层循环都+1)
  • left: 只在满足条件时往右收缩(内层循环)
while(right < n) {          // 外层:right一直往右进窗口;right++;while(条件满足) {       // 内层:left只在满足条件时才往右更新结果;出窗口;left++;}
}

我的薄弱环节

  • valid计数器的维护:最小覆盖子串的valid还不够熟练
  • 思维转换能力:将x减到0的转换思路,要再理解一遍
  • 固定窗口 vs 可变窗口:有时不知道该用哪个

下一步计划

  • 最小覆盖子串要再做一遍(valid计数器是核心)
  • 异位词问题要再做一遍(erase的理解)
  • 继续学习其他专题(可能是栈、队列或二分查找)

典型模板总结

可变窗口模板(求最长)

for(int left = 0, right = 0; right < n; right++) {进窗口(right);while(条件不满足) {出窗口(left);left++;}ret = max(ret, right - left + 1);  // 在while外
}

可变窗口模板(求最短)

while(right < n) {进窗口(right);right++;while(条件满足) {ret = min(ret, right - left);  // 在while内出窗口(left);left++;}
}

固定窗口模板

// 第0步:判断边界
if(n < len) return false;// 第1步:初始化第一个窗口
for(int i = 0; i < len; i++) {window[s[i]]++;
}
if(window == target) ret.push_back(0);// 第2步:滑动窗口
for(int right = len; right < n; right++) {int left = right - len;// 出窗口window[s[left]]--;if(window[s[left]] == 0) window.erase(s[left]);// 进窗口window[s[right]]++;// 判断if(window == target) ret.push_back(left + 1);
}

最小覆盖子串模板(valid计数器)

unordered_map<char, int> target, window;
for(char c : t) target[c]++;int left = 0, right = 0, valid = 0;while(right < n) {char c = s[right++];if(target.count(c)) {window[c]++;if(window[c] == target[c]) valid++;}while(valid == target.size()) {// 更新结果char d = s[left++];if(target.count(d)) {if(window[d] == target[d]) valid--;window[d]--;}}
}

我的理解

滑动窗口的本质:

  • 双指针的进阶版,两个指针维护一个区间
  • right一直往右走,left按条件收缩
  • 通过维护窗口内的状态(和、频次等)来优化时间复杂度

两种类型的选择:

  1. 固定窗口: 题目要求固定长度 → 异位词问题
  2. 可变窗口: 题目要求找最长/最短 → 大部分滑动窗口题

三个关键操作:

  1. 进窗口: 更新窗口内的状态(sum、hash、count等)
  2. 判断: 检查是否满足条件
  3. 出窗口: 更新状态,移动left

更新结果的位置:

  • 求最长:在while外更新(窗口越大越好)
  • 求最短:在while内更新(窗口越小越好)

什么时候复习:

  • 最小覆盖子串必须再做一遍(valid计数器是核心技巧)
  • 字母异位词再做一遍(erase的理解)
  • 将x减到0再做一遍(思维转换)

valid计数器的理解:

  • 记录的是"满足要求的字符种类数",不是字符个数
  • window[c] == target[c] 时才 valid++(刚好满足)
  • 出窗口时也要判断(先判断再减)
  • 这是滑动窗口最难的技巧,但也是最核心的

写于: 2025年10月8日
刷题进度: 9题 → 17题
上个专题: 双指针(已完成9题)
下个专题: 栈和队列 或 二分查找

http://www.dtcms.com/a/457630.html

相关文章:

  • 深圳市盐田区建设局网站WordPress制作安卓
  • Next.js useState useEffect useRef 速记
  • 图论算法刷题的第五十一天
  • Linux自动化构建工具make/Makefile及Linux下的第一个程序—进度条
  • Vue使用原生方式把视频当作背景
  • 铜陵app网站做招聘信息wordpress第一篇文章id
  • 从玩具到工业:基于 CodeBuddy code CLI 构建电力变压器绕组短路智能诊断系统
  • wordpress 中英文网站模板手机创建网页
  • 基于 GEE 的 Sentinel-2 光谱、指数、纹理特征提取与 Sentinel-1 SAR 数据处理
  • 嘉兴网站排名优化价格windows 安装 wordpress
  • 2-C语言中的数据类型
  • 免费企业营销网站制作公司建网站有何意义
  • LeetCode算法日记 - Day 66: 衣橱整理、斐波那契数(含记忆化递归与动态规划总结)
  • 建行官方网站网站模块数据同步
  • HTTP 协议的基本格式
  • 【代码】洛谷 P6150 [USACO20FEB] Clock Tree S [思维]
  • 专业做网站的公司哪家好西宁网站建设公司
  • 信息安全基础知识:06认证技术
  • 哪一个网站做专栏作家好点橙色企业网站模板
  • 【区间DP】戳气球 题解
  • Ventoy下载和安装教程(图文并茂,非常详细)
  • 无向图的回路检测(广度优先并查集)
  • 磁悬浮轴承损耗:深度解析损耗机理与降耗之道
  • AI大模型赋能药物研发:破解“双十困局”的跨界革命
  • 哲林高拍仪网站开发宁波南部商务区网站建设
  • 经典的逻辑函数化简算法 Espresso
  • ZKEACMS:基于ASP.Net Core开发的开源免费内容管理系统
  • 【QT常用技术讲解】opencv实现摄像头图像检测并裁剪物体
  • 深圳建网站哪个好网页设计实训总结3000字大学篇
  • 【密码学实战】openHiTLS mac命令行:消息认证码工具