你有没有遇到过这样的场景:一群小伙伴排队买奶茶,每次只能有k个人进店,老板想知道每一批进店小伙伴中,谁的身高最高?这其实就是著名的“滑动窗口最大值”问题!别眨眼,今天带你用最通俗的语言,玩转队列和滑动窗口,顺便避开那些让人头秃的坑!
一、滑动窗口是什么?队列又是啥?
先来点“人话”解释:
- 滑动窗口:就像你用尺子在一串数字上滑动,每次只看尺子覆盖的那一段,尺子长度就是窗口大小k。
- 队列:现实生活中排队买奶茶,先来先服务,谁先进谁先出(FIFO)。
在算法世界里,这俩是黄金搭档,尤其是在处理“连续区间最大值”这类问题时,简直是如鱼得水。
二、LeetCode 239:滑动窗口最大值
题目大意:给你一个数组和一个窗口大小k,让你输出每个窗口里的最大值。
举个栗子:
输入: nums = [1,3,-1,-3,5,3,6,7], k = 3
输出: [3,3,5,5,6,7]
每次窗口滑动一格,记录当前窗口的最大值。
三、初学者的“暴力解法”——简单粗暴但慢
很多同学第一反应就是:每次窗口滑动时,把窗口里的数都遍历一遍,找最大值。
代码长这样:
function getMax(arr){
let max = -Infinity;
for (let i = 0; i < arr.length; i++) {
if (arr[i] > max) max = arr[i];
}
return max;
}
主函数里,每次窗口滑动都调用一次 getMax,结果就是——能跑,但大数据下会超时。
四、暴力解法的“致命伤”
- 时间复杂度高:每次窗口都要遍历k个元素,总共n-k+1个窗口,复杂度O(nk)。
- 大数据下会超时:LeetCode会毫不留情地给你一个“超时警告”。
- 代码虽然简单,但效率感人。
五、初学者常见的“引用大坑”
有同学会这样初始化二维数组:
const arr2 = new Array(5).fill(new Array(5).fill(0));
看起来很美好,实际上每一行都是同一个数组的引用,改一个全都变!
正确姿势:
const arr2 = new Array(5).fill().map(() => new Array(5).fill(0));
每一行都是独立的,互不干扰。
六、队列的正确打开方式
队列在滑动窗口问题中,常用来维护窗口内的元素。最常见的就是单调队列,它能让你在O(1)时间内拿到窗口最大值。
单调队列的核心思想:
- 队列里只存可能成为最大值的元素(或索引)。
- 新元素进队时,把队尾比它小的都踢出去。
- 队首永远是当前窗口的最大值。
七、单调队列解法——高效又优雅
来一段“掘金级”代码:
var maxSlidingWindow = function(nums, k) {
const deque = [];
const res = [];
for (let i = 0; i < nums.length; i++) {
// 队尾小于当前元素的都出队
while (deque.length && nums[i] >= nums[deque[deque.length - 1]]) {
deque.pop();
}
deque.push(i);
// 队首超出窗口范围就出队
if (deque[0] <= i - k) {
deque.shift();
}
// 记录最大值
if (i >= k - 1) {
res.push(nums[deque[0]]);
}
}
return res;
};
时间复杂度O(n),LeetCode大数据也能轻松通过!
八、幽默小结:队列的“买奶茶哲学”
想象一下,队列里每个人都想当“窗口最大值”,但只要有比你高的进来,你就得乖乖让位。队首的那位,永远是当前窗口的“身高王者”。
这就是单调队列的精髓:只留最有竞争力的选手,其他的都请出队!
九、初学者常见问题答疑
- 为什么不用数组的sort?
- sort复杂度O(nlogn),而且会改变原数组,得不偿失。
- 为什么不用暴力法?
- 小数据可以,大数据就“超时”了。
- 单调队列难吗?
- 其实就是维护一个“只存最大值候选人”的队列,理解了就很简单。
十、写在最后
滑动窗口和队列是算法面试中的高频考点。初学时别怕踩坑,理解原理、勤加练习,队列和窗口就会变成你算法路上的好帮手。
下次再遇到“窗口最大值”,你就能自信地说:“这题我会!”