题目描述
难度:困难
给定一个数组 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
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
提示:
你可以假设 k 总是有效的,在输入数组不为空的情况下,1 ≤ k ≤ 输入数组的大小。
题解分析
此题可以通过维护一个单调队列这样一种策略来解题。(不熟悉的话可以先学习一下单调队列)
单调队列,简单来说就是按顺序往队列存放一个区域的值时,队列的头部元素是一个区域(窗口) 的最值,无论最大最小都可;然后以此题来说队列会维护成一个降序排列。(如果要求最小值那么就是一个升序排列)
每次窗口移动的时候,队列会去除掉一些元素并添加一个元素,这类变化就是来维护这个队列的特性,即队列的头部是该窗口的最大值,队列降序排序。
窗口移动时,我们都要向队列中添加移动后窗口的最后一个元素并尝试排除原窗口中的第一个元素。
单调队列的特性如何维持呢?
当我们向队列尾部添加一个元素 i 时,为了使队列维持一个降序排序,可以将队列中所有小于该元素 i 的元素剔除,这样这个队列就能维持成降序排序。如队列中元素为 3 -1 -2 时,如果向尾部插入一个元素为 2 ,那么队列元素就会变成 3 2,小于2的元素都会被剔除。
另外,由于是滑动窗口,有一种可能是队列中的头部最大值为上一窗口的值,我们可以在每次滑动时进行判断,如果窗口滑动了,并且队列中的头部最大值为上一窗口的值,那么就将它剔除。
再按照上面维护降序排序的逻辑,往队列尾部放入滑动窗口中的新元素,此时的队列头部就一定是当前窗口的最大值,记录下来即可。
解题代码
/*
单调队列 在运行时获取每个区域的最值
此题队列头部维护滑动窗口中的最大值 队列保持一个降序排序,即后面插入的元素会把前面所有比它小的元素剔除
维护这个特性可以保证头部元素移除后,新的头部元素一定是最大值
*/
public int[] maxSlidingWindow(int[] nums, int k) {
if( nums == null || nums.length == 0 || k == 0) return new int[0]; // 检查
int[] res = new int[nums.length - k + 1]; // 返回的数组的长度为 多出来的数组长度 + 选中的窗口中的最大值(窗口中会选出1最大值)
Deque<Integer> mq = new LinkedList<>(); // 单调队列
// 先处理第一个窗口
for (int i = 0 ; i < k ; i++){
while (!mq.isEmpty() && mq.peekLast() < nums[i]) mq.pollLast(); // 剔除队列前面所有比当前插入元素小的值
mq.offerLast(nums[i]);
}
res[0] = mq.peekFirst(); // 此时头部元素是最大值
for(int i = k ; i < nums.length ; i++){
if(nums[i - k] == mq.peekFirst()) mq.pollFirst(); // 对队头元素做排查
while (!mq.isEmpty() && mq.peekLast() < nums[i]) mq.pollLast(); // 剔除队列前面所有比当前插入元素小的值
mq.offerLast(nums[i]);
res[i - k + 1] = mq.peekFirst();
}
return res;
}
总结
新学了一手单调队列,感觉用法很神奇,对于求这种运动区域的最值有奇效,再接再厉!
岁月悠悠,衰微只及肌肤;热忱抛却,颓废必致灵魂