一、问题回顾
题目:给定数组 nums = [1,3,-1,-3,5,3,6,7] 和窗口大小 k = 3,返回每个滑动窗口的最大值。
窗口位置 最大值
[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
结果:[3,3,5,5,6,7]
二、核心思想:为什么用单调队列?
滑动窗口有两个核心操作:
- 窗口右移:新元素进入
- 窗口左移:旧元素离开
这天然的 "先进先出" 特性,提示我们应该用队列而非栈来维护。而为了快速获取最大值,我们需要队列保持单调递减——队头始终最大。
关键洞察 :当一个新元素进入窗口时,它比队列中所有比它小的元素都"更有前途"——这些小的元素再也不可能成为最大值了(因为它们会先被滑出窗口)。因此,我们可以放心地把它们移除。
三、三大黄金法则
维护单调队列只需记住三条规则:
法则1:队尾入队,保持递减
// 从队尾开始,移除所有小于新元素的值
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
deque.offerLast(i); // 新元素从队尾入队
效果:队列从头到尾永远是递减的,如 [5, 3, 1]
法则2:队头过期,及时清理
// 如果队头元素已滑出窗口(下标超出范围)
if (!deque.isEmpty() && deque.peekFirst() <= i - k) {
deque.pollFirst();
}
效果:确保队头元素一定在当前窗口内
法则3:队头即答案
// 当前窗口的最大值就是队头元素
max = nums[deque.peekFirst()];
四、图解全过程
让我们用 nums = [1,3,-1,-3,5,3,6,7], k = 3 实战演练:
| 步骤 | i | 当前窗口 | 操作说明 | 队列(下标) | 队列值 | 最大值 |
|---|---|---|---|---|---|---|
| 1 | 0 | [1] | 1入队 | [0] | [1] | - |
| 2 | 1 | [1,3] | 3比1大,1永无出头之日,移除1,3入队 | [1] | [3] | - |
| 3 | 2 | [1,3,-1] | -1比3小,直接入队 | [1,2] | [3,-1] | 3 |
| 4 | 3 | [3,-1,-3] | -3入队 | [1,2,3] | [3,-1,-3] | 3 |
| 5 | 4 | [-1,-3,5] | 5王者降临,比-1、-3都大,全部移除,5入队 | [4] | [5] | 5 |
| 6 | 5 | [-3,5,3] | 3比5小,入队 | [4,5] | [5,3] | 5 |
| 7 | 6 | [5,3,6] | 6出现,3出局,6比5小?不,6>5,5也出局! | [6] | [6] | 6 |
| 8 | 7 | [3,6,7] | 7王者降临,6移除,7入队 | [7] | [7] | 7 |
动画演示:
窗口滑动 → [1 3 -1] → [3 -1 -3] → [-1 -3 5] → ...
↓ ↓ ↓
队列变化 [1]→[3] [3,-1]→[3,-1,-3] [5] ...
↑ ↑ ↑
1被3干掉 -3加入 -1,-3被5干掉
五、完整代码实现
Java 版本(推荐)
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || nums.length == 0) return new int[0];
int n = nums.length;
int[] result = new int[n - k + 1];
Deque<Integer> deque = new LinkedList<>(); // 存储下标
for (int i = 0; i < n; i++) {
// 1. 移除队头过期元素(如果窗口已滑过)
if (!deque.isEmpty() && deque.peekFirst() < i - k + 1) {
deque.pollFirst();
}
// 2. 从队尾维护单调递减
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
// 3. 当前元素入队
deque.offerLast(i);
// 4. 窗口形成后开始记录答案
if (i >= k - 1) {
result[i - k + 1] = nums[deque.peekFirst()];
}
}
return result;
}
六、复杂度分析
-
时间复杂度:O(n)
每个元素最多入队一次、出队一次,所有操作都是O(1) -
空间复杂度:O(k)
队列最多存储k个元素下标
对比暴力解法的 O(n*k),这是质的飞跃!
七、总结与升华
单调队列的本质:不是存储所有元素,而是只存储"有潜力成为最大值"的候选者。那些小的、老的元素被无情淘汰,留下的是精简而有效的信息。
这个思想可以迁移到很多场景:
- 滑动窗口中位数
- 滑动窗口最大值/最小值
- 一些动态规划问题
记住三个操作口诀:队尾入队保递减,队头过期要清理,队头永远是答案。
八、互动思考
如果题目改为滑动窗口最小值,应该怎么改代码?
(提示:把队列改成单调递增即可)
希望这篇博客能帮你彻底掌握这个经典算法!如果对你有帮助,欢迎点赞收藏~