滑动窗口的最大值

滑动窗口的最大值

给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{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() 返回队列反向迭代器
posted @ 2020-03-10 17:21  别再闹了  阅读(139)  评论(0)    收藏  举报