Algorithm · Sliding Window

滑动窗口

同向双指针的收缩艺术:模板三件套 · 定长/可变/计数三类 · 从无重复子串到最小覆盖子串

模板

右指针扩张、左指针收缩、窗口内维护统计——O(n) 的秘密是各走一遍

三类

定长窗口 · 可变窗口(求最长/最短)· 计数窗口(频次/异位词)

前提

窗口扩大/缩小时答案性质单调变化——否则只能 O(n²) 暴力

定位:滑动窗口是"连续子数组/子串"家族的万能钥匙,本质是利用单调性把 O(n²) 的枚举压成 O(n)。与单调队列(栈队列 deck)、前缀和(数组链表 deck)构成三角。

Expand · Shrink · Maintain

模板三件套:扩张、收缩、维护

// 可变窗口万能骨架
func window(s string) {
    need := map[byte]int{}  // 窗口统计
    l := 0
    for r := 0; r < len(s); r++ {
        // 1. 扩张:s[r] 进窗
        need[s[r]]++
        // 2. 收缩:不满足时缩左
        for 窗口不合法(need) {
            need[s[l]]--
            l++
        }
        // 3. 此刻 [l,r] 合法,更新答案
        ans = max(ans, r-l+1)
    }
}
// 求最短:合法时收缩,收缩完更新 ans

三个部件各司其职

右指针无条件前进(探索);左指针按合法性收缩(修复);窗口统计(哈希/计数/位掩码)O(1) 维护窗口内信息。求最长=不合法时收缩;求最短=合法时收缩——只有第 2 步的 while 条件和第 3 步的更新时机互换。

为什么是 O(n)

l 和 r 都只前进不后退:r 走 n 步、l 最多走 n 步 → 总操作 ≤ 2n。这是摊还分析复杂度 deck)——每个元素最多"进窗一次、出窗一次"。

什么时候模板失效

窗口合法性不单调:右扩可能先变好又变坏再变好——收缩无法一步到位。此时回退 O(n²) 或换前缀和+哈希(560 求和类)。

窗口内容怎么维护

字符频次 map(子串族)、和变量(定长和)、位掩码(小写字母可用 int32 优化成 O(1))——统计结构要支持"进出各 O(1)"。

骨架两行核心:求最长 vs 求最短的"while 条件 + 更新时机"互换。O(n) 的摊还证明、"合法性不单调则失效"这两个点比模板本身更重要。

Fixed · Variable · Counting

三类窗口:定长、可变、计数

① 定长窗口(k 固定)

滑动时一进一出、减去左端加右端。代表:1456 定长子串中元音最大数438 找所有字母异位词(定长计数窗口,窗口频次 == 目标频次即命中)。

② 可变窗口求最长

不合法就收缩、合法时记长度。代表:3 无重复字符最长子串(频次 >1 不合法)、209 长度最小的子数组(和 ≥ target 合法,求最短——合法时收缩更新)。

③ 计数/覆盖窗口

维护"已满足的字符种类数" vs 目标。76 最小覆盖子串:need 表 + match 计数,全部满足时收缩左端找最短——窗口模板的天花板题。

三类的判定语句

定长:r−l+1 > k 时 l++;求最长:while 不合法 l++;求最短:while 合法 l++。while 的条件就是题意的镜像,写窗口题先翻译这句话。

类型收缩条件更新时机
定长长度超 k → l++每步(长度==k 时)
求最长不合法 → l++while 之后([l,r] 合法)
求最短合法 → l++while 之中/之后(记录最小)
计数命中长度超 len(p) → l++频次相等时记下标
面试金句:"滑动窗口题的解题动作只有两步:翻译合法性(while 条件)+ 选统计结构(进出 O(1))。模板永远不变,变的是这两件。"
三类窗口的判定语句对照表是本页核心——"while 条件是题意的镜像"这句话是方法论总结。76 是三类中的天花板,下一页展开。

LC 3 · LC 76 · LC 209

三道代表题的窗口设计

// LC 3 无重复字符最长子串
func lengthOfLongestSubstring(s string) int {
    cnt := map[byte]int{}
    l, ans := 0, 0
    for r := 0; r < len(s); r++ {
        cnt[s[r]]++              // 扩张
        for cnt[s[r]] > 1 {      // 重复→不合法
            cnt[s[l]]--          // 收缩到去重
            l++
        }
        if r-l+1 > ans {
            ans = r - l + 1      // 合法时更新
        }
    }
    return ans
}

