滑动窗口—最值问题

题目一:剑指 Offer 59 - II. 队列的最大值

问题描述

请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。若队列为空,pop_front 和 max_value 需要返回 -1。

输入: 

  • ["MaxQueue","push_back","push_back","max_value","pop_front","max_value"]
  • [[],[1],[2],[],[],[]]

输出: [null,null,null,2,1,2]

算法思路

我们知道对于一个普通队列,push_back 和 pop_front 的时间复杂度都是O(1),因此我们直接使用队列的相关操作就可以实现这两个函数。对于 max_value 函数,我们通常会这样思考,即每次入队操作时都更新最大值:59.gif

但是当出队时,这个方法会造成信息丢失,即当最大值出队后,我们无法知道队列里的下一个最大值。

fig2.gif

为了解决上述问题,我们只需记住当前最大值出队后,队列里的下一个最大值即可。具体方法是使用一个双端队列 deque,在每次入队时,如果 deque队尾元素小于即将入队的元素 value,则将小于 valuevalue 的元素全部出队后,再将 valuevalue 入队;否则直接入队。这时,辅助队列 deque 队首元素就是队列的最大值。

fig3.gif

上段文字来源:https://leetcode-cn.com/problems/dui-lie-de-zui-da-zhi-lcof/solution/ru-he-jie-jue-o1-fu-za-du-de-api-she-ji-ti-by-z1m/

class MaxQueue {
    //保存队列所有数据
    Deque<Integer> dq = new LinkedList<Integer>();
    //保存队列中后出队的最大值
    Deque<Integer> decrease = new LinkedList<Integer>();

    public MaxQueue() {
    }
    
    public int max_value() {
        return this.dq.isEmpty()?-1:this.decrease.peekFirst();
    }
    
    public void push_back(int value) {
        //如果 deque队尾元素小于即将入队的元素 value,则将小于 valuevalue 的元素全部出队后,再将 valuevalue 入队
        while(!this.decrease.isEmpty() && this.decrease.peekLast() < value){
            this.decrease.removeLast();
        }
        this.decrease.offerLast(value);
        this.dq.offerLast(value);
    }
    
    public int pop_front() {
        int res = this.dq.isEmpty()?-1:this.dq.peekFirst();
        if(!this.dq.isEmpty())
            this.dq.removeFirst();
        //如果此次出队的是剩余队列中最大元素,则在最大值队列中将该元素弹出
        if(!this.decrease.isEmpty() && res == this.decrease.peekFirst()){
            this.decrease.removeFirst();
        }  
        return res;
    }
}

题目二:155. 最小栈

问题描述

设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。

  • push(x) —— 将元素 x 推入栈中。
  • pop() —— 删除栈顶的元素。
  • top() —— 获取栈顶元素。
  • getMin() —— 检索栈中的最小元素

输入:

  • ["MinStack","push","push","push","getMin","pop","top","getMin"]
  • [[],[-2],[0],[-3],[],[],[],[]]

输出:

  • [null,null,null,null,-3,null,0,-2]

算法思路

借用一个辅助栈min_stack,用于存获取stack中最小值。

  • push()方法: 每当push()新值进来时,如果 小于等于 min_stack栈顶值,则一起push()到min_stack,即更新了栈顶最小值;
  • pop()方法: 判断将pop()出去的元素值是否是min_stack栈顶元素值(即最小值),如果是则将min_stack栈顶元素一起pop(),这样可以保证min_stack栈顶元素始终是stack中的最小值。
  • getMin()方法: 返回min_stack栈顶即可。

min_stack作用分析:

  • min_stack等价于遍历stack所有元素,把升序的数字都删除掉,留下一个从栈底到栈顶降序的栈
  • 相当于给stack中的降序元素做了标记,每当pop()这些降序元素,min_stack会将相应的栈顶元素pop()出去,保证其栈顶元素始终是stack中的最小元素。

155.gif

复杂度分析:

  • 时间复杂度 O(1) :压栈、出栈、获取最小值的时间复杂度都为 O(1) 。
  • 空间复杂度 O(N) :包含 N 个元素辅助栈占用线性大小的额外空间。
class MinStack {
     
    Deque<Integer> stack = new LinkedList<Integer>();
    Deque<Integer> minValueStack = new LinkedList<Integer>();
    /** initialize your data structure here. */
    public MinStack() {

    }
    
