【算法图解】滑动窗口:双指针的艺术
滑动窗口把“枚举所有子数组”压缩成线性扫描,是字符串与数组题的高频解法。本文讲清窗口不变量、何时扩张/收缩、与前缀和的区别,并用最小覆盖子串、无重复最长子串等题型做模板化总结。
18 分钟阅读
小明
【算法图解】滑动窗口:双指针的艺术
滑动窗口(Sliding Window)是很多题的“降维打击”:
- 本来要枚举所有子数组/子串:$O(n^2)$
- 用窗口维护一个区间:一趟扫描就搞定:$O(n)$
关键不在“双指针”,在窗口不变量:
你维护的窗口始终满足某个条件,一旦不满足就收缩。
1. 滑动窗口适用的典型条件
它适合“连续区间”的问题:
- 最长/最短满足条件的子串
- 统计满足条件的子数组数量
- 固定长度窗口的最大/最小值(常配单调队列)
不适合:
- 子序列(不连续)
- 需要全局组合枚举(通常是回溯/DP)
2. 通用模板:扩张右边界,必要时收缩左边界
function slidingWindow(s) {
let left = 0
const window = new Map()
for (let right = 0; right < s.length; right++) {
const ch = s[right]
window.set(ch, (window.get(ch) ?? 0) + 1)
while (/* 窗口不满足条件 */) {
const d = s[left]
window.set(d, window.get(d) - 1)
if (window.get(d) === 0) window.delete(d)
left++
}
// 此时窗口满足条件,可更新答案
}
}
你要做的就是定义:
- 什么时候窗口满足
- 什么时候不满足
3. 例题:无重复字符的最长子串
目标:窗口内所有字符都不重复。
当加入新字符导致重复,就收缩左边界直到不重复。
function lengthOfLongestSubstring(s) {
let left = 0
let ans = 0
const count = new Map()
for (let right = 0; right < s.length; right++) {
const ch = s[right]
count.set(ch, (count.get(ch) ?? 0) + 1)
while (count.get(ch) > 1) {
const d = s[left]
count.set(d, count.get(d) - 1)
if (count.get(d) === 0) count.delete(d)
left++
}
ans = Math.max(ans, right - left + 1)
}
return ans
}
4. 例题:最小覆盖子串(模板升级)
这类题是滑动窗口“进阶版”:
- 你不仅要“不重复”,还要“包含某个需求集合”
核心思路:
need:目标字符计数window:当前窗口计数valid:满足要求的字符种类数
当 valid === need.size 时,说明窗口已经覆盖需求,此时尝试收缩求最小。
(代码略长,这里先讲框架;需要的话我可以补完整实现。)
5. 与前缀和的区别
- 前缀和擅长“可加性”与区间和查询
- 滑动窗口擅长“单调可维护”的条件(例如字符频次、distinct 数)
很多题两者都能做,但窗口更像“在线维护”。
6. 面试速答模板
- 我用滑动窗口维护 left, right 区间。
- 右指针扩张加入元素,更新窗口状态。
- 一旦窗口不满足条件,就移动 left 收缩直到满足。
- 过程中记录最优答案,整体每个指针最多移动 n 次,所以是 O(n)。
总结
- 滑动窗口的核心是窗口不变量。
- 右扩张、左收缩,整体线性。
- 高频题:最长无重复、最小覆盖、固定窗口最大值(配单调队列)。