📖 第17课:滑动窗口最大值

2 阅读22分钟

想系统提升编程能力、查看更完整的学习路线,欢迎访问 AI Compass:github.com/tingaicompa… 仓库持续更新刷题题解、Python 基础和 AI 实战内容,适合想高效进阶的你。

📖 第17课:滑动窗口最大值

模块:滑动窗口 | 难度:Hard ⭐⭐ LeetCode 链接leetcode.cn/problems/sl… 前置知识:第14-16课 - 滑动窗口系列 预计学习时间:30分钟


🎯 题目描述

给你一个整数数组 nums,有一个大小为 k滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回滑动窗口中的最大值

示例 1:

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

示例 2:

输入:nums = [1], k = 1
输出:[1]

约束条件:

  • 1 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4
  • 1 <= k <= nums.length

🧪 边界用例(面试必考)

用例类型输入期望输出考察点
k=1nums=[1,2,3], k=1[1,2,3]窗口大小为1,每个元素都是最大值
k=nnums=[1,2,3], k=3[3]窗口覆盖整个数组,只有一个输出
递增序列nums=[1,2,3,4,5], k=3[3,4,5]窗口最大值始终是右边界
递减序列nums=[5,4,3,2,1], k=3[5,4,3]窗口最大值始终是左边界
有负数nums=[-1,-3,-5], k=2[-1,-3]负数情况,最大值可能是负数
波动序列nums=[1,3,-1,-3,5,3,6,7], k=3[3,3,5,5,6,7]经典用例,窗口最大值变化

💡 思路引导

生活化比喻

想象你在看一场接力赛,赛道上有 n 个选手排成一列,你手里有一个观察窗(只能看到连续的 k 个选手)。观察窗从左边开始,每次向右移动一格,你要记录每次观察窗内跑得最快的选手是谁

🐌 笨办法:每次观察窗移动,你都把窗口内的 k 个选手从头到尾看一遍,找出跑得最快的。窗口要移动 n-k+1 次,每次都要看 k 个人,总共要看 (n-k+1) × k 次,累死了!这就是暴力法,时间 O(nk)。

🚀 聪明办法:你用一个**"候选榜"**(单调队列)来记录"有可能成为最大值的选手":

  • 榜首永远是当前窗口的最快选手
  • 当观察窗右移加入新选手时:
    • 如果新选手比榜尾的选手快,就把榜尾那些"永远不可能成为最大值"的选手踢掉(因为新选手既更快又更靠右,榜尾的选手没希望了)
    • 把新选手加入榜尾
  • 当观察窗左移离开旧选手时:
    • 如果榜首选手已经不在窗口内了(过期了),就把他从榜首踢掉
  • 榜首选手就是当前窗口的最大值

这样每个选手最多进榜一次、出榜一次,总时间 O(n),比暴力法快 k 倍!

关键洞察

固定长度窗口 + 求最大值 → 用单调递减队列维护"有可能成为最大值的候选者" → 队首永远是当前窗口最大值


🧠 解题思维链

这一节模拟你在面试中"从零开始思考"的过程。

Step 1:理解题目 → 锁定输入输出

  • 输入:整数数组 nums(长度 1~10^5),窗口大小 k
  • 输出:每个窗口的最大值组成的数组(长度 n-k+1)
  • 限制
    • 窗口大小固定为 k
    • 窗口每次向右移动一位
    • 需要返回每个窗口位置的最大值

Step 2:先想笨办法(暴力法)

对于每个窗口位置 i(i 从 0 到 n-k),找出 nums[i:i+k] 的最大值。

  • 外层循环枚举窗口位置:O(n-k+1) ≈ O(n)
  • 内层循环找窗口内最大值:O(k)
  • 总时间复杂度:O(nk)
  • 瓶颈在哪:每次窗口移动都要重新扫描 k 个元素找最大值

Step 3:瓶颈分析 → 优化方向

暴力法的核心问题:

  • 窗口 [i, i+k-1] 和窗口 [i+1, i+k] 有 k-1 个元素重叠,但我们每次都重新找最大值,浪费计算
  • 能不能利用"窗口移动"这个特性,动态维护最大值?

