题目:给定一个整数数组nums
和一个大小为k
的滑动窗口,要求将窗口从数组的最左侧移到最右侧,返回每个滑动窗口中的最大值。
解题思路
本题很有意思,乍一看感觉好像很简单,人工模拟很快就能出答案,但实际上代码没有这么简单。
滑动窗口,有点类似于队列,每次滑动无非就是从队列的左边出一个元素,从队列的右边进一个元素,那么如何判断队列中的最大元素则是一个关键的问题。在Java中,我们知道使用优先级队列可以得到一个集合输入元素的最大值或者最小值,因为优先级队列的底层是大根堆或者小根堆,每次输入元素都是堆的构建过程,而此大根堆或者小根堆的底层是数组,因此查询很快。
那么本题就可以使用优先级队列来解决,首先构建一个大小为k
的大根堆,之后每次输出大根堆的堆顶和两边新增结点即可,可得代码如下:
public int[] maxSlidingWindow(int[] nums, int k) {
PriorityQueue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer integer, Integer t1) {
return t1-integer;
}
});
int[] result = new int[nums.length - k + 1];
for(int i=0;i<k;i++){
queue.offer(nums[i]);
}
result[0] = queue.peek();
for(int i=1;i<nums.length-k+1;i++){
queue.remove(nums[i-1]);
queue.offer(nums[i+k-1]);
result[i] = queue.peek();
}
return result;
}
上述代码是没有问题的,但在LeetCode中无法通过测试,超出时间限制,考虑了一下最大的可能性就是堆中节点的删除太耗时了,因为此处是根据结点的内容删除元素,此时就需要遍历整个数组,找到那个元素,因此我们考虑将每个元素的下标都记住,这样删除元素的时候直接根据下标删除即可,可得代码如下:
public int[] maxSlidingWindow(int[] nums, int k) {
PriorityQueue<int[]> queue = new PriorityQueue<>(new Comparator<int[]>() {
@Override
public int compare(int[] ints, int[] t1) {
return t1[0]-ints[0];
}
});
int[] result = new int[nums.length - k + 1];
for(int i=0;i<k;i++){
queue.offer(new int[]{nums[i], i});
}
result[0] = queue.peek()[0];
for(int i=k;i<nums.length;i++){
queue.offer(new int[]{nums[i], i});
while(queue.peek()[1]<=i-k) queue.poll();
result[i-k+1] = queue.peek()[0];
}
return result;
}
上述代码耗时150ms,还是很慢,但通过AC了。上述使用优先级队列的思路可学习的地方就是在如何删除数组最左侧的元素:我们可以每次判断当前堆顶元素的下标,如果下标大于某个值则必然不在此滑动窗口内!出队列即可。
进阶解法
我们使用优先级队列的原因是他可以为我们找到一段序列中的最大值,那么我们可否换个思路:
-
在窗口大小为
k
的范围内,如果后一个元素大于前一个元素,那么前一个元素必然不可能是最大值了! -
后一个元素小于前一个元素,则其可能是下一组的最大元素。
根据上面的分析,我们此时还可以使用队列,只不过此队列是普通的队列,在这个队列中,我们要始终保证队首的元素是最大的,也就是如果下一个元素的值大于队首,则队首出队,换此元素为队首(因为有机会入队那么必然是属于同一个窗口,同一个窗口只有一个最大值)。如果当前元素小于队尾元素,则直接入队(因为可能是下一组的最大元素)。
在保证了上述的条件之后,每次判断队首元素是否同样属于这个窗口,不满足则直接出队即可。之后如果达到k
窗口大小则将队首元素取出赋值即可。代码如下:
public int[] maxSlidingWindow(int[] nums, int k) {
int[] result = new int[nums.length - k + 1];
LinkedList<Integer> queue = new LinkedList<>();
for(int i=0;i<nums.length;i++){
//后一个元素小于前一个元素,则其可能是下一组的最大元素
//后一个元素大于前一个元素,则在这个k邻域范围内不可能是最大元素,因此可以直接出去
while(!queue.isEmpty()&&nums[queue.peekLast()]<=nums[i]){
queue.pollLast();
}
queue.addLast(i);
if(queue.peek()<=i-k) queue.poll();
if(i+1>=k){
result[i+1-k] = nums[queue.peek()];
}
}
return result;
}
最终耗时31ms,时间复杂度和空间复杂度都是O(n)O(n)O(n)。