一、题目描述:滑动窗口最大值问题
给定一个整数数组 nums
和一个滑动窗口的大小 k
,请找出所有滑动窗口中的最大值。
例如:
输入: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
- 依此类推…
二、暴力解法的痛点
最容易想到的方法是遍历每个窗口,每次用 O(k) 时间找出窗口内的最大值。
但这样的时间复杂度是 O(nk),当数组长度 n
和窗口大小 k
很大时,会超时。
核心痛点:每次窗口滑动后,我们都重新比较了整个窗口内的所有元素,浪费了大量重复计算。
例如,窗口从 [1,3,-1]
滑动到 [3,-1,-3]
时,3
和 -1
这两个元素的大小关系已经比较过了,但暴力解法还是会重新比较。
三、单调队列解法:用队列维护窗口最大值
核心思路:用队列存储元素下标,严格保持队列单调递减
我们可以用一个双端队列(Deque)来存储元素的下标,队列中的下标对应的元素值严格单调递减。
这样,队列的队首元素始终是当前窗口的最大值。
关键操作:
- 剔除超出窗口边界的元素:当队列中的下标超出当前窗口的左边界时,将其从队首弹出。
- 剔除小于最新元素的元素:在插入新元素前,从队尾弹出所有比新元素小的元素,确保队列单调递减。
- 队列存储索引下标:通过下标判断元素是否在当前窗口内,避免越界。
- 严格单调递减:队列中的元素值从队首到队尾严格递减,确保队首始终是最大值。
代码实现:
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++) {
// 1. 剔除超出窗口边界的元素
// 当队列中的下标小于当前窗口的左边界(i - k + 1)时,从队首弹出
while (!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) {
res[i - k + 1] = nums[deque.peekFirst()];
}
}
return res;
}
}
四、单调队列的奥秘:为什么这样做可行?
1. 为什么要剔除超出窗口边界的元素?
队列中存储的是元素的下标。当窗口滑动时,左侧的元素会被移出窗口。
如果队列中的下标小于当前窗口的左边界(i - k + 1
),说明该元素已经不在当前窗口内,需要从队首弹出。
2. 为什么要剔除小于最新元素的元素?
当新元素加入窗口时,如果队列中存在比新元素小的元素,那么这些元素在新元素存在的情况下,永远不可能成为窗口的最大值。
例如,窗口 [3,1,2]
中,新元素 2
加入时,队列中已有 [3,1]
(下标)。由于 1
比 2
小,且 2
在窗口中的位置比 1
更靠右,因此 1
永远不可能成为窗口的最大值,所以将其从队尾弹出。
3. 为什么队列要严格单调递减?
队列的单调性确保了队首元素始终是当前窗口的最大值。
每次窗口滑动后,我们可以直接从队首获取最大值,时间复杂度为 O(1)。
4. 为什么存储下标而不是元素值?
存储下标可以方便我们判断元素是否在当前窗口内。
如果只存储元素值,当窗口滑动时,我们无法知道该元素是否已经超出窗口边界。
五、复杂度分析
- 时间复杂度:O(n)。每个元素最多入队和出队一次,因此时间复杂度是线性的。
- 空间复杂度:O(1)。只需开辟一个新数组和双端队列。
六、总结:单调队列的适用场景
滑动窗口最大值问题是单调队列的经典应用。
当遇到需要在滑动窗口中快速查找最大值或最小值的问题时,都可以考虑使用单调队列来优化。
核心思想:通过维护一个单调队列,避免重复比较,将时间复杂度从 O(nk) 降低到 O(n)。
掌握单调队列的思想后,类似的问题如滑动窗口最小值、区间最大值等都可以迎刃而解。