❌|✅|💡|📌 239. 滑动窗口最大值
🔗 问题简介
题解github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions
📝 题目描述
给你一个整数数组 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]
💡 解题思路
方法一:暴力法(❌ 超时)
- 对每个窗口遍历
k个元素找最大值。 - 时间复杂度高,不适用于大输入。
方法二:优先队列(堆)(✅ 可行但非最优)
- 使用最大堆存储
(值, 索引)。 - 每次取堆顶,若索引不在当前窗口内则弹出。
- 时间复杂度:O(n log n),空间 O(n)。
方法三:单调双端队列(✅ 最优解)
核心思想 :维护一个单调递减的双端队列(deque),队首始终是当前窗口的最大值。
步骤详解:
- 初始化 :创建一个双端队列
deque存储数组索引,结果列表res。 - 遍历数组 (i 从 0 到 n-1):
- 移除队尾较小元素 :若
nums[i] >= nums[deque.back()],则不断弹出队尾,保持队列单调递减。 - 加入当前索引 :将
i加入队尾。 - 移除队首过期索引 :若队首索引
<= i - k(即不在当前窗口),则弹出队首。 - 记录结果 :当
i >= k - 1(即第一个完整窗口形成),将nums[deque.front()]加入结果。
- 移除队尾较小元素 :若
- 返回结果数组。
✅ 为什么有效?
- 队列中索引对应的值单调递减 → 队首是最大值。
- 及时移除过期索引 → 保证队首在窗口内。
- 每个元素最多入队出队一次 → 线性时间。
💻 代码实现
java:Java
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || k <= 0) return new int[0];
int n = nums.length;
int[] res = new int[n - k + 1];
Deque<Integer> deque = new ArrayDeque<>(); // 存储索引
for (int i = 0; i < n; i++) {
// 移除队尾:保持单调递减
while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()]) {
deque.pollLast();
}
deque.offerLast(i);
// 移除队首过期索引
if (deque.peekFirst() <= i - k) {
deque.pollFirst();
}
// 记录结果(从第k个元素开始)
if (i >= k - 1) {
res[i - k + 1] = nums[deque.peekFirst()];
}
}
return res;
}
}
go:Go
func maxSlidingWindow(nums []int, k int) []int {
if len(nums) == 0 || k <= 0 {
return []int{}
}
n := len(nums)
res := make([]int, 0, n-k+1)
deque := make([]int, 0) // 存储索引
for i := 0; i < n; i++ {
// 移除队尾:保持单调递减
for len(deque) > 0 && nums[i] >= nums[deque[len(deque)-1]] {
deque = deque[:len(deque)-1]
}
deque = append(deque, i)
// 移除队首过期索引
if deque[0] <= i-k {
deque = deque[1:]
}
// 记录结果(从第k个元素开始)
if i >= k-1 {
res = append(res, nums[deque[0]])
}
}
return res
}
🎯 示例演示
以 nums = [1,3,-1,-3,5,3,6,7], k = 3 为例:
| i | nums[i] | deque (索引) | deque (值) | 是否记录 | res |
|---|---|---|---|---|---|
| 0 | 1 | [0] | [1] | 否 | [] |
| 1 | 3 | [1] | [3] | 否 | [] |
| 2 | -1 | [1,2] | [3,-1] | 是 | [3] |
| 3 | -3 | [1,2,3] | [3,-1,-3] | 是 | [3,3] |
| 4 | 5 | [4] | [5] | 是 | [3,3,5] |
| 5 | 3 | [4,5] | [5,3] | 是 | [3,3,5,5] |
| 6 | 6 | [6] | [6] | 是 | [3,3,5,5,6] |
| 7 | 7 | [7] | [7] | 是 | [3,3,5,5,6,7] |
✅ 队列始终保持单调递减,队首为窗口最大值。
✅ 答案有效性证明
-
正确性:
- 队列单调递减 ⇒ 队首是当前窗口最大值。
- 过期索引被及时移除 ⇒ 队首始终在窗口
[i-k+1, i]内。 - 每个窗口恰好记录一次 ⇒ 结果长度为
n - k + 1。
-
边界处理:
k = 1:每个元素单独成窗,结果即原数组。k = n:整个数组一个窗口,结果为全局最大值。
📊 复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 暴力法 | O(nk) | O(1) | 每个窗口遍历k次 |
| 优先队列 | O(n log n) | O(n) | 堆操作log n |
| 单调队列 | O(n) | O(k) | 每个元素入队出队各一次 |
✅ 单调队列是最优解,线性时间,常数空间(忽略结果数组)。
📌 问题总结
- 关键洞察 :滑动窗口最大值问题本质是动态维护区间最值 ,适合用单调队列。
- 技巧 :队列存索引而非值,便于判断是否过期。
- 扩展:类似问题如"滑动窗口中位数"可用双堆,"最小值"只需改为单调递增队列。
- 面试重点:单调队列是高频考点,需熟练掌握其维护逻辑与应用场景。