滑动窗口专题

315 阅读6分钟

1. 滑动窗口

常见套路

滑动窗口主要用来处理连续问题,例如"连续子串balaba","连续子数组balaba"
大多数情况下,滑动窗口能将本来需要 O(n) 时间复杂度的算法降低到 O(n),简单题能直接使用,复杂点的题需要借助哈希,前缀和等数据结构一起达成目的
通常情况下,题目不会明确说要使用滑动窗口,需要认真审题分析出来

思考方向

核心都在一点:窗口内是什么?
再考虑窗口大小是否固定,或者窗口大小不固定
如果是可变窗口大小,通常需要考虑:

  1. 什么情况下窗口收缩(移动左边界)?如何移动?
  2. 什么情况下窗口扩张(移动右边界)?如何移动?

考虑清楚后再看是求最大值还是最小值,拿着模板改

2. 常见题型

1. 固定窗口大小

其模板通常为:

初始化慢指针 := 0
初始化 ans
// 初始化固定窗口
for 快指针 := range 窗口大小的集合 {
    更新窗口内信息
}
判断是否需要更新答案
// 从窗口大小开始
for 快指针 := 窗口大小; 快指针 < 迭代集合长度; 快指针++ {
    更新窗口内信息
    移动慢指针
    判断是否需要更新答案
}
return ans

438. 找到字符串中所有字母异位词

窗口内是一个可能和 p 成为字母异位词的词,其长度需要和 p 相同,所以是固定窗口大小

// 固定窗口
// 窗口就是可能成为同分异位词的字符串
// 判断完后进入下一个窗口
func findAnagrams(s string, p string) []int {
    if len(s) < len(p) {
        return []int{}
    }
    l := 0
    ans := make([]int, 0)
    m := [128]int{}
    for i := range p {
        m[p[i]]++
    }
    // 初始化固定窗口
    for r := range p {
        m[s[r]]--
        r++
    }
    if check(m, p) {
        ans = append(ans, l)
    }
    for r := len(p); r < len(s); r++ {
        m[s[r]]--
        m[s[l]]++
        l++
        // 满足条件,收集结果
        if check(m, p) {
            ans = append(ans, l)
        }
    }
    return ans
}
func check(m [128]int, p string) bool {
    for i := range p {
        if m[p[i]] != 0 {
            return false
        }
    }
    return true
}

2. 可变窗口大小寻找最大值

寻找最大值时,窗口中通常就是要找的内容,比如连续子串,连续子数组啥的
窗口右滑是由于窗口当前还不满足条件,需要不断右滑找到满足条件的窗口
窗口左滑通常是由于当前窗口内不满足条件,左滑的目的是把前面的一些不满足条件集合元素移出数组,这样更新答案之前,就确保窗口内元素可能是要找的值
所以答案更新是在左滑之后

l := 0
初始化 ans
for r := range 迭代集合 {
    更新窗口内信息
    for 窗口内不符合题意 {
        窗口收缩
        l++
    }
    更新答案
}
return ans

3. 无重复字符的最长子串

窗口内是无重复子串
不断右滑
当发现有重复字符,左滑直到把原有的重复字符滑出
借助 map 去重

func lengthOfLongestSubstring(s string) int {
    m := [128]int{}
    l := 0
    ans := 0
    for r := range s {
        m[s[r]]++
        for m[s[r]] > 1 {
            m[s[l]]--
            l++
        }
        if ans < r-l+1 {
            ans = r-l+1
        }
    }
    return ans
}

904. 水果成篮

窗口内是水果的种类数
不断右滑
当水果种类数不满足条件,左滑直到满足条件
借助 map 记录值

func totalFruit(tree []int) int {
    m := make(map[int]int)
    ans := 0
    l := 0
    for r := range tree {
        m[tree[r]]++
        for len(m) > 2 {
            m[tree[l]]--
            if m[tree[l]] == 0 {
                delete(m, tree[l])
            }
            l++
        }
        if ans < r-l+1 {
            ans = r-l+1
        }
    }
    return ans
}

978. 最长湍流子数组

滑动窗口内是所求数组,可变窗口大小
符合条件右滑,不符合条件更新窗口信息时就跳出来了
每次左滑一次尝试

func maxTurbulenceSize(arr []int) int {
    ans := 0
    // 考虑两种湍流子数组
    for l := range arr {
        r := l
        // 更新窗口内情况,暗含不符合题意跳出
        for r < len(arr)-1 && ((r%2 == 1 && arr[r] > arr[r+1]) || (r%2==0 && arr[r] < arr[r+1])  ){
            r++
        }
        // 更新答案
        if r-l+1 > ans {
            ans = r-l+1
        }
    }
    for l := range arr {
        r := l
        for r < len(arr)-1 && ((r%2 == 1 && arr[r] < arr[r+1]) || (r%2==0 && arr[r] > arr[r+1])  ){
            r++
        }
        if r-l+1 > ans {
            ans = r-l+1
        }
    }
    return ans
}