// LC 76 最小覆盖子串:need + match
// need[t] 目标频次;window 匹配一格 match++
// match == len(need) → 全覆盖,收缩左端
// 更新最短答案,直到不合法继续扩张

LC 3:合法性 = 唯一冲突字符

新进的 s[r] 计数 >1 就是唯一冲突源 → while 只需把左端缩到"这个字符只出现一次"。收缩结束后 [l,r] 无重复。判定 O(1)、收缩摊还 O(n)

LC 76:匹配计数避免全表比对

不能每次比对两个 map(O(26) 也行但丑)——用 match 计数:"窗口内频次达到 need 的字符种类数"。match==len(need) 即覆盖,收缩时 match--。

LC 209:正整数数组的最短子数组

和 ≥ target 合法 → 合法时收缩记最短。依赖"元素全正"(和随窗口单调)——有负数时失效,要换前缀和+单调队列( LC 862)。

为什么它们是"母题"

3 是求最长、209 是求最短、76 是计数覆盖——三类窗口各一。其余 90% 的窗口题是它们的变装(把"字符"换成"数字/元音/种类")。

三道母题对应三类窗口。76 的 match 计数是"避免 map 全比对"的技巧;209 的"全正数"前提是窗口合法性的来源——前提被拿掉就要换算法,这个边界意识是加分项。

LC 3 · Visualized

图解:无重复子串的窗口滑动

无重复字符最长子串的滑动窗口过程 对字符串 abcabcbb 滑动窗口:右指针扩张到出现重复字符 a 时,左指针收缩跳过第一个 a,窗口持续向右滑动,全程左右指针各前进不超过 n 步。 a b c a b c b b 0 r=3 冲突 新 a 与窗内 a 重复 → 左端缩到 l=1,窗口 [b,c,a] 继续滑动:[a,b,c] 恒为长 3 —— l 只前进不后退 答案 3(abc)· r 走 8 步、l 走 ≤8 步 → O(n)
用 abcabcbb 走一遍:r=3 遇到重复 a(橙色标记),左端收缩跳过第一个 a 得到 [b,c,a];之后窗口恒为长 3——abcabcbb 的答案就是 3(abc)。强调两个动作:l 只前进不后退、每步统计 O(1) 摊还。

Which Tool For Which Subarray

区间三兄弟:窗口、前缀和、单调队列

滑动窗口

适合:合法性随窗口滑动单调变化(元素全正、约束可满足性单调)。求最长/最短连续段。失效场景:数组含负数(和不再单调)。

前缀和 + 哈希

适合:精确和/计数匹配(和恰为 K、0/1 平衡)。560 和为 K:窗口做不了(有负数),pre[j]−pre[i]=K 计数即可。数组 deck 的前缀和段。

单调队列

适合:窗口内最值(滑动窗口最大值 239)。合法性单调但要求的不是"存在"而是"最值"——窗口 + 单调队列 组合。

选择决策树

问"是否存在子数组"→ 先看单调:单调走窗口;不单调但和可累加 → 前缀和+哈希;问"窗口最值" → 单调队列;问"所有方案" → 回溯。连续子数组家族四大件各管一摊。

题目特征工具复杂度
最长无重复子串(3)滑动窗口O(n)
和 ≥ target 最短(209,全正)滑动窗口O(n)
和恰为 K(560,有负数)前缀和+哈希O(n)
窗口最大值(239)单调队列O(n)
最短超和(862,有负数)前缀和+单调栈O(n)
异位词定位(438)定长计数窗口O(n)
面试金句:"'连续子数组'四个字后面有四套工具:合法性单调→窗口;求精确和→前缀和;求最值→单调队列;都要→组合。选错工具再怎么写都是 O(n²)。"
本页是"选型页":连续子数组家族四套工具的边界。209 vs 862(有无负数)的对比是最能体现工具选择重要性的案例。

LeetCode Shortlist

必刷题单:窗口的六种变装

