刷滑动窗口的题,你是不是上手就想写两层循环?我当初也是这么干的,结果这道 hard 题一提交直接超时,给我整懵了。今天就聊聊怎么用「单调队列」把这道题轻松拿捏。
题目说个明白
题目其实不复杂。给你一个整数数组,一个大小为 k 的窗口从左往右慢慢滑,每次挪一格。你要返回每个窗口里的最大值,拼成一个新数组。
我踩过的第一个坑:暴力法
最开始我思路特别直白。窗口滑到哪,我就遍历哪 k 个数字找最大值。写起来两分钟,跑示例也没问题,一提交就超时。毕竟 O (n*k) 的时间复杂度,数组一长根本顶不住。
后来我慢慢反应过来。每次窗口只移动一格,只走一个旧元素、来一个新元素。为了找最大值重新扫一遍窗口,也太浪费了。有没有什么结构,能直接告诉我当前窗口最大值是谁?这时候就轮到单调队列出场了。
单调队列到底是啥
我们要维护的是一个单调递减的队列。重点是,队列里存的不是数值,是元素的下标。
这么做有两个好处:队头永远是当前窗口最大值的下标,拿结果直接用。也方便判断队头元素有没有滑出窗口的左边界。
具体怎么操作
遍历数组的时候,每来一个新元素,先做第一件事。把队列尾部所有对应值比当前元素小的下标,全弹出去。你想啊,这些数既比新来的小,还比它先离开窗口,以后不可能再当最大值了,留着纯纯占位置。弹完就把当前下标加到队尾。
接着做第二件事,检查队头。窗口左边界是 i - k + 1,如果队头下标小于左边界,说明它已经滑出去了,直接从队头删掉。
最后,当窗口正式形成的时候(也就是 i >= k-1),把队头对应的值丢进结果数组就 ok。
拿示例里的 [1,3,-1,-3,5,3,6,7],k=3 走两步你就懂了。
- i=0,队列为空,加 0,队列:
[0] - i=1,3 比 1 大,弹出 0,加 1,队列:
[1] - i=2,-1 比 3 小,加 2,队列:
[1,2]这时候窗口刚好形成,队头是 1,对应值 3,放进结果。后面的步骤都是一个逻辑,顺下来就行。
代码实现
直接上 JavaScript 代码,注释我都标好了,都是我当初踩过的坑。
javascript
var maxSlidingWindow = function(nums, k) {
const res = [];
const deque = []; // 单调递减队列,存元素下标
const n = nums.length;
for (let i = 0; i < n; i++) {
// 队尾元素比当前小就弹出,维持递减
// 别漏了等于的情况,不然重复元素会出问题
while (deque.length && nums[i] >= nums[deque.at(-1)]) {
deque.pop();
}
deque.push(i);
const left = i - k + 1; // 窗口左边界
// 队头已经滑出窗口,从队头移除
if (deque[0] < left) {
deque.shift();
}
// 窗口形成后,收集当前最大值
if (left >= 0) {
res.push(nums[deque[0]]);
}
}
return res;
};
这里我必须提一句,队列一定要存下标。我一开始图省事存数值,结果边界处理全乱套,卡了半小时才反应过来。
复杂度咋样
时间复杂度 O (n),每个元素最多进队一次、出队一次,效率拉满。空间复杂度 O (k),队列最多装 k 个元素。
最后唠两句
这道题本质上考的就是滑动窗口 + 单调队列的组合。核心是理解 "为什么要维护单调递减"------ 把不可能成为最大值的元素提前淘汰掉。我踩过的坑还挺多,存值不存下标、漏了等于的情况、忘了检查队头出界...... 说多了都是泪。
你第一次做这题是直接想到单调队列,还是跟我一样先写了暴力?有没有在什么奇怪的地方踩过坑?评论区唠唠,我每条都会看。觉得文章有用的话,点个赞让更多小伙伴看到呀~