一句话总结
滑动窗口用左右两个指针维护一段连续区间(窗口):右指针不断扩张探索新元素,左指针在窗口"不合法/不满足条件"时收缩。由于两个指针都只前进不后退,整个数组最多被进出窗口各一次,总复杂度 O(n)。
→
右指针:扩张
每轮固定右移一格,把新元素纳入窗口,更新窗口统计量。
←
左指针:收缩
窗口不满足条件时右移左边界,吐出旧元素,直到重新合法。
📊
窗口状态
用哈希表/计数器增量维护(和、字符频次、不同元素数),避免每次重算。
🎯
何时记录答案
求最长:合法时更新;求最短:收缩后/收缩中更新。
一、万能模板(可变长度窗口)
1.1 动态图:求最长无重复子串
1.2 代码模板(Python)
def lengthOfLongestSubstring(s):
window = {} # 窗口状态:字符 → 出现次数
left = 0
ans = 0
for right in range(len(s)): # ① 右指针:逐个纳入新元素
c = s[right]
window[c] = window.get(c, 0) + 1
while window[c] > 1: # ② 窗口不合法 → 收缩左边界
d = s[left]
window[d] -= 1
if window[d] == 0:
del window[d]
left += 1
ans = max(ans, right - left + 1) # ③ 窗口合法 → 更新最长答案
return ans
为什么是 O(n)?右指针从 0 走到 n-1 共 n 步;左指针也只会从 0 走到 n-1,全程单调不减。两指针移动总次数 ≤ 2n,所以即使代码里有 while 套 for,均摊后仍是线性时间。
二、求"最短窗口":更新答案的位置变了
2.1 关键差异
| 问题类型 | 收缩时机 | 更新答案时机 | 代表题 |
|---|---|---|---|
| 求最长 | 窗口不合法时收缩 | 窗口合法时更新 | 3 无重复最长子串、424 替换后最长重复子串 |
| 求最短 | 窗口合法时收缩(越短越好) | 每次收缩后更新 | 76 最小覆盖子串、209 长度最小子数组 |
2.2 模板对比:最小覆盖子串(LeetCode 76)
from collections import Counter
def minWindow(s, t):
need = Counter(t) # 目标:需要的字符及数量
window = {}
valid = 0 # 已满足的字符种数
left = 0
best = (float('inf'), 0, 0) # (长度, 起点, 终点)
for right, c in enumerate(s):
if c in need: # ① 扩张:纳入新字符
window[c] = window.get(c, 0) + 1
if window[c] == need[c]:
valid += 1
while valid == len(need): # ② 窗口已合法 → 尝试收缩到更短
if right - left + 1 < best[0]:
best = (right - left + 1, left, right) # ③ 收缩中更新最短
d = s[left]
if d in need:
window[d] -= 1
if window[d] < need[d]:
valid -= 1
left += 1
return "" if best[0] == float('inf') else s[best[1]:best[2]+1
三、定长窗口:更简单的特例
当题目明确"子串/子数组长度固定为 k"(如找平均数最大且长度为 k 的子数组),左右指针同步移动,像一个固定大小的框平移过去:
def findMaxAverage(nums, k):
# 先算第一个窗口的和
total = sum(nums[:k])
best = total
for right in range(k, len(nums)): # 右边界推进
total += nums[right] - nums[right - k] # 进一个、出一个,O(1) 更新
best = max(best, total)
return best / k
| 定长窗口题目 | 窗口状态维护什么 |
|---|---|
| 643 子数组最大平均数 | 元素和(加减更新) |
| 438 找到字符串中所有字母异位词 | 长度 26 的字符计数数组 |
| 567 字符串的排列 | 同上,判断计数数组相等 |
| 1052 爱生气的书店老板 | 窗口内可挽回的不满意值之和 |
四、怎么判断一道题该用滑动窗口?
4.1 三个信号
- 问的是连续子串 / 连续子数组(substring / subarray,不是 subsequence 子序列);
- 窗口的合法性具有单调性:窗口越大越容易违法(求最长)或越容易合法(求最短);
- 窗口状态可以增量维护:加入/移出一个元素时 O(1) 更新(和、计数、不同元素个数)。
4.2 反例:什么时候不能用
⚠️ 有负数的"和 ≥ target 求最短"不能直接滑窗!例如数组含负数时,窗口右扩不保证和单调增大,"合法性随窗口扩大而单调变化"的前提被破坏。此时应改用前缀和 + 单调队列 / 哈希表(详见《前缀和与差分》一篇)。
4.3 题目难度阶梯
| 阶段 | 题目 | 考点 |
|---|---|---|
| 入门 | 643 / 1052 定长窗口 | 同步平移、O(1) 更新 |
| 基础 | 209 长度最小子数组 | 可变窗口求最短 |
| 进阶 | 3 无重复最长子串、424 替换最长 | 哈希计数求最长 |
| 困难 | 76 最小覆盖子串、30 串联所有单词 | need/window 双计数 + valid |
滑动窗口 = 同向双指针 + 窗口状态增量维护。记住两类模板(求最长:不合法才收缩、合法时更新;求最短:合法才收缩、收缩中更新),配合一个哈希表/计数器,就能覆盖 LeetCode 上绝大多数标记为"滑动窗口"的题目。