剑指 Offer59-I-滑动窗口的最大值

问题描述

给定一个数组 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

解题思路

本题可以采用暴力方法求解,每次滑动窗口所移动到的k个数中求最大值,那么每次需要O(k)个时间,一共n步,则O(nk)
当然也可以用单调队列进行求解,单调队列与单调栈问题相类似,单调栈问题参看包含min函数的栈(单调栈)
在这里插入图片描述
单调队列与单调栈问题类似,我们可以观察队列里面是否有一些元素是没有用的,我们把这些没有用的元素去掉的话,看看是否会得到单调性。
如上图为例,假设我们每次求的是滑动窗口中的最小值,当-3进来之后,第一个3肯定没有用,我们每次求的是队列中最小值,-3小于3,3是在-3的左边,所以说,这个3会被先弹出去,换句话说,只要-3在,3就永远不会被当成最小值输出,并且-3还活的更久一点,它会在3被移出滑动窗口之后才会被移出去,因此我们就可以断定前面的3一定不会被当成答案输出出来,就可以去掉。同样,-1也是如此。
因此,只要队列里面存在前面一个数比后面的数还要大,那么前面的数就肯定没有用,因为后面的数会后被弹出来,而且更小,因此,只要有这样的逆序对的话,我们就可以把大的点删掉,我们把所有这样的数都删掉,整个队列就会变成严格单调上升的队列了。
我们要求队列中的最小值,那么严格单调上升的队列就是队头元素,所以每次找最小值的时候,直接找队头元素即可。
总结:单调栈和单调队列的问题,我们可以先考虑用栈和队列来暴力的模拟原来的问题,即常规思路,然后再看看常规思路里面,栈和队列中哪些元素是没有用的,然后再删掉,看看剩下的元素是否有单调性,如果剩下的元素有单调性,就可以做一些优化,如取极值或者二分

而对于本题而言:

由于我们需要求出的是滑动窗口的最大值,如果当前的滑动窗口中有两个下标 i 和 j,其中 i 在 j 的左侧(i < j),并且 i对应的元素不大于 j对应的元素即(nums[i]<=nums[j]),那么会发生什么呢?

当滑动窗口向右移动时,只要 i 还在窗口中,那么 j 一定也还在窗口中,这是 i 在 j的左侧所保证的。因此,由于nums[j] 的存在,nums[i] 一定不会是滑动窗口中的最大值了,我们可以将nums[i] 永久地移除

因此我们可以使用一个队列存储所有还没有被移除的下标。在队列中,这些下标按照从小到大的顺序被存储,并且它们在数组 nums 中对应的值是严格单调递减的。因为如果队列中有两个相邻的下标,它们对应的值相等或者递增,那么令前者为 i,后者为 j,就对应了上面所说的情况,即nums[i] 会被移除,这就产生了矛盾。

当滑动窗口向右移动时,我们需要把一个新的元素放入队列中。为了保持队列的性质,我们会不断地将新的元素与队尾的元素相比较,如果前者大于等于后者,那么队尾的元素就可以被永久地移除,我们将其弹出队列。我们需要不断地进行此项操作,直到队列为空或者新的元素小于队尾的元素。

**由于队列中下标对应的元素是严格单调递减的,因此此时队首下标对应的元素就是滑动窗口中的最大值。**但与方法一中相同的是,此时的最大值可能在滑动窗口左边界的左侧,并且随着窗口向右移动,它永远不可能出现在滑动窗口中了。因此我们还需要不断从队首弹出元素,直到队首元素在窗口中为止。

为了可以同时弹出队首和队尾的元素,我们需要使用双端队列。满足这种单调性的双端队列一般称作「单调队列」

在Java中,双端队列及其应用可以参考Java双端队列Deque及其应用

class Solution {
    
    
    public int[] maxSlidingWindow(int[] nums, int k) {
    
    
        int len=nums.length;
        if(len<1){
    
    
            return nums;
        }
        //创建双端队列,使用LinkedList实现类
        //注意,双端队列存储的是元素的下标
        Deque<Integer> queue=new LinkedList<Integer>();
        List<Integer> list=new ArrayList<Integer>();        //用于保存最终结果
        int st=0,ed=k-1;        //st,ed分别指向滑动窗口的左右边界
        //我们先把初始情况下滑动窗口里面的元素都加入到单调队列中,这里隐含了k>=len的问题
        for(int i=st;i<=ed;i++){
    
        
            //当滑动窗口右移时,会不断判断当前元素是否大于或者等于队尾元素对应的值,如果满足条件的话,就移出队尾元素
            //一直到队列为空或者当前元素小于队尾元素对应的值为止
            //才把当前元素的下标加入到队尾,这样可以保持队列的单调递减的性质
            while(!queue.isEmpty() && nums[i]>=nums[queue.peekLast()]){
    
    
                int temp=queue.pollLast();
            } 
            queue.offerLast(i);
        }
        //把队头元素加入到结果集中,队头元素对应的值即滑动窗口的最大值
        list.add(nums[queue.peekFirst()]);
        while(ed<len-1){
    
    
            //滑动窗口后移一位
            st++;
            ed++;
            //当队列不为空,并且队头元素(下标)处在滑动窗口左端点的左边时,就需要把它移出队列
            if(!queue.isEmpty() && queue.peekFirst()<st){
    
    
                int temp=queue.pollFirst();
            }
            //当滑动窗口右移时,会不断判断当前元素是否大于或者等于队尾元素对应的值,如果满足条件的话,就移出队尾元素
            //一直到队列为空或者当前元素小于队尾元素对应的值为止
            //才把当前元素的下标加入到队尾,这样可以保持队列的单调递减的性质   
            while(!queue.isEmpty() && nums[ed]>=nums[queue.peekLast()]){
    
    
                int temp=queue.pollLast();
            }
            queue.offerLast(ed);
            list.add(nums[queue.peekFirst()]);
        }
        //把结果存入到数组中
        int size=list.size();
        int res[]=new int[size];
        for(int i=0;i<size;i++){
    
    
            res[i]=list.get(i);
        }
        return res;
    }
}

猜你喜欢

转载自blog.csdn.net/qq_39736597/article/details/113815879