优化思路:

  • 方法1:用大顶堆维护窗口内元素 → 但删除任意元素是 O(k),不够优
  • 方法2:用单调队列 → 队列中存储"有可能成为最大值的下标",队首是最大值

Step 4:选择武器

  • 选用:单调递减队列(Monotonic Decreasing Deque)
  • 理由:
    • 队列中的元素从队首到队尾单调递减
    • 队首元素永远是当前窗口的最大值
    • 加入新元素时,从队尾删除所有小于新元素的"无用元素"
    • 窗口滑动时,如果队首元素过期(不在窗口内),从队首删除
    • 每个元素最多入队一次、出队一次,时间 O(n)

🔑 模式识别提示:当题目出现"固定长度滑动窗口"+"求窗口内最大值/最小值",优先考虑"单调队列"


🔑 解法一:暴力法(直觉法)

思路

对于每个窗口位置,遍历窗口内的 k 个元素,找出最大值。

图解过程

示例:nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3

窗口1: [1, 3, -1]
  遍历: 1, 3, -1 → max = 3

窗口2: [3, -1, -3]
  遍历: 3, -1, -3 → max = 3

窗口3: [-1, -3, 5]
  遍历: -1, -3, 5 → max = 5

窗口4: [-3, 5, 3]
  遍历: -3, 5, 3 → max = 5

窗口5: [5, 3, 6]
  遍历: 5, 3, 6 → max = 6

窗口6: [3, 6, 7]
  遍历: 3, 6, 7 → max = 7

结果: [3, 3, 5, 5, 6, 7]

Python代码

from typing import List


def max_sliding_window_brute(nums: List[int], k: int) -> List[int]:
    """
    解法一:暴力法
    思路:对每个窗口遍历 k 个元素找最大值
    """
    n = len(nums)
    result = []

    for i in range(n - k + 1):  # 枚举窗口起点
        window_max = max(nums[i:i+k])  # 找窗口内最大值
        result.append(window_max)

    return result


# ✅ 测试
print(max_sliding_window_brute([1, 3, -1, -3, 5, 3, 6, 7], 3))  # 期望输出:[3, 3, 5, 5, 6, 7]
print(max_sliding_window_brute([1], 1))                         # 期望输出:[1]
print(max_sliding_window_brute([1, -1], 1))                     # 期望输出:[1, -1]
print(max_sliding_window_brute([9, 11], 2))                     # 期望输出:[11]

复杂度分析

  • 时间复杂度:O(nk) — n-k+1 个窗口,每个窗口找最大值需要 O(k)
    • 如果 n=100000, k=10000,需要约 10^9 次操作,会超时
  • 空间复杂度:O(1) — 不计输出数组,只用了几个变量

优缺点

  • ✅ 思路直观,代码简单
  • ✅ 空间复杂度低
  • ❌ 时间复杂度 O(nk),数据量大时必然超时
  • ❌ 重复计算,没有利用窗口移动的特性

⚡ 解法二:单调递减队列(最优解)

优化思路

用一个双端队列(deque)维护一个"候选榜",队列中存储的是元素的下标,且队列中的元素从队首到队尾对应的值单调递减

核心思想:

  1. 队首元素永远是当前窗口的最大值的下标
  2. 加入新元素时,从队尾删除所有小于新元素的"无用元素"(因为新元素既更大又更靠右,那些小元素永远不可能成为最大值了)
  3. 窗口滑动时,如果队首元素的下标已经不在窗口内(过期),从队首删除

💡 关键想法:队列中只保留"有可能成为未来某个窗口最大值"的元素。如果一个元素比新加入的元素小,且位置更靠左,它就永远没有机会成为最大值了,可以直接踢掉。

图解过程

示例:nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3
队列中存储的是下标,显示时用 [下标:值] 表示

初始状态:deque = [], result = []

Step 1: i=0, 加入 nums[0]=1
  1. 队列为空,直接加入 → deque = [0:1]
  2. 窗口还没形成(i < k-1),不输出

Step 2: i=1, 加入 nums[1]=3
  1. 3 > 1(队尾元素),从队尾删除 0:1 → deque = []
  2. 加入 1:3 → deque = [1:3]
  3. 窗口还没形成(i < k-1),不输出

