今天复习了“长度最小的子数组”这道题,突然对滑动窗口这个技巧有了更深的理解。滑动窗口其实是双指针的一种变体,它用两个指针(一般叫 left 和 right)维护一个区间(也可以理解为一个“窗口”),根据题目的不同需求,动态地调整窗口的左右边界。
有趣的是,虽然代码里可能用了两个循环,但滑动窗口的时间复杂度依然是 O(n)。这是因为每个元素最多被“看”两次:一次被 right 扫进去窗口,一次被 left 从窗口移出去。因此整体上仍然是线性时间。
什么时候使用滑动窗口?
滑动窗口特别适合在一个连续区间内寻找某种性质的问题,比如:
连续子数组的最大/最小长度
连续子串是否包含某些字符
一段区间内最多/最少包含多少种元素
你可以把它想象成一扇在数组上移动的窗户,这扇窗户只关心“窗户里的内容”是否满足某种条件。一旦满足,就尝试收缩窗户(优化结果);不满足,就扩展窗户(继续寻找)。
滑动窗口的基本用法
滑动窗口的核心步骤其实很固定,基本可以套模板:
初始化窗口的两个边界:left 和 right
不断移动右边界 right,扩大窗口,直到满足条件
当条件满足后,尝试移动左边界 left,缩小窗口,同时记录最优结果
重复上述过程,直到右边界走到尽头
例题分析一:长度最小的子数组
这道题是滑动窗口的经典题型:
给你一个整数数组 nums 和一个整数目标值 target,请你找出数组中 总和 大于等于 target 的 最短子数组 的长度,并返回该长度。如果不存在符合条件的子数组,返回 0。
这个题目的关键点是:子数组必须是连续的,并且我们要在所有满足条件的子数组中找到最短的一个。
所以思路就是:
移动右指针,不断累加窗口内的和
一旦窗口内的和 ≥ target,就尝试移动左指针缩小窗口
每次满足条件时,更新最小长度
虽然是两个指针嵌套循环,但每个指针只会走一遍数组,所以时间复杂度仍是 O(n)。
例题分析二:Total Fruit(最多采几棵树的水果)
这题和长度最小子数组很像,也是滑动窗口。不同的是这题需要统计种类数量,而不是数值总和。
在 Python 中可以用 Counter() 或字典来维护窗口内每种水果的数量。
每次移动右指针采一棵水果,同时把它记录进字典。如果种类超过两种,就移动左指针,并更新字典(数量减1,为0就 pop 掉)。最终记录窗口的最大长度就是答案。
例题分析三:最小覆盖子串(minWindow)
这道题是滑动窗口中难度稍高的一道,很多人(包括我)第一次做都卡在了字典的使用上。
题目要求:
给两个字符串 s 和 t,返回 s 中最短的包含所有 t 中字符的子串(包括重复的字符)。
关键点是:我们不仅要知道有没有这个字符,还要知道每个字符的数量是否足够,因此用了两个 Counter() —— 一个记录 t 中每个字符需要的数量,一个实时记录窗口中字符的数量。
在滑动窗口的过程中,我们还需要一个额外的变量 start 来记录当前“最短窗口”的起始位置。如果找到更短的覆盖,就更新 start 和 min_len。
这也是我第一次意识到,滑动窗口虽然是模板化的写法,但遇到不同的题,窗口里需要维护的状态可能完全不同(和哈希表配合使用是很常见的变体)。
做题久了我还意识到,如果你对 dict、Counter 这些基本结构不熟,就会很难写出滑动窗口的变种。比如我之前卡在 totalFruit 和 minWindow,就是因为不知道怎么去维护窗口里的状态。