1004. 最大连续1的个数 III

这道题需要读懂题意
将 k 个值从 0 变成 1,需要转换成:最长子数组中 0 的个数不超过 k 的长度
窗口内是 0 的个数不超过 k 的子数组

func longestOnes(nums []int, k int) int {
    l := 0
    ans := 0
    for r := range nums {
        if nums[r] == 0 {
            k--
        }
        for k < 0 {
            if nums[l] == 0 {
                k++
            }
            l++
        }
        if ans < r - l + 1 {
            ans = r-l+1
        }
    }
    return ans
}

3. 可变窗口大小寻找最小值

寻找最大值时,窗口中通常就是要找的内容,比如连续子串,连续子数组啥的
窗口右滑是由于窗口当前还不满足条件,需要不断右滑找到满足条件的窗口
窗口左滑通常是由于当前窗口满足条件了,左滑的目的是在满足条件的情况下,尝试去找更小的答案
所以答案更新是在左滑的过程中
其模板如下:

l := 0
初始化 ans
for r := range 迭代集合 {
    更新窗口内信息
    for 窗口内符合题意 {
		更新答案        
        窗口收缩
        l++
    }
}
return ans

76. 最小覆盖子串

窗口内是可能涵盖所有 t 中字符的最小子串
不断右移
当窗口内已涵盖所有 t 中字符,尝试左移缩小范围
借助 map 记录窗口内字符

func minWindow(s string, t string) string {
    if len(t) > len(s) {
        return ""
    }
    m := [128]int{}
    for i := range t {
        m[t[i]]++
    }
    str := s + s
    l := 0 
    for r := range s {
        m[s[r]]--
        for l <= r && check(m) {
            if len(str) > r-l+1 {
                str = s[l: r+1]
            } 
            m[s[l]]++
            l++
        }
    }
    if str == s + s {
        return ""
    }
    return str
}
func check(m [128]int) bool {
    for _, v := range m {
        if v > 0 {
            return false
        }
    }
    return true
}

209. 长度最小的子数组

窗口内是连续子数组和
窗口不断右滑
当满足条件时,尝试缩小范围,左滑

// 滑动窗口
func minSubArrayLen(target int, nums []int) int {
    l := 0
    ans := math.MaxInt32
    sum := 0
    for r := range nums {
        sum += nums[r]
        for sum >= target {
            if ans > r-l+1 {
                ans = r-l+1
            }
            sum -= nums[l]
            l++
        }
    }
    if ans == math.MaxInt32 {
        return 0
    }
    return ans
}

1234. 替换子串得到平衡字符串

窗口内是替换的子串,当替换了这个子串,就能让整个字符串成为"平衡字符串"
当不满足平衡条件,右滑扩大这个窗口
当满足平衡条件,左滑尝试缩小这个窗口

func balancedString(s string) int {
    ans := len(s)
    l := 0
    m := [128]int{}
    for i := range s {
        m[s[i]]++
    }
    balance := len(s)/4
    if check(m, balance) {
        return 0
    }
    for r := range s {
        m[s[r]]--
        for l <= r && check(m, balance) {
            if ans > r-l+1 {
                ans = r-l+1
            }
            m[s[l]]++
            l++
        }
    }
    return ans
}
func check(m [128]int, balance int) bool {
    for _, v := range m {
        if v > balance {
            return false
        }
    }
    return true
}

1658. 将 x 减到 0 的最小操作数

还是审题,移除数组最左或最右元素的最小操作数,可以等价理解为相当于要找到一个"和为 sum - x 的最长连续序列"
窗口内就是这个序列的和
不断右滑求和
当和小于等于 sum-x,收集长度,左滑
再结合题目做一些变换处理

func minOperations(nums []int, x int) int {
	ans := make([]int, 0, len(nums))
	sum := 0
	for _, v := range nums {
		sum += v
	}
	sum -= x
    if sum == 0 {
        return len(nums)
    }
	l := 0
	for r := range nums {
		sum -= nums[r]
		for l <= r && sum <= 0 {
			if sum == 0 && len(ans) < r-l+1 {
				ans = nums[l : r+1]
			}
			sum += nums[l]
			l++
		}
	}
	if len(ans) == 0 {
		return -1
	}
    return len(nums) - len(ans)
}