    public void push(int x) {
        stack.offerLast(x);
        //如果 小于等于 min_stack栈顶值,则一起push()到min_stack,即更新了栈顶最小值;
        if(minValueStack.isEmpty() || x <= minValueStack.peekLast())
            minValueStack.offerLast(x);
    }
    
    public void pop() {
        int num = 0;
        if(!stack.isEmpty()){
            num = stack.peekLast();
            stack.removeLast();
            //将pop()出去的元素值是否是min_stack栈顶元素值(即最小值),如果是则将min_stack栈顶元素一起pop(),这样可以保证min_stack栈顶元素始终是stack中的最小值
            if(!minValueStack.isEmpty() && num == minValueStack.peekLast())
                minValueStack.removeLast();
        }
    }
    
    public int top() {
        return stack.peekLast();
    }
    
    public int getMin() {
        return minValueStack.isEmpty()?Integer.MIN_VALUE:minValueStack.peekLast();
    }
}

/**
 * Your MinStack object will be instantiated and called as such:
 * MinStack obj = new MinStack();
 * obj.push(x);
 * obj.pop();
 * int param_3 = obj.top();
 * int param_4 = obj.getMin();
 */

题目三:239. 滑动窗口最大值

问题描述

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 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

算法思路

这个题目,算是很经典的题目,我们的滑动窗口主要分为两种,一种的可变长度的滑动窗口,一种是固定长度的滑动窗口,这个题目算是固定长度的代表。今天我们用双端队列来解决我们这个题目,学会了这个题目的解题思想你可以去解决一下两道题目 剑指 Offer 59 - II. 队列的最大值155. 最小栈,虽然这两个题目和该题类型不同,但是解题思路是一致的,都是很不错的题目,我认为做题,那些考察的很细的,解题思路很难想,即使想到,也不容易完全写出来的题目,才是能够大大提高我们编码能力的题目,希望能和大家一起进步。这个题目我们用到了双端队列,队列里面保存的则为每段滑动窗口的最大值。我们先来了解下双端队列吧,队列我们都知道,是先进先出,双端队列呢?既可以从队头出队,也可以从队尾出队,则不用遵循先进先出的规则。下面我们通过一个动画来了解一下吧。

 

好啦,我们了解双端队列是什么东东了,下面我们再通过一个动画,来看一下代码的执行过程吧,相信各位一下就能够理解啦。我们就通过题目中的例子来表述。nums = [1,3,-1,-3,5,3,6,7], k = 3

 

不知道通过上面的例子能不能给各位描述清楚,如果不能的话,我再加把劲,各位看官,请接着往下看。我们将执行过程进行拆解:

  1. 想将我们第一个窗口的所有值存入单调双端队列中,单调队列里面的值为单调递减的。如果发现队尾元素小于要加入的元素,则将队尾元素出队,直到队尾元素大于新元素时,再让新元素入队,目的就是维护一个单调递减的队列。
  2. 我们将第一个窗口的所有值,按照单调队列的规则入队之后,因为队列为单调递减,所以队头元素必为当前窗口的最大值,则将队头元素添加到数组中。
  3. 移动窗口,判断当前窗口前的元素是否和队头元素相等,如果相等则出队。
  4. 继续然后按照规则进行入队,维护单调递减队列。
  5. 每次将队头元素存到返回数组里。
  6. 返回数组

是不是懂啦,再回去看一遍视频吧。祝大家新年快乐,天天开心呀!

上段文字来源:https://leetcode-cn.com/problems/sliding-window-maximum/solution/zhe-hui-yi-miao-dong-bu-liao-liao-de-hua-7fy5/

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int[] res = new int[nums.length-k+1];
        Deque<Integer> dq = new LinkedList<Integer>();
        //先将第一个窗口的元素按单调递减规则存如双端队列中;
        for(int i=0; i<k; i++){
            while(!dq.isEmpty() && nums[i]>dq.peekLast())
                dq.removeLast();
            dq.offerLast(nums[i]);
        }
        res[0]=dq.peekFirst();

        //移动窗口
        for(int i=1; i< nums.length-k+1; i++){
            //双端队列的头元素已经不在当前窗口中
            if(dq.peekFirst()==nums[i-1])
                dq.removeFirst();
            //维护单调递减队列
            while(!dq.isEmpty() && nums[i+k-1]>dq.peekLast())
                dq.removeLast();
            dq.offerLast(nums[i+k-1]);
            res[i]=dq.peekFirst();
        }
        return res;
    }
}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值