滑动窗口模板:子串子数组问题的标准武器#
只要题目里出现“连续子串”“连续子数组”“最短”“最长”“满足条件”,滑动窗口就值得你立刻想到。
⚡ 速记版模板#
// 模板用途:维护一个动态区间 [left, right],在 O(n) 内完成连续区间统计
int left = 0; // 窗口左边界,表示当前窗口起点
for (int right = 0; right < s.length(); right++) { // right 逐步右移,扩张窗口
add(s.charAt(right)); // 把新进入窗口的字符计入状态(频次/计数/和等)
while (!isValid()) { // 只要窗口不合法,就不断收缩左边界
remove(s.charAt(left)); // 移出左端字符并同步更新状态
left++; // 左边界右移,缩小窗口
}
updateAnswer(left, right); // 当前窗口合法,尝试更新题目答案
}java🎯 什么时候想到滑动窗口?#
典型特征:
- 处理的是连续区间
- 需要在线性时间内统计某种性质
- 左右边界可以逐步移动,而不是每次重新枚举
Hot 100 里的典型题目:
💡 核心思路#
窗口本质上就是维护一个区间 [left, right]:
right负责扩张窗口left负责收缩窗口- 在移动过程中维护窗口内的状态
常见状态包括:
- 字符频次
- 元素和
- 不同字符个数
- 是否满足某个约束条件
🚀 模板一:最长型窗口#
适合这类问题:
- 最长无重复子串
- 最长满足某条件的连续区间
class Solution {
public int maxWindowLength(String s) {
int[] count = new int[128]; // 频次数组:记录窗口内每个 ASCII 字符出现次数
int left = 0; // 左边界
int answer = 0; // 最长合法窗口长度
for (int right = 0; right < s.length(); right++) { // right 扩张窗口
char current = s.charAt(right); // 当前进入窗口的字符
count[current]++; // 计入窗口状态
while (!isValid(count, current)) { // 当前窗口不合法(如出现重复字符)
char leftChar = s.charAt(left); // 准备移出的左端字符
count[leftChar]--; // 从状态中删除该字符贡献
left++; // 缩小窗口直到重新合法
}
answer = Math.max(answer, right - left + 1); // 合法后更新最长长度
}
return answer; // 返回最长合法窗口长度
}
private boolean isValid(int[] count, char current) {
return count[current] <= 1; // 本模板场景:无重复字符,故当前字符出现次数必须 <= 1
}
}java这个模板的关键是:
- 先扩张
- 不合法就收缩
- 合法时更新答案
🚀 模板二:最短型窗口#
适合这类问题:
- 最短覆盖子串
- 最短满足条件的连续区间
class Solution {
public int minWindowLength(String s) {
int left = 0; // 左边界
int answer = Integer.MAX_VALUE; // 记录最短满足条件窗口长度,先设为无穷大
for (int right = 0; right < s.length(); right++) { // right 不断扩张窗口
add(s.charAt(right)); // 加入右端字符并更新状态
while (isSatisfied()) { // 一旦窗口满足条件,就尽量收缩求最短
answer = Math.min(answer, right - left + 1); // 先更新当前可行解
remove(s.charAt(left)); // 再移除左端字符,尝试继续缩短
left++; // 左边界右移
}
}
return answer == Integer.MAX_VALUE ? 0 : answer; // 若从未满足条件,按题意返回 0
}
private void add(char current) {
// 模板占位:将 current 加入窗口状态(频次、种类、缺失计数等)
}
private void remove(char current) {
// 模板占位:将 current 从窗口状态移除
}
private boolean isSatisfied() {
return false; // 模板占位:判断当前窗口是否覆盖了所有需求条件
}
}java这个模板的关键是:
- 先把窗口扩到满足条件
- 再尽量缩到最短
- 每次满足条件时都尝试更新答案
🚀 模板三:固定长度窗口#
适合这类问题:
- 长度固定为
k - 每次移动一格做统计
class Solution {
public void fixedWindow(int[] nums, int k) {
int left = 0; // 左边界,配合 right 维持固定长度 k 的窗口
for (int right = 0; right < nums.length; right++) { // 右边界逐步扩张
add(nums[right]); // 新元素进入窗口
if (right - left + 1 > k) { // 若窗口超过 k,就从左边缩回到长度 k
remove(nums[left]); // 移除即将离开窗口的元素
left++; // 左边界右移一位
}
if (right - left + 1 == k) { // 当窗口恰好长度为 k 时统计答案
updateAnswer(); // 例如更新最大值、最小值、和、均值等
}
}
}
private void add(int value) {
// 模板占位:把 value 计入窗口状态
}
private void remove(int value) {
// 模板占位:把 value 从窗口状态中删除
}
private void updateAnswer() {
// 模板占位:用当前窗口状态更新题目答案
}
}java🧠 常见题目怎么套?#
1. 无重复字符的最长子串#
- 状态:字符出现次数
- 约束:窗口内每个字符次数都不超过 1
- 答案:最大长度
2. 最小覆盖子串#
- 状态:目标字符的需求与当前窗口的覆盖情况
- 约束:窗口必须覆盖全部需求字符
- 答案:最短长度
3. 找到所有异位词#
- 状态:字符频次
- 窗口长度固定为模式串长度
- 满足时记录左边界
4. 滑动窗口最大值#
这题本质也是窗口,但通常搭配 单调队列 一起使用。
⚠️ 易错点#
-
先更新答案还是先收缩窗口
- 最长型:通常在合法状态下更新
- 最短型:通常在满足条件时更新后再收缩
-
窗口状态忘记同步删除
right加入了什么,left移动时就要删除什么
-
固定窗口长度判断写错
- 注意条件是
right - left + 1
- 注意条件是
-
并非所有连续区间题都能用窗口
- 如果存在负数,某些“和相关”问题就不具备可滑动性
- 比如 560. 和为 K 的子数组 实际更适合前缀和 + 哈希
🎨 面试时怎么说#
你可以这样说:
我用滑动窗口维护一个连续区间,右指针负责扩张,左指针在条件不满足时收缩,同时维护窗口内的状态信息,从而把暴力枚举的
O(n^2)优化到O(n)。
📌 一句话总结#
滑动窗口的本质是:
- 把“重复枚举区间”变成“增量维护区间”
- 关键不是窗口本身,而是你维护的状态