滑动窗口的最大值
滑动窗口的最大值
给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}
这道题还是要看清题啊,我一开始以为是要返回和最大的那个窗口,汗
讲道理,这道题真的很适合用堆解决:一开始我想的是维护一个整数记录这个窗口中的最大值,为了应对窗口移动还设置了一个整数计算最大值的个数,减到0了之后就换,但是后来才发现一个问题,就是如果原来的最大值因为窗口移动减没了,怎么确定下一个最大值,如果遍历确定的话会导致很差的最大时间消耗,比如数组是5,4,3,2,1这样的,每次移动都要重新确定,那时间消耗是O(n^2)的,很差。
但是其实也是可以的,只需要再加一个freq变量表示最大值在窗口中出现了几次,但是这样的话对于有些情况就需要遍历两次窗口了,一次找最大值一次计算出现了几次。如果最差的话是O(nk)的,但是一般不会那么差,因为只有特定的情况需要重新遍历窗口,所以oj其实也过了
代码如下:
import java.util.ArrayList;
public class Solution {
public ArrayList<Integer> maxInWindows(int [] num, int size)
{
ArrayList<Integer>ans=new ArrayList<Integer>();
if(size<=0){
return ans;
}
if(size>num.length){
return ans;
}
int max=Integer.MIN_VALUE;
int freq=0;
for(int i=0;i<size;i++){
if(num[i]>max){
max=num[i];
}
}
for(int i=0;i<size;i++){
if(num[i]==max){
freq++;
}
}
ans.add(max);
//i是要舍弃的左边缘
for(int i=0;i<num.length-size;i++){
int right=i+size;
if(num[right]>max){
max=num[right];
freq=1;
ans.add(max);
}else{
if(num[i]==max&&freq==1){
//重新找max
max=Integer.MIN_VALUE;
freq=0;
for(int j=i+1;j<=i+size;j++){
if(num[j]>max){
max=num[j];
}
}
for(int j=i+1;j<=i+size;j++){
if(num[j]==max){
freq++;
}
}
ans.add(max);
}else if(num[i]==max){
freq--;
ans.add(max);
}else{
ans.add(max);
}
}
}
return ans;
}
}
但是这种情况就特别时候大顶堆,比较找最大值就是它的本职工作。
import java.util.*;
public class Solution {
public ArrayList<Integer> maxInWindows(int [] num, int size)
{
ArrayList<Integer>ans=new ArrayList<Integer>();
int length=num.length;
if(length==0||length<size||size<=0){
return ans;
}
PriorityQueue<Integer>big=new PriorityQueue<Integer>(new Comparator<Integer>(){
@Override
public int compare(Integer i,Integer j){
return j.compareTo(i);
}
});
for(int i=0;i<size;i++){
big.offer(num[i]);
}
ans.add(big.peek());
int left=0,right=size-1;
while(right<length-1){
big.remove(num[left]);
left++;
right++;
big.offer(num[right]);
ans.add(big.peek());
}
return ans;
}
}
但是用堆的话就有一个缺点,就是时间复杂度不是最优的。这篇博客里有对其复杂度的介绍:建堆的时候是O(n)的,每次插入都是O(lgn)的,而每次删除是O(n)的,如果删除的不是根节点的话(删除根节点是O(lgn)),其实主要是堆是基本无序的,只约束了父节点大于等于子节点,要找到特定的节点很麻烦。当然我们在本题中堆的大小是确定的,就是窗口大小,所以这个n其实说的是那个size,那么总的时间复杂度应该是O(nk),k就是size,还是不太好的
最好的方法是使用双向链表LinkedList,这篇博文有详细介绍。这个双向链表挺常用的,可以用来实现栈和队列。但是它的效率不高,现在推荐使用的是以循环数组为底层的ArrayDeque来实现栈和队列,参见这篇博文。当然这里我们就不计较了,这两个都实现了Queue接口,使用方法都是一样的。
这里的思路是每次都和队列的末尾比较,如果当前值比末尾大就将末尾弹出,直到当前值没有末尾大,然后将其放入末尾,每次的最大值就是头部值。这个道理挺明显的,因为每次放入之前都和尾部进行了比较,直到现存的尾部比它大才放入,所以队列中是从左到右的降序,很适合做这个问题。并且,双端队列中的元素一定是符合原来数组中的排序的,因为插入都在尾部,弹出都在左部。所以即使因为窗口移动而弹出,弹出的也绝对是最左边的元素。此外,它很聪明地保存的是数组的下标而不是值,因为数组的随机访问性质非常好,每次拿着下表去找值即可。但是这就要求写程序的时候要保持清醒,知道自己现在操作的是下标还是值,需要的是下标还是值
import java.util.*;
public class Solution {
public ArrayList<Integer> maxInWindows(int [] num, int size) {
if (num == null || num.length == 0 || size <= 0 || num.length < size) {
return new ArrayList<Integer>();
}
ArrayList<Integer> result = new ArrayList<>();
//双端队列,用来记录每个窗口的最大值下标
//这里可以声明对象类型是Deque,因为ArrayDeque也实现了这个接口
LinkedList<Integer> qmax = new LinkedList<>();
for (int i = 0; i < num.length; i++) {
while (!qmax.isEmpty() && num[qmax.peekLast()] < num[i]) {
qmax.pollLast();
}
qmax.addLast(i);
//判断队首元素是否过期
if (qmax.peekFirst() == i - size) {
qmax.pollFirst();
}
//向result列表中加入元素
if (i >= size - 1) {
result.add(num[qmax.peekFirst()]);
}
}
return result;
}
}
它的好处就是时间复杂度是O(n)的,因为只遍历了一遍
下面是我使用ArrayDeque实现的:
import java.util.*;
public class Solution {
public ArrayList<Integer> maxInWindows(int [] num, int size)
{
ArrayList<Integer>ans=new ArrayList<Integer>();
int length=num.length;
if(length==0||length<size||size<=0){
return ans;
}
//这次选择ArrayDeque实现双端队列
Deque<Integer>big=new ArrayDeque<Integer>(size+1);
for(int i=0;i<length;i++){
//这里要注意,比较大小时候不要错拿着下标比较
while(!big.isEmpty()&&num[big.peekLast()]<num[i]){
big.pollLast();
}
big.addLast(i);
if(big.peekFirst()<=i-size){
big.pollFirst();
}
if(i>=size-1){
//这里也要注意,不要错把下标放进去了
ans.add(num[big.peekFirst()]);
}
}
return ans;
}
}
附:Deque接口定义的方法
修饰符和返回值 | 方法名 | 描述 |
---|---|---|
添加功能 | ||
void | push(E) | 向队列头部插入一个元素,失败时抛出异常 |
void | addFirst(E) | 向队列头部插入一个元素,失败时抛出异常 |
void | addLast(E) | 向队列尾部插入一个元素,失败时抛出异常 |
boolean | offerFirst(E) | 向队列头部加入一个元素,失败时返回false |
boolean | offerLast(E) | 向队列尾部加入一个元素,失败时返回false |
获取功能 | ||
E | getFirst() | 获取队列头部元素,队列为空时抛出异常 |
E | getLast() | 获取队列尾部元素,队列为空时抛出异常 |
E | peekFirst() | 获取队列头部元素,队列为空时返回null |
E | peekLast() | 获取队列尾部元素,队列为空时返回null |
删除功能 | ||
boolean | removeFirstOccurrence(Object) | 删除第一次出现的指定元素,不存在时返回false |
boolean | removeLastOccurrence(Object) | 删除最后一次出现的指定元素,不存在时返回false |
弹出功能 | ||
E | pop() | 弹出队列头部元素,队列为空时抛出异常 |
E | removeFirst() | 弹出队列头部元素,队列为空时抛出异常 |
E | removeLast() | 弹出队列尾部元素,队列为空时抛出异常 |
E | pollFirst() | 弹出队列头部元素,队列为空时返回null |
E | pollLast() | 弹出队列尾部元素,队列为空时返回null |
迭代器 | ||
Iterator |
descendingIterator() | 返回队列反向迭代器 |