单调队列¶
最后更新:2024-05-11-22
概述¶
单调队列(Monotonic Queue)是一种特殊的双端队列,队列中的元素始终保持**单调递增**或**单调递减**的性质。它主要用于高效解决**滑动窗口最值问题**,能够在 O(n) 时间复杂度内处理整个数组。
核心特征
单调队列维护窗口内的**候选最值元素**,通过在入队时移除无效元素,保证队首始终是当前窗口的最值。每个元素最多入队出队一次,因此整体时间复杂度为 O(n)。
与普通队列的对比¶
graph LR
subgraph "普通队列"
A1["3"] --> A2["1"]
A2 --> A3["4"]
A3 --> A4["1"]
A4 --> A5["5"]
A6["↑ 队首"] --> A1
end
subgraph "单调队列(递减)"
B1["5"] --> B2["4"]
B2 --> B3["3"]
B4["↑ 队首(最大)"] --> B1
end
style A1 fill:#E3F2FD,stroke:#2196F3
style A2 fill:#E3F2FD,stroke:#2196F3
style A3 fill:#E3F2FD,stroke:#2196F3
style A4 fill:#E3F2FD,stroke:#2196F3
style A5 fill:#E3F2FD,stroke:#2196F3
style B1 fill:#E8F5E9,stroke:#4CAF50
style B2 fill:#E8F5E9,stroke:#4CAF50
style B3 fill:#E8F5E9,stroke:#4CAF50
| 队列类型 | 特点 |
|---|---|
| 普通队列 | 保持FIFO顺序,不保证任何有序性 |
| 单调队列 | 队首是当前窗口最值,元素单调递减 |
单调队列特点¶
1. 双端操作¶
单调队列支持队首和队尾两端操作:
- 队首出队:移除超出窗口范围的元素
- 队尾出队:移除违反单调性的元素
- 队尾入队:新元素从队尾进入(在移除无效元素后)
graph LR
subgraph "单调递减队列(维护窗口最大值)"
A1["9"] --> A2["7"]
A2 --> A3["5"]
A3 --> A4["3"]
end
A5["↑ front (最大值)"] -.-> A1
A6["↑ rear (入队位置)"] -.-> A4
style A1 fill:#E8F5E9,stroke:#4CAF50
style A2 fill:#E8F5E9,stroke:#4CAF50
style A3 fill:#E8F5E9,stroke:#4CAF50
style A4 fill:#E8F5E9,stroke:#4CAF50
| 操作 | 说明 |
|---|---|
| 从队首出队 | 窗口滑出,获取最大值 |
| 从队尾入队 | 移除比新元素小的元素,保持单调性 |
2. 单调性¶
队列中的元素始终保持单调有序:
| 队列类型 | 单调性 | 队首元素 | 应用场景 |
|---|---|---|---|
| 单调递减队列 | 大 → 小 | 最大值 | 滑动窗口最大值 |
| 单调递增队列 | 小 → 大 | 最小值 | 滑动窗口最小值 |
单调递减队列示例
输入数组: [3, 1, 4, 2, 5, 3] 窗口大小: 3
| 步骤 | 当前元素 | 操作 | 队列状态 |
|---|---|---|---|
| 1 | 3 | 入队 | [3] |
| 2 | 1 | 1 < 3, 直接入队 | [3, 1] |
| 3 | 4 | 4 > 1, 4 > 3, 移除后入队 | [4] ✅ |
| 4 | 2 | 2 < 4, 直接入队 | [4, 2] |
| 5 | 5 | 5 > 2, 5 > 4, 移除后入队 | [5] ✅ |
| 6 | 3 | 3 < 5, 直接入队 | [5, 3] |
结论
队列始终保持单调递减,队首是当前窗口的最大值
3. 下标存储¶
单调队列通常存储**元素下标**而非元素值,原因:
- 判断窗口范围:通过下标计算元素是否超出窗口
- 获取元素值:通过下标在原数组中获取值
- 处理重复元素:下标唯一,避免歧义
| Text Only | |
|---|---|
4. 滑动窗口配合¶
单调队列与滑动窗口完美配合:
原理详解¶
单调队列维护策略¶
以滑动窗口最大值(单调递减队列)为例:
flowchart TB
A[新元素 nums i 入队] --> B{队列是否为空?}
B -->|是| C[直接入队]
B -->|否| D{队尾元素 < 新元素?}
D -->|是| E[移除队尾元素]
E --> D
D -->|否| F[新元素入队]
F --> G{队首下标超出窗口?}
G -->|是| H[移除队首元素]
H --> G
G -->|否| I[队首即为最大值]
C --> I
入队操作详解¶
入队时移除所有比当前元素小的队尾元素(对于单调递减队列):
出队操作详解¶
窗口滑动时移除超出范围的队首元素:
| Text Only | |
|---|---|
完整算法流程¶
可视化演示¶
滑动窗口最大值完整演示¶
| Text Only | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | |
操作流程图¶
sequenceDiagram
participant A as 数组遍历
participant Q as 单调队列
participant R as 结果数组
loop 对于每个元素 i
A->>Q: 检查队首是否超出窗口
Q->>Q: 若超出则移除队首
A->>Q: 检查队尾是否违反单调性
Q->>Q: 若违反则移除队尾
A->>Q: 新元素入队
alt 窗口已满
Q->>R: 记录队首元素值
end
end
代码实现¶
滑动窗口最大值¶
滑动窗口最小值¶
完整结构实现¶
复杂度分析¶
时间复杂度¶
| 操作 | 单次复杂度 | 摊还复杂度 | 说明 |
|---|---|---|---|
| 入队 | \(O(k)\) 最坏 | \(O(1)\) 摊还 | 最多移除k个元素,但每个元素只被移除一次 |
| 出队 | \(O(1)\) | \(O(1)\) | 只移除一个元素 |
| 获取最值 | \(O(1)\) | \(O(1)\) | 直接访问队首 |
整体时间复杂度: \(O(n)\)
摊还分析
每个元素最多: - 入队一次 - 出队一次(被移除)
因此,虽然有嵌套循环,但总操作次数 \(\leq 2n\)
总时间复杂度 \(= O(n)\)
空间复杂度¶
- \(O(k)\):队列最多存储 \(k\) 个元素(窗口大小)
单调栈 vs 单调队列¶
| 特性 | 单调栈 | 单调队列 |
|---|---|---|
| 数据结构 | 栈 | 双端队列 |
| 操作端 | 单端(栈顶) | 双端(队首+队尾) |
| 单调性 | 单调递增/递减 | 单调递增/递减 |
| 适用场景 | 下一个更大/更小元素 | 滑动窗口最值 |
| 时间复杂度 | O(n) | O(n) |
| 空间复杂度 | O(n) | O(k) |
应用场景¶
1. 滑动窗口最大值¶
最常见的应用,已在上面详细讲解。
2. 滑动窗口中位数¶
使用两个单调队列维护窗口内的元素:
中位数计算图解:
| Text Only | |
|---|---|
3. 和至少为K的最短子数组¶
使用前缀和 + 单调队列:
算法原理:
| Text Only | |
|---|---|
4. 动态规划优化¶
单调队列可以优化某些动态规划问题:
| Text Only | |
|---|---|
参考资料¶
- 《算法竞赛入门经典》- 单调队列专题
- LeetCode 239. 滑动窗口最大值
- LeetCode 862. 和至少为K的最短子数组
- 单调队列与单调栈的对比与应用