Step 3: i=2, 加入 nums[2]=-1
  1. -1 < 3(队尾元素),不删除
  2. 加入 2:-1 → deque = [1:3, 2:-1]
  3. 窗口已形成!队首 1:3 → 输出 3
     result = [3]

  当前窗口: [1, 3, -1]
  队列状态: [1:3, 2:-1]  (3 > -1,单调递减✅)
  队首 3 是最大值 ✅

Step 4: i=3, 加入 nums[3]=-3
  1. -3 < -1(队尾元素),不删除
  2. 加入 3:-3 → deque = [1:3, 2:-1, 3:-3]
  3. 检查队首:下标 1 是否过期?窗口是 [1,3],1 在窗口内,不删除
  4. 队首 1:3 → 输出 3
     result = [3, 3]

  当前窗口: [3, -1, -3]
  队列状态: [1:3, 2:-1, 3:-3]  (3 > -1 > -3,单调递减✅)
  队首 3 是最大值 ✅

Step 5: i=4, 加入 nums[4]=5
  1. 5 > -3(队尾元素),从队尾删除 3:-3 → deque = [1:3, 2:-1]
  2. 5 > -1(队尾元素),从队尾删除 2:-1 → deque = [1:3]
  3. 5 > 3(队尾元素),从队尾删除 1:3 → deque = []
  4. 加入 4:5 → deque = [4:5]
  5. 检查队首:下标 4 在窗口 [2,4] 内,不删除
  6. 队首 4:5 → 输出 5
     result = [3, 3, 5]

  当前窗口: [-1, -3, 5]
  队列状态: [4:5]
  队首 5 是最大值 ✅

Step 6: i=5, 加入 nums[5]=3
  1. 3 < 5(队尾元素),不删除
  2. 加入 5:3 → deque = [4:5, 5:3]
  3. 检查队首:下标 4 在窗口 [3,5] 内,不删除
  4. 队首 4:5 → 输出 5
     result = [3, 3, 5, 5]

  当前窗口: [-3, 5, 3]
  队列状态: [4:5, 5:3]  (5 > 3,单调递减✅)
  队首 5 是最大值 ✅

Step 7: i=6, 加入 nums[6]=6
  1. 6 > 3(队尾元素),从队尾删除 5:3 → deque = [4:5]
  2. 6 > 5(队尾元素),从队尾删除 4:5 → deque = []
  3. 加入 6:6 → deque = [6:6]
  4. 检查队首:下标 6 在窗口 [4,6] 内,不删除
  5. 队首 6:6 → 输出 6
     result = [3, 3, 5, 5, 6]

  当前窗口: [5, 3, 6]
  队列状态: [6:6]
  队首 6 是最大值 ✅

Step 8: i=7, 加入 nums[7]=7
  1. 7 > 6(队尾元素),从队尾删除 6:6 → deque = []
  2. 加入 7:7 → deque = [7:7]
  3. 检查队首:下标 7 在窗口 [5,7] 内,不删除
  4. 队首 7:7 → 输出 7
     result = [3, 3, 5, 5, 6, 7]

  当前窗口: [3, 6, 7]
  队列状态: [7:7]
  队首 7 是最大值 ✅

最终结果: [3, 3, 5, 5, 6, 7]

Python代码

from typing import List
from collections import deque


def max_sliding_window(nums: List[int], k: int) -> List[int]:
    """
    解法二:单调递减队列
    思路:维护一个单调递减的双端队列,队首是当前窗口最大值的下标
    """
    n = len(nums)
    dq = deque()  # 存储下标,对应的值单调递减
    result = []

    for i in range(n):
        # 1. 移除队首过期元素(不在窗口内的)
        while dq and dq[0] < i - k + 1:
            dq.popleft()

        # 2. 维护单调性:移除队尾所有小于当前元素的下标
        #    (因为当前元素更大且更靠右,那些元素永远不可能成为最大值)
        while dq and nums[dq[-1]] < nums[i]:
            dq.pop()

        # 3. 加入当前元素的下标
        dq.append(i)

        # 4. 窗口形成后,队首元素就是当前窗口的最大值
        if i >= k - 1:
            result.append(nums[dq[0]])

    return result


