要求:
给定一个数组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
思路1:
先引入介绍一个此类方法常用的思路:单调双向队列。双向队列的意思就是既可以在头部插入删除,又可以在尾部插入删除,单调就是指队列存储的元素是依次递减或递增的。此处借用一张图以示说明:
头部 尾部
--------------------------
| 5 3 2 1 0 -1 |
--------------------------
由大 → 到小
我们一直要使用这个单调的双向队列来做判断。 说是双向队列其实是用LinkedList实现的,因为LinkedList有添加,删除队首队尾元素等方法。 每次窗口滑动的时候需要做两个判断:
- 之前的最大值是不是已经不在窗口范围里了
- 新加进来的值是不是比之前的最大值要来的大
- 如果刚进来的元素比队列尾部元素大,那么先将队列尾部的元素弹出,把刚进来的元素添加到队列尾部。这个操作需要循环操作,只要队尾元素小就要弹出,以此来保证队列元素的单调性。
- 若刚进来的元素比队列的队尾元素小,那么将元素直接添加到队列尾部即可。
演示过程:
添加元素
假设要进入队列的元素是5、4、1、2、6。第一次队列为空,所以直接进入即可。元素5,索引为0.
头部 尾部
--------------------------
| 5 |
| ↑ |
| 0 |
--------------------------
大 小
第二次进入队列的是元素4,索引为1,需要与5比较后再进。比5小,可以进。
头部 尾部
--------------------------
| 5 4 |
| ↑ ↑ |
| 0 1 |
--------------------------
大 小
第三次进入队列的是元素1,索引为2,需要与4比较后再进。比4小,可以进。
头部 尾部
--------------------------
| 5 4 1 |
| ↑ ↑ ↑ |
| 0 1 2 |
--------------------------
大 小
第四次进入队列的是元素2,索引为3,需要与1比较后再进。比1大,所以需要丢掉队尾元素1,才能让2进。
头部 尾部
--------------------------
| 5 4 2 |
| ↑ ↑ ↑ |
| 0 1 3 |
--------------------------
大 小
每次滑动窗口,其实就是每次添加新元素进去,都可以取一个最大值出来。 最后,来了索引为4的元素6,比里面的所有元素都大,就需要弹出所有元素,让6进队。
头部 尾部
--------------------------
| 6 |
| ↑ |
| 5 |
--------------------------
大 小
删除元素
例如:窗口大小为2,此前窗口包含[5,4],现在窗口是[4,1],需要删除5,让4成为队首元素。
元素: 5 [4 1] 2
索引: 0 1 2 3
头部 尾部
--------------------------
| 5 4 1 |
| ↑ ↑ ↑ |
| 0 1 2 |
--------------------------
大 小
删除元素后:
头部 尾部
--------------------------
| 4 1 |
| ↑ ↑ |
| 1 2 |
--------------------------
大 小
注意事项:
- 我们存储进队列,进行比较的是索引,并不是具体的值,因为双向队列是靠LinkedList实现的,提供的是数组,有下标,队列里的元素是没有下标的,所以我们一直用索引记录数字,这样好从数组里取数字。
- 然后当窗口滑动的时候,需要检查当前的最大元素的索引是否还在窗口范围内。
- 新要入队的元素只要大小等于队尾元素,都需要进行替换
- 不需要特别的去为了前面几个单独的数字做比较,只需要判断窗口有没有形成,只有形成了大小为k的窗口,才开始收集窗口内的最大值。
代码:
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || k < 1 || nums.length < k) {
return new int[0];
}
int index = 0;
int[] res = new int[nums.length - k + 1];
LinkedList<Integer> qMax = new LinkedList<>();
for (int i = 0; i < nums.length; i++) {
// 在队列不为空的情况下,如果队列尾部的元素要比当前的元素小,或等于当前的元素
// 那么为了维持从大到小的原则,我必须让尾部元素弹出
while (!qMax.isEmpty() && nums[qMax.peekLast()] <= nums[i]) {
qMax.pollLast();
}
// 不走 while 的话,说明我们正常在队列尾部添加元素
qMax.addLast(i);
// 如果滑动窗口已经略过了队列中头部的元素,则将头部元素弹出
if (qMax.peekFirst() == (i - k)) {
qMax.pollFirst();
}
// 看看窗口有没有形成,只有形成了大小为 k 的窗口,我才能收集窗口内的最大值
if (i >= (k - 1)) {
res[index++] = nums[qMax.peekFirst()];
}
}
return res;
}
}
方法二:优先队列
还是这种抖机灵的做法,先往里添加元素。重写优先队列,使得它改为按从大到小排列,而优先队列有一个很巧的方法是remove()指定元素。这样我们不需要像上面那样在一个数组中保存索引了。
代码:
import java.util.*;
public class Solution {
public ArrayList<Integer> maxInWindows(int [] num, int size){
ArrayList<Integer> res = new ArrayList<Integer>();
if (size > num.length || size <= 0) return res;
PriorityQueue<Integer> heap = new PriorityQueue<Integer>(15,new Comparator<Integer>(){
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
for (int i = 0; i < size; i++) {
heap.add(num[i]);
}
res.add(heap.peek());
for (int i = 0; i < num.length - size ; i++) {
heap.remove(num[i]);
heap.add(num[size + i ]);
res.add(heap.peek());
}
return res;
}
}
个人感觉优先队列是一个很好的工具,然后操作的步骤自己也可以想得到,就好比是java顺着我们的心意给我们的一个工具。单调双向队列,就也很常用,需要学习一下,然后后面也会经常用到的那种。