变装题目(编号 · 难度)要点
无重复3 无重复最长子串 M · 159 至多两个不同字符 M · 340 至多 K 个不同 H159/340 是 3 的参数化(至多 k 种)
至多转换424 替换后的最长重复 M · 1004 最大连续1的个数 M"至多 k 次操作"= 窗口内坏元素 ≤k
求最短209 长度最小子数组 M · 76 最小覆盖子串 H合法时收缩;76 加 match 计数
定长计数438 异位词 E · 567 排列字符串 M · 1456 定长子串元音 M窗口频次 == 目标频次
组合技239 滑动窗口最大值 H · 480 中位数 H窗口 + 单调队列/双堆(堆 deck
刷法建议:3 → 209 → 76 按顺序吃透三类 → 424/1004 练"至多 k"转换 → 438/567 定长计数 → 239 收尾衔接单调队列。窗口题写完必查:l 是否只前进、更新答案的时机对不对。
六种变装的覆盖清单。"至多 k"转换(424/1004)是窗口题最常见的变装手法:把"最少操作"转成"至多容忍"。

Interview QA

高频追问合集

1 · 滑动窗口为什么是 O(n)?

各走一遍

l 与 r 都单调前进:r 走 n 步、l 总共 ≤ n 步 → 总操作 ≤ 2n。这是摊还论证(每个元素最多进窗一次出窗一次),不是"每步 O(1)"的误会。

2 · 窗口的前提"单调性"指什么?

合法性单调

窗口变大不会让"非法变合法"(求最长时)或反之(求最短时)。209 靠"全正数"保证;LC 3 靠"去掉左端重复即可恢复"。不单调 → 窗口失效。

3 · 求最长和求最短的模板差在哪?

while 与更新时机

求最长:不合法才 while 收缩、之后更新;求最短:合法就 while 收缩、收缩中/后更新。两处互换,其余骨架相同。

4 · 76 的 match 计数解决什么问题?

避免全表比对

判断"窗口覆盖 t"若每次比对两个频次表要 O(字符集);维护 match=已满足种类数,进出窗时 O(1) 增减——覆盖判定变 O(1)。

5 · 209 为什么要求"元素全正"?有负数怎么办?

单调性前提

全正保证"和随窗口扩大单调增、缩小单调减"——合法性单调。有负数(LC 862)要用前缀和 + 单调栈:找 pre[j]−pre[i] ≥ k 的最小 j−i。

6 · 滑动窗口和双指针是什么关系?

同向子类

窗口 = 同向双指针 + 窗口统计。对撞指针(两数之和)也是双指针但反向。窗口的特殊性在"维护窗口内聚合信息",而不只是两个下标。

7 · 424 替换字符的"至多 k"怎么转成窗口?

坏元素计数

把"替换 k 次"翻译成"窗口内非最高频字符数 ≤ k"——维护 maxFreq 即可判定。求最长窗口 + 容忍度 ≤k 是万能转换句式(1004 同款)。

8 · 什么时候不能用窗口要换工具?

三岔口

合法性不单调(负数和)→ 前缀和+哈希(560)/单调栈(862);求窗口最值 → 单调队列(239);求所有方案 → 回溯。先验证单调性再写窗口。

八题:O(n) 摊还、单调性前提、两类模板差异、match 计数、209 前提、与双指针关系、424 转换、工具三岔口。

Related & References

相关知识点与参考

算法系列(本分类)

复杂度分析 →(O(n) 的摊还论证)
排序算法 →(区间题的排序前置)
二分查找 →(答案单调时的另一把刀)

数据结构系列

栈与队列 →(单调队列:窗口最值的搭档)
哈希表 →(窗口统计的载体)
数组与链表 →(前缀和与双指针的地基)

参考来源(本 deck 结论可溯源至下列一手材料)

LeetCode 3/76/209/239/424/438 官方题解三类窗口模板与"合法性单调"前提的讨论
CLRS ch(滑动窗口作为 amortized 例)· 挑战程序设计竞赛(虫取法)日文系教材的"虫取法"即滑动窗口
oi-wiki.org/basic/two-pointer双指针与滑动窗口的中文系统讲解
LeetCode 862 官方题解负数场景的前缀和+单调栈(窗口失效案例)
收尾:复杂度/排序/二分 + 单调队列/哈希/前缀和三向链接。LC 862 作为"窗口失效"案例进入参考,反向强化选型意识。总页数 9。