单调队列详解
单调队列(Monotone Queue)是一种特殊的双端队列(Deque),队列内元素保持单调递增或单调递减的性质。单调队列常用于解决滑动窗口中的最值问题,时间复杂度为 O(n)。
一、单调队列简介
1.1 定义
单调队列是一个双端队列,满足以下性质:
- 单调性:队列中元素(从队首到队尾)保持单调递增或单调递减
- 双端操作:可以从队首或队尾删除元素
- 时间复杂度:每个元素最多入队、出队各一次,均摊 O(1)
1.2 核心思想
在维护队列单调性的同时:
- 队首元素:保存当前窗口的最值(最大或最小)
- 新元素入队:从队尾移除所有不如新元素”优秀”的元素
- 窗口滑动:从队首移除已经不在窗口内的元素
flowchart LR
A[新元素到来] --> B{比队尾元素优秀?}
B -->|是| C[队尾出队]
C --> B
B -->|否| D[新元素入队]
D --> E{队首超出窗口?}
E -->|是| F[队首出队]
E -->|否| G[队首 = 当前窗口最值]
1.3 与单调栈的区别
| 特性 | 单调栈 | 单调队列 |
|---|---|---|
| 数据结构 | 栈(单端) | 双端队列 |
| 操作 | 只能栈顶操作 | 队首队尾都可操作 |
| 应用场景 | 找左右第一个更大/更小元素 | 滑动窗口最值 |
| 时间复杂度 | O(n) | O(n) |
二、基本操作
2.1 单调递减队列(求最大值)
维护规则:从队首到队尾递减,队首元素最大。
1 | type MonotoneQueue struct { |
2.2 单调递增队列(求最小值)
维护规则:从队首到队尾递增,队首元素最小。
1 | // Push 从队尾入队(单调递增队列:求最小值) |
三、经典问题:滑动窗口最大值
3.1 问题描述
给定数组 nums 和窗口大小 k,返回滑动窗口中每个位置的最大值。
示例:
1 | 输入: nums = [1,3,-1,-3,5,3,6,7], k = 3 |
3.2 暴力解法(O(n*k))
1 | func maxSlidingWindowBrute(nums []int, k int) []int { |
时间复杂度:O(n*k)
空间复杂度:O(1)
3.3 单调队列解法(O(n))
1 | func maxSlidingWindow(nums []int, k int) []int { |
时间复杂度:O(n),每个元素最多入队、出队各一次
空间复杂度:O(k),队列最多存储 k 个元素
3.4 执行过程演示
以 nums = [1,3,-1,-3,5,3,6,7], k = 3 为例:
1 | i=0: nums[0]=1, deque=[0], 窗口未形成 |
四、变种问题
4.1 滑动窗口最小值
将单调递减队列改为单调递增队列即可。
1 | func minSlidingWindow(nums []int, k int) []int { |
4.2 滑动窗口中位数
使用两个单调队列(一个递减、一个递增)分别维护较大和较小的一半元素。
4.3 滑动窗口最大值减最小值
1 | func maxMinusSlidingWindow(nums []int, k int) []int { |
五、完整实现(泛型版)
Go 1.18+ 支持泛型,可以实现通用的单调队列。
1 | package main |
六、复杂度分析
6.1 时间复杂度
| 操作 | 均摊时间复杂度 | 说明 |
|---|---|---|
| Push | O(1) | 每个元素最多入队一次 |
| PopFront | O(1) | 每个元素最多出队一次 |
| Front | O(1) | 直接访问队首 |
| 总体 | O(n) | n 个元素,每个元素入队出队各一次 |
6.2 空间复杂度
- 最坏情况:O(n),所有元素单调递增/递减时
- 滑动窗口问题:O(k),队列最多存储 k 个元素
七、应用场景总结
7.1 适用场景
- 滑动窗口最值问题:窗口大小固定,求每个窗口的最大/最小值
- 区间最值查询:动态维护区间最值
- 带限制的最值:如”最近 k 个元素中的最大值”
7.2 不适用场景
- 求中位数:需要有序结构(堆、平衡树)
- 求和/平均值:不需要单调队列,前缀和即可
- 需要随机访问:单调队列只能访问队首
7.3 经典题目
- LeetCode 239:滑动窗口最大值
- LeetCode 862:和至少为 K 的最短子数组(单调队列 + 前缀和)
- LeetCode 1438:绝对差不超过限制的最长连续子数组(双单调队列)
- LeetCode 918:环形子数组的最大和(单调队列)
八、小结
- 单调队列是维护滑动窗口最值的高效数据结构,时间复杂度 O(n)。
- 核心操作:队尾入队时维护单调性、队首出队时检查窗口边界。
- 单调递减队列:求最大值,队首最大;单调递增队列:求最小值,队首最小。
- 与单调栈区别:单调队列是双端队列,可从队首队尾操作;单调栈只能栈顶操作。
- 实现技巧:队列存储索引而非值,便于判断元素是否超出窗口。
- 泛型实现:Go 1.18+ 可用泛型实现通用单调队列。