# ✅ 测试
print(max_sliding_window([1, 3, -1, -3, 5, 3, 6, 7], 3))  # 期望输出:[3, 3, 5, 5, 6, 7]
print(max_sliding_window([1], 1))                         # 期望输出:[1]
print(max_sliding_window([1, -1], 1))                     # 期望输出:[1, -1]
print(max_sliding_window([9, 11], 2))                     # 期望输出:[11]
print(max_sliding_window([7, 2, 4], 2))                   # 期望输出:[7, 4]

复杂度分析

  • 时间复杂度:O(n) — 每个元素最多入队一次、出队一次,每次操作 O(1)
    • 具体地说:如果 n=100000,只需要约 20 万次操作,比暴力法快 k 倍!
  • 空间复杂度:O(k) — 队列中最多存储 k 个元素的下标

🐍 Pythonic 写法

利用 Python 的 deque 简化代码:

from typing import List
from collections import deque


def max_sliding_window_pythonic(nums: List[int], k: int) -> List[int]:
    """
    Pythonic 写法:单调递减队列(简化版)
    """
    dq = deque()
    result = []

    for i, num in enumerate(nums):
        # 移除过期元素
        if dq and dq[0] == i - k:
            dq.popleft()

        # 维护单调性
        while dq and nums[dq[-1]] < num:
            dq.pop()

        dq.append(i)

        # 输出结果
        if i >= k - 1:
            result.append(nums[dq[0]])

    return result


# ✅ 测试
print(max_sliding_window_pythonic([1, 3, -1, -3, 5, 3, 6, 7], 3))  # 期望输出:[3, 3, 5, 5, 6, 7]

这个写法用到了:

  • enumerate():同时获取下标和值
  • 条件简化dq[0] == i - k 而不是 dq[0] < i - k + 1,逻辑更清晰

⚠️ 面试建议:推荐使用解法二的标准写法,逻辑最清晰。面试时先画图说明单调队列的工作原理,再写代码。重点强调"为什么可以删除队尾的小元素"——这是单调队列的核心思想。


📊 解法对比

维度解法一:暴力法解法二:单调队列
时间复杂度O(nk)O(n)
空间复杂度O(1)O(k)
代码难度简单中等
面试推荐⭐⭐⭐
适用场景小规模数据或说明思路面试首选,高效且经典

面试建议:先用 30 秒口述暴力法思路和复杂度(展示你能想到基本解法),然后重点讲解单调队列优化(展示优化能力)。关键点在于画图说明"为什么队列中的元素保持单调递减",以及"为什么可以删除队尾的小元素"。


🎤 面试现场

模拟面试中的完整对话流程,帮你练习"边想边说"。

面试官:给你一个数组和一个窗口大小 k,返回每个窗口的最大值。

:(审题 30 秒)好的,让我确认一下——输入是一个数组 nums 和一个窗口大小 k,窗口从左到右滑动,每次移动一位,我需要返回每个窗口位置的最大值,对吧?

面试官:没错。

:好的。我先想一个最直接的办法:对于每个窗口位置,遍历窗口内的 k 个元素找最大值。时间 O(nk),空间 O(1)。不过当 k 很大时会很慢。

让我想想怎么优化……关键问题是:窗口每次只移动一位,有 k-1 个元素重叠,我们能不能利用这个特性避免重复计算?

我想到的优化方案是用单调递减队列

  • 队列中存储元素的下标
  • 队列中的元素从队首到队尾对应的值单调递减
  • 队首元素永远是当前窗口的最大值

具体做法:

  1. 遍历数组,对每个元素:
    • 先检查队首元素是否过期(不在窗口内),如果过期就从队首删除
    • 从队尾删除所有小于当前元素的"无用元素"(因为当前元素更大且更靠右)
    • 把当前元素的下标加入队尾
    • 如果窗口已形成(i >= k-1),队首元素就是当前窗口的最大值

时间 O(n),空间 O(k)。

面试官:为什么可以从队尾删除小元素?

:好问题!假设队列中有元素 A(值为 5,下标为 2),现在要加入元素 B(值为 7,下标为 4)。因为:

  1. B 的值更大(7 > 5)
  2. B 的位置更靠右(4 > 2)

