滑动窗口(Sliding Window)
📌 定义
滑动窗口是一种用于处理连续子数组/子串问题的算法技巧。通过维护一个窗口(区间),在数组/字符串上滑动,高效地求解满足特定条件的子数组/子串。
核心思路
使用两个指针(left和right)维护一个窗口:
- right指针:扩大窗口,将元素加入窗口
- left指针:缩小窗口,将元素移出窗口
通过移动这两个指针,遍历所有可能的窗口,时间复杂度从O(n²)优化到O(n)。
数组: [1, 3, -1, -3, 5, 3, 6, 7]
窗口大小: 3
窗口1: [1, 3, -1] max=3
窗口2: [3, -1, -3] max=3
窗口3: [-1, -3, 5] max=5
窗口4: [-3, 5, 3] max=5
...
Go 模板
1. 固定窗口大小
func fixedWindow(arr []int, k int) []int {
if len(arr) == 0 || k <= 0 {
return nil
}
result := make([]int, 0, len(arr)-k+1)
// 1. 初始化窗口状态
for i := 0; i < k; i++ {
// add(arr[i])
}
// result = append(result, currentAnswer)
// 2. 滑动窗口
for i := k; i < len(arr); i++ {
// remove(arr[i-k])
// add(arr[i])
// result = append(result, currentAnswer)
}
return result
}2. 可变窗口大小(求最大)
func maxVariableWindow(s string) int {
chars := []rune(s)
left, best := 0, 0
window := make(map[rune]int)
for right, ch := range chars {
window[ch]++
for windowInvalid(window) {
leftChar := chars[left]
window[leftChar]--
if window[leftChar] == 0 {
delete(window, leftChar)
}
left++
}
if width := right - left + 1; width > best {
best = width
}
}
return best
}3. 可变窗口大小(求最小)
func minVariableWindow(s string, target string) int {
chars := []rune(s)
left := 0
best := len(chars) + 1
window := make(map[rune]int)
for right, ch := range chars {
window[ch]++
for windowValid(window, target) {
if width := right - left + 1; width < best {
best = width
}
leftChar := chars[left]
window[leftChar]--
if window[leftChar] == 0 {
delete(window, leftChar)
}
left++
}
}
if best == len(chars)+1 {
return 0
}
return best
}💻 经典问题实现
1. 无重复字符的最长子串
func lengthOfLongestSubstring(s string) int {
chars := []rune(s)
left, best := 0, 0
window := make(map[rune]int)
for right, ch := range chars {
window[ch]++
for window[ch] > 1 {
window[chars[left]]--
left++
}
if width := right - left + 1; width > best {
best = width
}
}
return best
}2. 最小覆盖子串
func minWindow(s, t string) string {
if len(s) == 0 || len(t) == 0 {
return ""
}
need := make(map[byte]int)
for i := 0; i < len(t); i++ {
need[t[i]]++
}
window := make(map[byte]int)
left, valid := 0, 0
start, minLen := 0, len(s)+1
for right := 0; right < len(s); right++ {
ch := s[right]
if _, ok := need[ch]; ok {
window[ch]++
if window[ch] == need[ch] {
valid++
}
}
for valid == len(need) {
if width := right - left + 1; width < minLen {
start = left
minLen = width
}
leftChar := s[left]
if _, ok := need[leftChar]; ok {
if window[leftChar] == need[leftChar] {
valid--
}
window[leftChar]--
}
left++
}
}
if minLen == len(s)+1 {
return ""
}
return s[start : start+minLen]
}3. 字符串的排列
func checkInclusion(s1, s2 string) bool {
if len(s1) > len(s2) {
return false
}
need := make(map[byte]int)
for i := 0; i < len(s1); i++ {
need[s1[i]]++
}
window := make(map[byte]int)
left, valid := 0, 0
for right := 0; right < len(s2); right++ {
ch := s2[right]
if _, ok := need[ch]; ok {
window[ch]++
if window[ch] == need[ch] {
valid++
}
}
if right-left+1 == len(s1) {
if valid == len(need) {
return true
}
leftChar := s2[left]
if _, ok := need[leftChar]; ok {
if window[leftChar] == need[leftChar] {
valid--
}
window[leftChar]--
}
left++
}
}
return false
}4. 找到字符串中所有字母异位词
func findAnagrams(s, p string) []int {
if len(p) > len(s) {
return nil
}
need := make(map[byte]int)
for i := 0; i < len(p); i++ {
need[p[i]]++
}
window := make(map[byte]int)
left, valid := 0, 0
result := make([]int, 0)
for right := 0; right < len(s); right++ {
ch := s[right]
if _, ok := need[ch]; ok {
window[ch]++
if window[ch] == need[ch] {
valid++
}
}
if right-left+1 == len(p) {
if valid == len(need) {
result = append(result, left)
}
leftChar := s[left]
if _, ok := need[leftChar]; ok {
if window[leftChar] == need[leftChar] {
valid--
}
window[leftChar]--
}
left++
}
}
return result
}5. 滑动窗口最大值
func maxSlidingWindow(nums []int, k int) []int {
if len(nums) == 0 || k == 0 {
return nil
}
queue := make([]int, 0, len(nums))
result := make([]int, 0, len(nums)-k+1)
for i := 0; i < len(nums); i++ {
for len(queue) > 0 && queue[0] < i-k+1 {
queue = queue[1:]
}
for len(queue) > 0 && nums[queue[len(queue)-1]] < nums[i] {
queue = queue[:len(queue)-1]
}
queue = append(queue, i)
if i >= k-1 {
result = append(result, nums[queue[0]])
}
}
return result
}复杂度分析
| 操作 | 朴素方法 | 滑动窗口 |
|---|---|---|
| 时间复杂度 | O(n²) 或 O(n³) | O(n) |
| 空间复杂度 | O(1) | O(k) |
注:k是窗口大小或字符集大小
经典题目
子串问题
- 无重复字符的最长子串 - LeetCode 3
- 最小覆盖子串 - LeetCode 76
- 字符串的排列 - LeetCode 567
- 找到字符串中所有字母异位词 - LeetCode 438
数组问题
- 滑动窗口最大值 - LeetCode 239
- 长度最小的子数组 - LeetCode 209
- 最大连续1的个数 III - LeetCode 1004
固定窗口
- 定长子串中元音的最大数目 - LeetCode 1456
- 子数组最大平均数 I - LeetCode 643
⚖️ 优缺点
优点
- ✅ 高效:将O(n²)优化到O(n)
- ✅ 简洁:代码模板清晰
- ✅ 通用:适用于多种子串/子数组问题
缺点
- ❌ 理解门槛:双指针移动的时机需要理解
- ❌ 细节多:边界条件容易出错
🎨 应用场景
- 子串/子数组问题:求满足条件的连续区间
- 字符串匹配:模式匹配
- 数据流处理:固定窗口的统计
- 网络流量控制:滑动窗口协议
💡 滑动窗口的关键点
1. 什么时候用滑动窗口?
- ✅ 问题涉及连续的子数组/子串
- ✅ 需要优化暴力O(n²)的解法
- ✅ 窗口的移动是单调的
2. 如何移动窗口?
for right := 0; right < len(arr); right++ {
add(arr[right])
for needShrink() {
remove(arr[left])
left++
}
}3. 何时更新答案?
- 求最大:收缩窗口之前更新
- 求最小:窗口满足条件时更新
- 固定窗口:窗口形成时更新
💡 滑动窗口 vs 双指针
| 特性 | 滑动窗口 | 双指针 |
|---|---|---|
| 应用 | 连续子数组/子串 | 数组/链表问题 |
| 窗口 | 动态或固定窗口 | 不一定是窗口 |
| 方向 | 单向滑动 | 可以对撞或同向 |
| 例子 | 最长无重复子串 | 两数之和、回文判断 |