滑动窗口:先定「窗口里维护什么」,再写循环
窗口能成立的前提是「窗口是否合法」随左右边界单调。先写下维护的状态与收缩条件,双指针的两层循环自然会摊还成 O(n)。
滑动窗口的难点从来不是双指针,而是窗口里维护什么状态。想清楚那个状态,两层循环会自然收敛;想不清楚,写出来的版本会把同一个字符反复数。
模板:右扩张,左收缩
以「最长无重复子串」为例:窗口是 [left, right],维护状态是「窗口内每个字符最后出现的下标」。右边界每前进一格,如果这个字符上次出现在窗口内,左边界就跳到它的下一位。
function longestUnique(s) {
const seen = new Map();
let left = 0;
let best = 0;
for (let right = 0; right < s.length; right += 1) {
const ch = s[right];
const last = seen.get(ch);
if (last !== undefined && last >= left) left = last + 1;
seen.set(ch, right);
best = Math.max(best, right - left + 1);
}
return best;
}
注意 last >= left 这个判断:字符可能在窗口外出现过(更早),那就与当前窗口无关,不该动左边界。初学者最常犯的错就是只用 seen.has(ch)。
为什么是 O(n)
right 和 left 都只向右移动,从不回头:right 走 n 步,left 总共也走不超过 n 步。所以虽然写了两层结构,总操作数 ≤ 2n。前提是收缩条件在左边界右移时不会反向。
什么条件下不该用
如果「窗口是否合法」不随窗口变大单调变化(变大了可能合法、也可能不合法,且没有规律),那左边界右移就无法恢复合法性,双指针失效。这时要么换状态设计(把「不合法」定义成单调的量),要么老实用前缀和 / 单调队列 / 二分。
同一模板的三个变体
| 问题 | 维护的状态 | 收缩条件 |
|---|---|---|
| 最长无重复子串 | 字符最后下标 | 出现重复 |
| 最小覆盖子串 | 每种字符还差几个 | 已覆盖全部字符 |
| 定长子数组最大和 | 窗口和 | 长度超过 k |
「最小覆盖」那类要在收缩的循环里更新答案,而不是在扩张时 —— 因为要的是最短的合法窗口,收缩的每一刻都可能是答案。
窗口成立与否要单调:右边界扩张会让它从合法变不合法(或反过来),左边界收缩能把它修回来。

评论
…