所以在未来的任何窗口中,如果 A 还在窗口内,那 B 一定也在窗口内,且 B 一定比 A 大。也就是说,A 永远不可能成为最大值了,可以直接删掉。

这就是"单调性"的核心——通过维护单调递减的队列,我们可以快速找到最大值。

面试官:很好,请写代码吧。

:好的。(边写边说)

from collections import deque

def maxSlidingWindow(self, nums, k):
    dq = deque()  # 存储下标
    result = []

    for i in range(len(nums)):
        # 1. 移除过期元素
        while dq and dq[0] < i - k + 1:
            dq.popleft()

        # 2. 维护单调性:删除队尾小元素
        while dq and nums[dq[-1]] < nums[i]:
            dq.pop()

        # 3. 加入当前元素
        dq.append(i)

        # 4. 输出结果
        if i >= k - 1:
            result.append(nums[dq[0]])

    return result

关键点:

  • dq[0] < i - k + 1 判断队首是否过期
  • nums[dq[-1]] < nums[i] 维护单调递减
  • dq[0] 永远是当前窗口最大值的下标

面试官:能手动跑一个例子验证一下吗?

:好的,用 nums=[1,3,-1,-3,5,3,6,7], k=3

  • i=0: 加入 1 → dq=[0], 窗口未形成
  • i=1: 加入 3,3>1 删除 0 → dq=[1], 窗口未形成
  • i=2: 加入 -1 → dq=[1,2], 窗口形成,输出 nums[1]=3 ✅
  • i=3: 加入 -3 → dq=[1,2,3], 输出 nums[1]=3 ✅
  • i=4: 加入 5,5>所有元素 → dq=[4], 输出 nums[4]=5 ✅
  • i=5: 加入 3 → dq=[4,5], 输出 nums[4]=5 ✅
  • i=6: 加入 6,6>3,6>5 → dq=[6], 输出 nums[6]=6 ✅
  • i=7: 加入 7,7>6 → dq=[7], 输出 nums[7]=7 ✅

结果:[3,3,5,5,6,7] ✅

高频追问

追问应答策略
"还有更优解吗?"时间已经是 O(n) 最优(至少要遍历一遍数组),这就是最优解了
"如果求窗口最小值呢?"改成单调递增队列即可,队首是最小值,其他逻辑完全一样
"为什么用 deque 而不是 list?"deque 的 popleft() 是 O(1),list 的 pop(0) 是 O(n)。用 list 会导致总时间复杂度退化为 O(n²)
"如果 k 非常大呢?"空间复杂度 O(k),但队列中实际元素数量通常远小于 k(因为很多元素被删掉了)
"实际工程中什么场景会用到?"股票价格分析(固定时间窗口内的最高价)、网络流量监控(固定时间窗口的峰值流量)、时间序列数据分析(移动最大值)

🎓 知识点总结

Python技巧卡片 🐍

from collections import deque

# deque — 双端队列,两端操作都是 O(1)
dq = deque([1, 2, 3])

# 常用操作
dq.append(4)        # 右端加入:[1, 2, 3, 4]
dq.appendleft(0)    # 左端加入:[0, 1, 2, 3, 4]
dq.pop()            # 右端删除:[0, 1, 2, 3],返回 4
dq.popleft()        # 左端删除:[1, 2, 3],返回 0
dq[0]               # 访问队首:1
dq[-1]              # 访问队尾:3
len(dq)             # 长度:3

# ⚠️ list vs deque 的性能对比
# list.pop(0)    → O(n)(需要移动所有元素)
# deque.popleft() → O(1)(直接删除)
# 所以单调队列必须用 deque,不能用 list!

# enumerate() — 同时拿到下标和值
for i, num in enumerate([10, 20, 30]):
    print(i, num)  # 0 10 / 1 20 / 2 30

💡 底层原理(选读)

为什么单调队列能保证 O(n) 时间?

关键在于均摊分析(Amortized Analysis):

  • 每个元素最多入队一次(在 dq.append(i) 时)
  • 每个元素最多出队一次(在 dq.pop()dq.popleft() 时)
  • 所以总共最多 2n 次队列操作,每次操作 O(1)
  • 总时间 O(2n) = O(n)

