🪟 滑动窗口算法详解

同向双指针的系统化:O(n) 解决所有"连续子串/子数组"问题

一句话总结

滑动窗口用左右两个指针维护一段连续区间(窗口):右指针不断扩张探索新元素,左指针在窗口"不合法/不满足条件"时收缩。由于两个指针都只前进不后退,整个数组最多被进出窗口各一次,总复杂度 O(n)。

→

右指针:扩张

每轮固定右移一格,把新元素纳入窗口,更新窗口统计量。

←

左指针:收缩

窗口不满足条件时右移左边界,吐出旧元素,直到重新合法。

📊

窗口状态

用哈希表/计数器增量维护(和、字符频次、不同元素数),避免每次重算。

🎯

何时记录答案

求最长:合法时更新;求最短:收缩后/收缩中更新。

一、万能模板(可变长度窗口)

1.1 动态图:求最长无重复子串

s = "abcabcbb":求不含重复字符的最长子串长度 a b c a b c b b 窗口 [a],长度 1,无重复 ✓ a b c a b c b b 窗口扩张到 [a,b,c],长度 3,仍然无重复 ✓ 记录答案 3 a b c a b c b b 右移纳入第二个 'a' → 出现重复 ✗ → 左指针右移吐出第一个 'a',窗口变 [b,c,a] 如此往复,右指针扫到结尾。最终答案 = 3("abc")

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
最小覆盖子串:s = "ADOBECODEBANC",t = "ABC" A D O B E C O D E B A N C ↑left right↑ 早期窗口 [A..C](长 6)已覆盖 A、B、C,但不是最短; 右指针推进到末尾,左指针持续收缩,最终锁定: 答案 "BANC"(长 4)—— 窗口滑动过程中出现过的最短合法区间

三、定长窗口:更简单的特例

当题目明确"子串/子数组长度固定为 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 三个信号

  1. 问的是连续子串 / 连续子数组(substring / subarray,不是 subsequence 子序列);
  2. 窗口的合法性具有单调性:窗口越大越容易违法(求最长)或越容易合法(求最短);
  3. 窗口状态可以增量维护:加入/移出一个元素时 O(1) 更新(和、计数、不同元素个数)。

4.2 反例:什么时候不能用

⚠️ 有负数的"和 ≥ target 求最短"不能直接滑窗!例如数组含负数时,窗口右扩不保证和单调增大,"合法性随窗口扩大而单调变化"的前提被破坏。此时应改用前缀和 + 单调队列 / 哈希表(详见《前缀和与差分》一篇)。

4.3 题目难度阶梯

阶段题目考点
入门643 / 1052 定长窗口同步平移、O(1) 更新
基础209 长度最小子数组可变窗口求最短
进阶3 无重复最长子串、424 替换最长哈希计数求最长
困难76 最小覆盖子串、30 串联所有单词need/window 双计数 + valid
滑动窗口 = 同向双指针 + 窗口状态增量维护。记住两类模板(求最长:不合法才收缩、合法时更新;求最短:合法才收缩、收缩中更新),配合一个哈希表/计数器,就能覆盖 LeetCode 上绝大多数标记为"滑动窗口"的题目。