滑动窗口
概述
定义
滑动窗口是一种常用的算法技术,通过维护移动窗口状态,避免重复计算,将算法复杂度从 \(O(n^2)\) 降低到 \(O(n)\),属于空间换时间。
- 窗口:数组或字符串中的一个连续区间;
- 滑动:类似窗口在数组或字符串中移动,对于固定窗口,左右边界会同时向右移动,对于可变窗口,边界移动是独立的,具体有以下表现:
- 右边界向右扩展,窗口变大,包含更多元素
- 左边界向右收缩,窗口变小,排除不符合条件的元素
术语说明
- 子数组:数组中连续的元素序列,如
[1,2,3,4]中的[2,3]。 - 子串:字符串中连续的字符序列,如
"abcd"中的"bc"。 - 子序列:保持相对顺序但不要求连续的序列,如
[1,2,3,4]中的[1,3,4]。
注意:滑动窗口算法仅适用于子数组和子串问题,不适用于子序列问题。
实现步骤
标准流程
flowchart TD
A[初始化: left=0, right=0] --> B[右指针向右移动]
B --> C[将新元素加入窗口]
C --> D[更新窗口状态]
D --> E{窗口满足条件?}
E -->|是| F[记录结果]
E -->|否| G[左指针向右移动]
F --> H{right到达末尾?}
G --> I[将元素移出窗口]
I --> J[更新窗口状态]
J --> K{窗口重新满足条件?}
K -->|否| G
K -->|是| H
H -->|否| B
H -->|是| L[返回结果]
style A fill:#e1f5fe
style F fill:#c8e6c9
style L fill:#ffcdd2
style E fill:#fff3e0
style K fill:#fff3e0
style H fill:#fff3e0
指针移动策略
-
同步窗口(同步移动)
- 适用场景:窗口大小固定的问题,如求长度为 k 的子数组的最大和等问题。
- 移动方式:左右指针保持固定距离,同时向右移动。
- 实现特点:通常使用单层循环,无需内层循环控制左指针。
-
动态窗口(条件收缩)
- 适用场景:窗口大小可变,需要满足特定约束条件,如包含特定元素个数的子数组问题。
- 移动方式:右指针逐步扩展,左指针在违反条件时连续收缩。
- 实现特点:外层循环控制右指针,内层循环控制左指针收缩。
-
双指针收缩(优化收缩)
- 适用场景:寻找满足条件的最优解,如包含特定元素个数的问题。
- 移动方式:右指针扩展寻找可行解,左指针收缩优化解质量。
- 实现特点:在满足条件时收缩左指针,寻找更优解。
复杂度分析
时间复杂度:\(O(n)\)
- 每个元素最多被左指针和右指针各访问一次
- 虽然存在嵌套循环,但内层循环的总执行次数不超过 n 次
- 关键洞察:左指针在整个算法过程中最多移动 n 次
空间复杂度:\(O(k)\)
- k 为维护窗口状态所需的额外空间
- 对于计数问题:\(O(字符集大小)\) 或 \(O(元素种类数)\)
- 对于求和问题:\(O(1)\)
适用场景
常见问题特征
- 连续的子数组/子串:明确提到“连续”、“子数组”、“子串”等关键字,但如果是允许跳跃的选择元素,则不适用。
- 存在某种单调性或可逆性:窗口扩大时某个指标(如元素个数、种类数、总和)发生可预测的变化,且这种变化在窗口收缩时可以有效逆转。
- 状态可维护性:能够通过简单的数据结构(如哈希表、计数器)以 \(O(1)\)时间复杂度维护窗口状态的增删操作。
- 存在优化空间:暴力解法存在大量重复计算,有优化潜力。
典型问题类型
第一阶段:固定窗口
- 使用技巧
- 左右指针同步移动,保持固定距离
- 使用"移除旧元素,添加新元素"的更新策略
- 通常只需单层循环,实现相对简单
- 示例算法
- 简单:LeetCode 643 - 子数组最大平均数 I
- 中等:LeetCode 1456 - 定长子串中元音的最大数目
- 中等:LeetCode 1052 - 爱生气的书店老板
第二阶段:动态窗口-计数约束
- 使用技巧
- 右指针扩展窗口,左指针收缩窗口
- 使用哈希表记录元素出现次数
- 当窗口违反约束时,连续收缩直到满足条件
- 示例算法
- 中等:LeetCode 904 - 水果成篮
- 中等:LeetCode 3 - 无重复字符的最长子串
- 中等:LeetCode 424 - 替换后的最长重复字符
第三阶段:动态窗口-和值约束
- 使用技巧
- 利用正数数组的单调性(窗口扩大 → 和增大)
- 当和超过目标时收缩左边界
- 注意处理负数情况的特殊性
- 示例算法
- 中等:LeetCode 209 - 长度最小的子数组
- 中等:LeetCode 930 - 和相同的二元子数组
- 困难:LeetCode 862 - 和至少为 K 的最短子数组
第四阶段:优化型窗口
- 使用技巧
- 先扩展窗口找到可行解
- 在满足条件的基础上收缩窗口寻找最优解
- 使用多个哈希表或计数器维护复杂状态
- 示例算法
- 中等:LeetCode 438 - 找到字符串中所有字母异位词
- 中等:LeetCode 567 - 字符串的排列
- 困难:LeetCode 76 - 最小覆盖子串
常见错误
-
无限循环
- 原因:忘记移动右指针
- 解决:确保每次外层循环都有移动右指针,如
right++
-
数组越界
- 原因:指针移动时未检查边界
- 解决:添加边界检查,如
left < len(arr) && right < len(arr)
-
状态维护错误
- 原因:添加/删除元素时哈希表更新不正确
- 解决:仔细检查计数的增减和删除逻辑
-
结果更新时机错误
- 原因:在错误的位置更新最优解
- 解决:明确何时窗口有效,何时需要记录结果

浙公网安备 33010602011771号