【剑指offer】59 - I. 滑动窗口的最大值

剑指 Offer 59 - I. 滑动窗口的最大值

知识点:队列;滑动窗口;单调数组

题目描述

给定一个数组 nums 和滑动窗口的大小 k,请找出全部滑动窗口里的最大值。ui

示例
输入: 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

解法一:滑动窗口+双端队列+单调

滑动窗口整体上分红两类,一类是可变长度的滑动窗口,一类是固定长度的滑动窗口,这道题目就是固定长度的。在遍历元素时,为了保持窗口的大小固定,右侧元素进入窗口后,左侧元素要可以出去。而后直到遍历结束。
想一下刚才的过程,右侧元素进入,左侧元素出去,这不就是双端队列吗?因此这道题目能够借助双端队列来解;code

想一下咱们常常会遇到求一个队列或者一个窗口一个栈内的最大最小值,怎么求呢,最简单的方法就是遍历这个窗口这个栈,这样时间复杂度就是O(N),有没有办法能在O(1)时间内得到一个栈或者一个窗口内的最值呢,这其实就是剑指offer30题,好比获取一个栈内的最小值,咱们能够采用一个辅助栈,这个辅助栈有一个最大的特色就是单调的,也就是咱们俗称的单调栈。好比咱们维持一个单调递减栈,若是当前值比栈顶元素大,那就不要了,由于咱们最后只获取最小值,若是比当前栈顶小,那就入栈,也就是更新了最小值;这样就能够在O(1)的时间内得到栈内最小值了,由于最小值就是辅助栈的栈顶。索引

这道题目也相似啊,咱们须要得到窗口内的最大值,这不就是一个双端队列的最大值吗,因此咱们要维持一个单调递减的双端队列,如何实现呢,每次入队前,判断此值与队尾元素的大小,小于的话就入队,这样就维持了一个单调递减队列;若是元素比队尾值要大,那就要将队尾元素出队了,由于咱们只关注大的值,可不能把这个大值错过了,这里面的小值就不用管了。队列

好比[5,3,4], 4要入队的时候发现3比其小,因此3从队尾出去,4入队;leetcode

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int len = nums.length;
        if(len == 0) return nums;
        int[] res = new int[len-k+1];
        Deque<Integer> deque = new LinkedList<>(); //双端队列;
        int index = 0;
        //未造成窗口;
        for(int i = 0; i < k; i++){
            while(!deque.isEmpty() && nums[i] > deque.peekLast()){
                deque.removeLast(); //保证单调递减队列;
            }
            deque.offerLast(nums[i]);
        }
        res[index++] = deque.peekFirst();  //队首始终是最大的;
        //滑动窗口;
        for(int i = k; i < len; i++){ //i表明当前窗口最后一个元素的索引;
            //保证队内只含有窗口内的元素,因此当窗口的前一个元素等于队首的时候,要将队首出队;
            if(deque.peekFirst() == nums[i-k]){
                deque.removeFirst(); 
            }
            while(!deque.isEmpty() && nums[i] > deque.peekLast()){
                deque.removeLast();  //保证单调递减队列;
            }
            deque.offerLast(nums[i]);
            res[index++] = deque.peekFirst(); //队首始终是当前窗口内最大的;
        }
        return res;
    }
}

解法二:滑动窗口+单调

上面的作法咱们每次入队的是元素的值,本质上就是用双端队列来模拟了窗口的滑动,双端队列是单调队列;
其实咱们也能够用一个单调队列,入队的是元素的下标索引。这样其实咱们能很明显的看出窗口的滑动,只要队首元素的下标<窗口的左边界,那就要把队首移除了,窗口进行了一次滑动;一个很明显的不一样,入队的是下标索引rem

流程get

  • 遍历给定数组中的元素,若是队列不为空且当前考察元素大于等于队尾元素,则将队尾元素移除。直到,队列为空或当前考察元素小于新的队尾元素;
  • 当队首元素的下标小于滑动窗口左侧边界left时,表示队首元素已经再也不滑动窗口内,所以将其从队首移除。
  • 因为数组下标从0开始,所以当窗口右边界right+1大于等于窗口大小k时,意味着窗口造成。此时,队首元素就是该窗口内的最大值。
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int[] res = new int[nums.length-k+1];
        if(nums.length == 0 || nums == null) return new int[]{}; //特例为空;
        Deque<Integer> deque = new LinkedList<>();
        //right 为窗口右边界;
        for(int right = 0; right < nums.length; right++){
            //若是队列不为空且当前考察值>队尾元素,将队尾元素移除,直到为空或遇到大的;
            while(!deque.isEmpty() && nums[right] > nums[deque.peekLast()]){
                deque.removeLast();
            }
            deque.offerLast(right); //存储下标;
            int left = right-k+1; //窗口左侧边界下标;
            if(deque.peekFirst() < left){
                deque.removeFirst();  //窗口进行了移动,左侧出去;
            }
            if(right + 1 >= k){
                res[left] = nums[deque.peekFirst()]; //这时候窗口造成,开始逐步获得答案;
            }
        }
        return res;
    }
}

体会

  • 滑动窗口一共有两种类型:io

    • 窗口长度可变:这种类型中长度是能够变化的,一个基本的流程就是,右边界长,而后到达某一个条件(好比窗口内的和达到某个值,窗口中出现了重复的元素),这时候右边界停下来,左边界长,而后跳出这个条件(好比窗口内的和又小于目标值了,好比窗口中又没有重复元素了),这时候右边界再去移动;(咱们要处理的始终保证窗口内知足某个条件,例如窗口内的值小于某值,窗口内没有重复的,只要不知足了就去移左边界);
    • 窗口长度固定,好比说固定一个长度的窗口的时候,那右边界长的时候,左边界也得跟着长,维持一个窗口的恒定值;
  • 要始终明白滑动窗口的左右边界是不会出现回退的,两个边界确定都是朝着一个方向前进的,不会走回头路。ast

  • 其次要知道滑动窗口其实就是一个队列,右边界移动就是有新元素入队了,左边界移动就是有元素出队了,因此在作题的时候能够想象成一个队列在进行处理,可能会想的更清楚;

参考连接

滑动窗口的最大值

相关文章
相关标签/搜索