**滑动窗口(Sliding Window)**是一种处理数组和字符串子区间问题的常用技巧。它通过维护一个由 leftright 定义的窗口,将很多 $O(n^2)$ 的双重遍历优化到 $O(n)$。

核心思想

从暴力到滑窗的关键在于:相邻窗口高度重叠,只需要增量更新,而不需要每次重算整个区间

D2 Diagram
qtopie.github.io

适用场景

两类窗口

固定窗口(Fixed Window)

窗口大小固定为 $k$,适合“每连续 $k$ 个元素”的问题。

  1. 先计算前 $k$ 个元素的初始状态。
  2. 每次右移一格:加入新元素,移除旧元素。
  3. 持续更新答案。

可变窗口(Variable Window)

窗口大小根据条件动态变化,常见于“最短/最长满足条件子串”。

  1. right 持续扩张窗口。
  2. 当窗口满足(或不满足)条件时,移动 left 收缩。
  3. 在扩张和收缩过程中记录最优解。

常用模板

Go 模板(可变窗口)

func slidingWindow(s string) int {
	left, right := 0, 0
	window := make(map[byte]int)
	res := 0

	for right < len(s) {
		c := s[right]
		right++
		window[c]++

		for windowNeedsShrink {
			d := s[left]
			left++
			window[d]--
		}

		// 根据题目更新 res
	}

	return res
}

为什么通常是 $O(n)$

设数组长度为 $n$:

因此总操作数满足:

$$ T(n) \le n + n = 2n = O(n) $$

这本质上依赖“指针不回退”的单调性,等价于对 $O(n^2)$ 搜索空间做了大量剪枝。

进一步优化手段

LeetCode 典型题

按套路整理,练的时候建议先做固定窗口,再做可变窗口,最后做单调队列。

固定窗口

可变窗口

单调队列(窗口最值)

使用时的检查清单

开始写代码前,先明确这 4 件事:

  1. 窗口里维护的状态是什么?
  2. 什么时候扩张窗口?
  3. 什么时候收缩窗口?
  4. 在哪个时机更新答案?

只要这四点清晰,绝大多数滑动窗口问题都能稳定落地。