题目描述
给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。
例如,如果输入数组{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]}。


  在每个滑动窗口依次比较找出最大值,每进来一个数循环比较一次,然后窗口会移动n次,时间复杂度是O(nk)。所以需要进行优化,有很多方式实现,比如队列来记录,或者使用堆。

解法1:

蛮力法

import java.util.ArrayList;
public class Solution {
    public ArrayList<Integer> maxInWindows(int [] num, int size){
        ArrayList<Integer> result = new ArrayList<>();
        if(num == null || size > num.length || size == 0)
           return result;
        int first = 0;
        int last = first+size-1;
        for(int n = 0; n < num.length - size + 1; n++){
            getMax(num,result,first,last);
            first++;
            last++;
        }
//改成while可以省一个变量,因为n和first同步变化
        //while(first < num.length - size + 1){
        //  getMax(num,result,first,last);
        //   first++;
        //   last++;
        //}
       return result; 
    }
 //获取最大值的方法可以根据个人喜好选择
    public void getMax(int [] num,ArrayList<Integer> result, int first, int last) {
        int i = first;
        int tmp = num[first];
        while(i<last){
            if(tmp<num[i+1])
                tmp = num[i+1];//如果这个数比较小,那么就更新成和它相比,比较大的数
            i++;
        }
        result.add(tmp);
    }
}

解法2:

  不管是建立堆还是队列或者其他方法,都需要保证使用的数据结构里面是有序的。其实也是一种排序的优化。在第一个窗口移动前,需要保证这个窗口已经有序,所以在调整窗口前需要先排序。整个队列其实是从大到小排列的。
  思路:建立一个两端开口的队列,放置所有可能是最大值的数字(存放的其实是最大值的下标),队首记录的是最大值的下标。从头开始扫描数组:

  • 1.如果遇到的数字比队列中所有的数字都大,那么它就是最大值,其它数字不可能是最大值了,将队列中的所有数字清空,放入该数字。(在尾部添加的)
  • 2.如果遇到的数字比队列中的所有数字都小,那么它还有可能成为之后滑动窗口的最大值,放入队列的末尾
  • 3.如果遇到的数字比队列中最大值小,最小值大,那么将比它小数字不可能成为最大值了,删除较小的数字,放入该数字。
  • 其中1和3可以放在一起处理,代码复用。
  • 由于滑动窗口有大小,因此,队列头部的数字如果其下标离滑动窗口末尾的距离大于窗口大小,那么也删除队列头部的数字。

  这么说的可能还是有点抽象,以题目给的例子分析,{2,3,4,2,6,2,5,1}及滑动窗口的大小3。第一个窗口的过程是,队列变成{2},3比2大,队列为{3},同理,4进入后,队列变成{4}。注意,队列的尾部的4就是第一个窗口最大值,应该添加到res中。(为了方便起见,下列分析中,队列中写的是数字,在实际代码中,队列中的是对应数字在整个数组中的索引)
  第二个窗口进来了2,将2和4比较,发现4大,说明第二窗口最大还是4,将4添加到res,2添加到队列中,队列变成{4,2}。
  第三个窗口进来了6,6比队首的4还大,清空队列,添加6到队列,队列{6},添加6到res。
  第四个窗口,2进来了,res添加6,队列变成{6,2}。
  第五个窗口进来5,5比2大,比6小。res添加队首6,队列变成{6,5}。
  最后一个窗口进来1,注意,6不在最后一个窗口中了,此时应该删除队首。而1也进不去队列,队列为{5},res添加5。因此res中添加的顺序就是4,4,6,6,6,5。
  在最后一个窗口进来1时,6应当被移出去,所以我们需要记录每一个进来的元素的索引,否则我们就不知道队首什么时候有效,什么时候失效,这就是队列中不存放元素,而存放对应元素的角标的原因。

import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
public class Solution {
    public ArrayList<Integer> maxInWindows(int [] num, int size){
        ArrayList<Integer> res = new ArrayList<>();
        Deque<Integer> deque = new LinkedList<>();
        if(size <= 0 || size > num.length){
            return res;
        }
        for (int i = 0; i < size; i++) {//在第一个窗口中,挑出最大的数,并记录该数的角标
            while (!deque.isEmpty() && num[deque.peekLast()] < num[i]){
                deque.pollLast();
            }
            deque.addLast(i);
        }
        res.add(num[deque.peekFirst()]);//第一个窗口已经比较完了,添加操作。

        for (int i = size; i < num.length; i++) {
            if (num[i] < num[deque.peekLast()]){//小于就直接添加到队列(注意比较的是队尾)
                deque.addLast(i);
            }else {//这个写法将大于或等于的两种情况合并成了一种写法
                while (!deque.isEmpty() && num[i] >= num[deque.peekLast()]){
                    deque.pollLast();
                }
                deque.addLast(i);
            }
            while (deque.peekLast() - deque.peekFirst() >= size){//如果太长,要删掉队首
                deque.pollFirst();
            }
            res.add(num[deque.peekFirst()]);//删掉队首后,可以放心的添加了
        }
        return res;
    }
}

解法3:

heap的方法也值得自己去学习。摘过来某位大佬的写法

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;
public class Solution {
    public ArrayList<Integer> maxInWindows(int [] num, int size)
    {
           ArrayList<Integer> list=new ArrayList<>();
        if(size==0||size>num.length){
            return list;
        }
        PriorityQueue<Integer>  maxheap=new PriorityQueue<>
                        (size,new Comparator<Integer>(){
           @Override
           public int compare(Integer o1, Integer o2) {
               return o2-o1;
           }
       });

        int z=0;
        for(int i=0;i<size;i++){
            maxheap.add(num[i]);
        }
        for(int i=size;i<num.length;i++){
            list.add(maxheap.peek());
            maxheap.remove(num[z++]);
            maxheap.add(num[i]);
        }
        list.add(maxheap.peek());
        return list;
    }
}

总结:

有一说一,第二种是书上的方法,第一次接触这个题,这个解法不太容易想到。可以看到第2,3种解法都是使得窗口已经有序,这样就避免每更新一个值时,需要操作很多次,所以这题也是考察了一下排序的优化。