为什么要存储下标而不是值?

  • 需要判断元素是否过期(dq[0] < i - k + 1),只有下标才能判断位置
  • 通过下标 dq[0] 可以访问对应的值 nums[dq[0]]

单调队列 vs 堆(优先队列)的区别

  • :可以 O(log n) 插入、O(log n) 删除最大值,但删除任意元素需要 O(n) 或 O(log n)(需要额外维护)
  • 单调队列:可以 O(1) 从两端插入/删除,且队首永远是最大值/最小值
  • 对于滑动窗口问题,单调队列更优,因为窗口移动时需要删除最左边的元素(可能不是最大值)

单调队列的本质

  • 是一种贪心思想:对于当前窗口,我们只关心"有可能成为最大值"的元素
  • 如果一个元素比新加入的元素小,且位置更靠左,它就"没有希望"了,可以直接扔掉
  • 这种"剪枝"让队列中的元素数量远小于 k,提高了效率

算法模式卡片 📐

  • 模式名称:单调递减队列(Monotonic Decreasing Deque)
  • 适用条件
    1. 固定长度滑动窗口
    2. 需要高效获取窗口内最大值/最小值
    3. 窗口每次移动一位
  • 识别关键词:"滑动窗口"+"最大值/最小值"+"固定长度"
  • 模板代码
from collections import deque


def sliding_window_max(nums: list[int], k: int) -> list[int]:
    """
    单调递减队列模板 - 求滑动窗口最大值
    """
    dq = deque()  # 存储下标,对应的值单调递减
    result = []

    for i in range(len(nums)):
        # 1. 移除队首过期元素(不在窗口内)
        while dq and dq[0] < i - k + 1:
            dq.popleft()

        # 2. 维护单调性:移除队尾小于当前元素的下标
        while dq and nums[dq[-1]] < nums[i]:
            dq.pop()

        # 3. 加入当前元素的下标
        dq.append(i)

        # 4. 窗口形成后,输出队首元素
        if i >= k - 1:
            result.append(nums[dq[0]])

    return result


# 变体:求滑动窗口最小值
def sliding_window_min(nums: list[int], k: int) -> list[int]:
    """
    单调递增队列 - 求滑动窗口最小值
    只需把 nums[dq[-1]] < nums[i] 改为 nums[dq[-1]] > nums[i]
    """
    dq = deque()
    result = []

    for i in range(len(nums)):
        while dq and dq[0] < i - k + 1:
            dq.popleft()

        # 维护单调递增(这里改了!)
        while dq and nums[dq[-1]] > nums[i]:
            dq.pop()

        dq.append(i)

        if i >= k - 1:
            result.append(nums[dq[0]])

    return result

易错点 ⚠️

  1. 用 list 而不是 dequelist.pop(0) 是 O(n),会导致总时间复杂度退化为 O(n²)。必须用 deque.popleft() O(1)。

  2. 队列中存储值而不是下标:无法判断元素是否过期。必须存储下标,通过 nums[dq[0]] 访问值。

  3. 过期条件写错:应该是 dq[0] < i - k + 1(或等价的 dq[0] <= i - k),很多人写成 dq[0] < i - k,导致窗口大小不对。

  4. 单调性条件写反:求最大值用 nums[dq[-1]] < nums[i](单调递减),求最小值用 nums[dq[-1]] > nums[i](单调递增)。很多人混淆。

  5. 忘记检查窗口是否形成:应该在 i >= k - 1 时才输出结果,不要在 i >= 0 就输出。


🏗️ 工程实战(选读)

这个算法思想在真实项目中的应用,让你知道"学了有什么用"。

  • 股票价格分析:金融系统中,需要实时计算"过去 N 分钟的最高价"或"过去 N 笔交易的最高价",用于技术分析指标(如布林带、MACD)。单调队列可以 O(1) 更新最大值,延迟极低。

  • 网络流量监控:网络设备需要监控"过去 1 分钟的峰值流量",用于流量整形和 QoS 调度。滑动窗口 + 单调队列可以高效追踪峰值。

  • 视频处理 - 去噪:视频帧序列中,用滑动窗口的最大值/中值滤波器去除噪点。单调队列可以加速最大值计算。

  • 游戏开发 - 技能冷却:MOBA 游戏中,技能系统需要追踪"过去 N 秒内释放的最强技能",用于 UI 显示和 AI 决策。


