239. 滑动窗口最大值 - 解题记录
📌 题目描述
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
示例 2:
输入:nums = [1], k = 1
输出:[1]
🧠 思路演进过程
思路一:暴力解法(最容易想到)
每次窗口移动后,遍历窗口内的 k 个元素,找出最大值。
java
for (int i = 0; i <= nums.length - k; i++) {
int max = nums[i];
for (int j = i; j < i + k; j++) {
max = Math.max(max, nums[j]);
}
result[i] = max;
}
时间复杂度: O(n × k)
问题: 数据量大时会超时 ❌
思路二:优化思路(记录最大值)
每次滑动窗口,只引入一个新元素,丢弃一个旧元素:
- 如果新元素 ≥ 当前最大值 → 新元素成为最大值
- 如果新元素 < 当前最大值:
- 如果丢弃的不是当前最大值 → 最大值不变
- 如果丢弃的是当前最大值 → 重新遍历窗口找最大值
问题: 数组递减时,每次都要重新遍历,复杂度仍为 O(n × k)
思路三:单调队列(最优解)✨
使用双端队列(Deque)维护一个单调递减 的队列,存储的是数组元素的下标。
核心思想:
- 队首永远是当前窗口的最大值
- 保持队列中下标对应的元素值从大到小
- 移除过期元素(不在当前窗口)
- 移除不可能成为最大值的元素(比新元素小的)
🏗️ 单调队列详解
队列特性
| 特性 | 说明 |
|---|---|
| 存储内容 | 数组元素的下标 |
| 队首 → 队尾 | 下标对应元素值单调递减 |
| 队首作用 | 当前窗口最大值的下标 |
| 队尾作用 | 插入新元素的位置 |
操作规则
- 移除过期元素:如果队首下标 ≤ i - k,说明已离开窗口,弹出队首
- 保持单调性:从队尾开始,移除所有比新元素小的元素(它们再也没机会成为最大值)
- 插入新元素:将新元素下标加入队尾
- 收集结果:当 i ≥ k-1 时,队首下标对应的元素就是当前窗口的最大值
🎬 动图演示
以 nums = [1,3,-1,-3,5,3,6,7], k = 3 为例:
| 步骤 | 窗口 | 队列(存下标) | 队列对应值 | 最大值 |
|---|---|---|---|---|
| i=0 | [1] | [0] | [1] | - |
| i=1 | [1,3] | [1] | [3] | - |
| i=2 | [1,3,-1] | [1,2] | [3,-1] | 3 |
| i=3 | [3,-1,-3] | [1,2,3] | [3,-1,-3] | 3 |
| i=4 | [-1,-3,5] | [4] | [5] | 5 |
| i=5 | [-3,5,3] | [4,5] | [5,3] | 5 |
| i=6 | [5,3,6] | [6] | [6] | 6 |
| i=7 | [3,6,7] | [7] | [7] | 7 |
💻 代码实现
java
class Solution {
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 ArrayDeque<>(); // 存储下标
for (int i = 0; i < n; i++) {
// 1. 移除不在窗口内的元素(从队首)
if (!deque.isEmpty() && deque.peekFirst() <= i - k) {
deque.pollFirst();
}
// 2. 保持单调递减:移除所有比新元素小的元素(从队尾)
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
// 3. 加入新元素
deque.offerLast(i);
// 4. 收集结果(从第 k-1 个元素开始)
if (i >= k - 1) {
result[i - k + 1] = nums[deque.peekFirst()];
}
}
return result;
}
}
⚙️ 复杂度分析
| 复杂度 | 说明 |
|---|---|
| 时间复杂度 | O(n) - 每个元素最多入队出队一次 |
| 空间复杂度 | O(k) - 队列最多存储 k 个元素 |
📝 关键点总结
-
为什么存下标不存值?
- 需要判断元素是否还在窗口内(通过下标和 i-k 比较)
- 通过下标可以快速获取元素值
-
为什么用双端队列?
- 需要从队首移除过期元素
- 需要从队尾移除比新元素小的元素
- 需要从队尾添加新元素
-
单调队列的本质
- 维护一个"候选最大值"的序列
- 新元素会"干掉"所有比它小的旧元素
- 队首永远是当前最强选手
🎯 举一反三
这种单调队列的思想还可以解决:
- 滑动窗口最小值
- 滑动窗口最值问题变种
- 需要维护一个动态集合的最值问题
📚 参考资料
- Java Deque 接口:
ArrayDeque是常用实现类 - 关键方法:
peekFirst()/peekLast()- 查看队首/队尾pollFirst()/pollLast()- 移除并返回队首/队尾offerFirst()/offerLast()- 添加元素到队首/队尾
💡 记住:单调队列就是用空间换时间,用 O(n) 的代价解决原本 O(n×k) 的问题!