1702 字
9 分钟
4月9日刷题笔记——滑动窗口

本篇题目#

#239滑动窗口最大值
Hard
#209长度最小的子数组
Medium
#567字符串的排列
Medium

一、滑动窗口最大值(单调队列)#

#239滑动窗口最大值
Hard
  • 核心思路:维护一个单调递减队列,队头始终是当前窗口的最大值下标
  • 关键洞察:如果窗口内左边的数比右边的数小,左边的数永远不可能成为最大值(它比右边的数小,还比右边的数先出窗口),可以直接淘汰
  • 队列存的是下标而不是值,这样才能判断某个数有没有滑出窗口

每次右指针前进时,流程是:

  1. 清理队尾:把所有比 nums[r] 小的队尾弹掉(它们没资格留了)
  2. 入队:把 r 加入队尾
  3. 清理队头:如果队头下标已滑出窗口左边界,弹掉
  4. 记录答案:窗口够 k 个后,nums[q[0]] 就是最大值
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
output = []
q = deque() # 存下标,队头始终是当前窗口最大值的下标
l = r = 0
while r < len(nums):
# 只要队列不为空且新来的数比队尾大,队尾就没资格留了
# (它没新数年轻,又没新数技术好)
while q and nums[q[-1]] < nums[r]:
q.pop()
# 删完队尾后把 r 入队(如果上面 while 没触发,说明新数不如队尾大,直接跟在后面)
q.append(r)
# 如果队头已经滑出窗口左边界,过期了,直接移除
if l > q[0]:
q.popleft()
# 窗口凑够 k 个后,队头就是当前最大值,记录并右移左边界
if (r + 1) >= k:
output.append(nums[q[0]])
l += 1
r += 1
return output

复杂度

  • 时间:O(n),每个元素最多入队/出队各一次
  • 空间:O(k),队列最多存 k 个下标

逐步追踪(nums = [1,3,-1,-3,5,3,6,7],k = 3)

队列存的是下标,格式:[下标(值), …]

r当前窗口操作队列(头→尾)本轮最大值输出
0[1]直接入队[0(1)]
1[1,3]3>1,弹出0,入队1[1(3)]
2[1,3,-1]-1<3,直接入队[1(3), 2(-1)]3[3]
3[3,-1,-3]-3<-1,直接入队[1(3), 2(-1), 3(-3)]3[3,3]
4[-1,-3,5]5>一切,弹出3,2,1,入队4[4(5)]5[3,3,5]
5[-3,5,3]3<5,直接入队[4(5), 5(3)]5[3,3,5,5]
6[5,3,6]6>一切,弹出5,4,入队6[6(6)]6[3,3,5,5,6]
7[3,6,7]7>6,弹出6,入队7[7(7)]7[3,3,5,5,6,7]

二、长度最小的子数组(可变窗口)#

#209长度最小的子数组
Medium
  • 核心思路:右指针一直往右走,总和够了就从左边缩窗口,找最短的满足条件的窗口
  • 和 LC 239 的区别:239 是固定大小窗口,这题是可变大小——右边扩,左边尽量缩
  • res = float("inf") 占位的原因:min() 每次要和 res 比较取更小值,初始用 inf 的话 min(inf, 任何数) 永远返回实际的数,相当于直接赋值;如果用 0 的话 min(0, 4) 返回 0,逻辑就坏掉了
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
"""
右指针一直往右走,每走一步就把 nums[r] 加进当前窗口的总和。
一旦总和够了(≥ target)就开始从左边缩窗口——记当前长度,把左边的数减掉,左指针右移,看能不能缩得更短。
一直缩到总和不够了为止,然后右指针继续往右走。
"""
l, total = 0, 0
# 还没找到答案时用无穷大占位
res = float("inf")
for r in range(len(nums)):
# 对于每次右指针的移动,我们都要更新一遍当前窗口中数字的总和
total += nums[r]
# 只要当前窗口总和比 target 大(或者等于)的时候
# 我们就缩小窗口,也就是左指针右移,看看能不能找到更短的满足条件的窗口
# 为什么不右移右指针呢?因为我们要找尽可能小的窗口来满足 target
# 往右移动右指针就是增加了我们窗口的长度了
while total >= target:
# 我们要记录当前窗口的长度,然后和之前记录的窗口长度做比较,谁小就选谁作为新的 res
res = min(res, r - l + 1)
# 我们移动左指针之前要先把左指针当前指向的数字从 total 里面减掉
# 因为我们马上左指针要右移了,当前左指针指向的数字会掉出滑动窗口,必须先减掉
total -= nums[l]
l += 1
return 0 if res == float("inf") else res

复杂度

  • 时间:O(n),每个元素最多被左右指针各访问一次
  • 空间:O(1)

三、字符串的排列(固定窗口 + Counter 比较)#

#567字符串的排列
Medium
  • 核心思路:s1 的任意排列字符种类和数量完全一样,只是顺序不同。所以问题变成:s2 里有没有一个长度等于 len(s1) 的窗口,里面每个字符的数量和 s1 完全一致?
  • Counter 统计字符频次,两个 Counter 可以直接用 == 比较
  • 初始化时 counter2 = Counter(s2[0:right]) 故意少统计一个(不含 right 位置),因为 while 循环第一行会立即把 s2[right] 加进来,避免重复计数
  • 计数为 0 时必须 del 掉这个 key,因为 {"a": 0} != {} 会导致误判
from collections import Counter
class Solution:
def checkInclusion(self, s1: str, s2: str) -> bool:
"""
滑动窗口:固定大小(len(s1)),每次右边加一个字符,左边移出一个字符,
用 Counter 比较窗口内字符分布是否和 s1 一致。
"""
# 如果 s1 = "ab" 那么 counterDict1 = {'a': 1, 'b': 1}
counterDict1 = Counter(s1)
# 初始化左指针为 0,右指针为 len(s1) - 1,构造定长的、长度等于 s1 长度的滑动窗口
left = 0
right = len(s1) - 1
# 初始化时故意只统计 s2[0:right],空出 right 位置
# 因为 while 循环第一行会立即把 s2[right] 加进来,避免重复计数
counterDict2 = Counter(s2[0:right])
# 只要右指针没出界
while right < len(s2):
# 立马将右指针指向的字母添加到计数字典中,并且计数 +1
counterDict2[s2[right]] += 1
# 判断如果两个计数字典相等就返回 True
if counterDict1 == counterDict2:
return True
# 如果不相等,先将左指针指向的字母计数 -1
counterDict2[s2[left]] -= 1
# 如果计数 -1 之后左指针指向的字母的计数直接为 0 了,那么在计数字典中删除左指针指向的字母
# 也就是在字典中删除对应的键({"a": 0} != {} 会导致误判)
if counterDict2[s2[left]] == 0:
del counterDict2[s2[left]]
left += 1
right += 1
return False

复杂度

  • 时间:O(n),每个字符最多进出窗口各一次
  • 空间:O(1),Counter 最多存 26 个字母

今日总结#

题目难度核心技巧数据结构
239 滑动窗口最大值Hard单调递减队列deque
209 长度最小的子数组Medium可变大小窗口,左边缩双指针
567 字符串的排列Medium固定大小窗口,Counter 比较Counter

三题共同点:都是滑动窗口,右指针扩张,条件触发时处理左边界。区别在于窗口大小是否固定,以及窗口内维护的状态(最大值 / 总和 / 字符频次)。

4月9日刷题笔记——滑动窗口
https://www.yyylegend.com/posts/4月9日刷题笔记滑动窗口/
作者
YYYLEGEND
发布于
2026-04-09
许可协议
CC BY-NC-SA 4.0