🏋️ 举一反三

完成本课后,试试这些同类题目来巩固知识:

题目难度相关知识点提示
LeetCode 剑指Offer 59-II. 队列的最大值Medium单调队列 + 队列设计维护一个单调递减的辅助队列
LeetCode 862. 和至少为 K 的最短子数组Hard前缀和 + 单调队列前缀和 + 单调队列优化
LeetCode 1438. 绝对差不超过限制的最长连续子数组Medium单调队列 × 2用两个单调队列分别维护最大值和最小值
LeetCode 84. 柱状图中最大的矩形Hard单调栈类似思想,用单调栈找左右第一个更小元素
LeetCode 42. 接雨水Hard单调栈/双指针可以用单调栈或双指针,思想类似
LeetCode 1696. 跳跃游戏 VIMedium单调队列 + DPDP + 单调队列优化

📝 课后小测

试试这道变体题,不要看答案,自己先想 5 分钟!

题目:给定数组 nums 和窗口大小 k,返回每个窗口的最大值和最小值的差

示例:nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3[4, 6, 8, 8, 3, 4]

  • 窗口 [1,3,-1]:max=3, min=-1, 差=4
  • 窗口 [3,-1,-3]:max=3, min=-3, 差=6
  • ...
💡 提示 1(实在想不出来再点开)

需要同时维护窗口的最大值和最小值。能不能用两个单调队列?

💡 提示 2(再给你一个线索)
  • 用一个单调递减队列维护最大值(和本题一样)
  • 用一个单调递增队列维护最小值(把比较符号反过来)
  • 每个窗口的差 = nums[max_dq[0]] - nums[min_dq[0]]
✅ 参考答案
from collections import deque


def sliding_window_max_min_diff(nums: list[int], k: int) -> list[int]:
    """
    变体题:滑动窗口最大值和最小值的差
    思路:用两个单调队列分别维护最大值和最小值
    """
    n = len(nums)
    max_dq = deque()  # 单调递减队列,队首是最大值
    min_dq = deque()  # 单调递增队列,队首是最小值
    result = []

    for i in range(n):
        # === 维护最大值队列 ===
        # 移除过期元素
        while max_dq and max_dq[0] < i - k + 1:
            max_dq.popleft()
        # 维护单调递减
        while max_dq and nums[max_dq[-1]] < nums[i]:
            max_dq.pop()
        max_dq.append(i)

        # === 维护最小值队列 ===
        # 移除过期元素
        while min_dq and min_dq[0] < i - k + 1:
            min_dq.popleft()
        # 维护单调递增(这里改了!)
        while min_dq and nums[min_dq[-1]] > nums[i]:
            min_dq.pop()
        min_dq.append(i)

        # === 输出结果 ===
        if i >= k - 1:
            max_val = nums[max_dq[0]]
            min_val = nums[min_dq[0]]
            result.append(max_val - min_val)

    return result


# 测试
print(sliding_window_max_min_diff([1, 3, -1, -3, 5, 3, 6, 7], 3))
# 期望输出:[4, 6, 8, 8, 3, 4]
# [1,3,-1]: 3-(-1)=4
# [3,-1,-3]: 3-(-3)=6
# [-1,-3,5]: 5-(-3)=8
# [-3,5,3]: 5-(-3)=8
# [5,3,6]: 6-3=3
# [3,6,7]: 7-3=4

核心思路

  • 单调递减队列维护最大值(队首是最大值)
  • 单调递增队列维护最小值(队首是最小值)
  • 两个队列的逻辑完全一样,只是比较符号相反

启示:单调队列是一种通用的数据结构,不仅能求最大值/最小值,还能扩展到很多变体问题。掌握了单调队列的核心思想,你就能灵活应对各种滑动窗口问题。


如果这篇内容对你有帮助,推荐收藏 AI Compass:github.com/tingaicompa… 更多系统化题解、编程基础和 AI 学习资料都在这里,后续复习和拓展会更省时间。