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

本文介绍如何利用单调递减队列和大根堆实现队列最大值(max_value)和滑动窗口最大值(maxSlidingWindow),在平均情况下保持O(1)的时间复杂度。通过单调递减队列策略,高效地维护窗口内的最大元素,简化队列操作,适用于面试题剑指Offer59。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

剑指 Offer 59 - II. 队列的最大值

1、题目

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

若队列为空,pop_front 和 max_value 需要返回 -1
在这里插入图片描述

2、题解

这题需要用到单调递减队列

如何更快的找到队列中的最大值?
    队列中没必要维护窗口中的所有元素,我们可以在队列中只保留那些可能成为窗口中的最大元素,去掉那些不可能成为窗口中的最大元素。
    如果新进来的数字大于滑动窗口的末尾元素,那么末尾元素就不可能再成为窗口中最大的元素了,因为这个大的数字是后进来的,一定会比之前先进入窗口的小的数字要晚离开窗口,因此我们就可以将滑动窗口中比其小的数字弹出队列,于是队列中的元素就会维持从队头到队尾单调递减,这就是单调递减队列

为了实现此递减列表,需要使用 双向队列 ,假设队列已经有若干元素:

  1. 当执行入队 push_back() 时: 若入队一个比队列某些元素更大的数字 x ,则为了保持此列表递减,需要将双向队列 尾部所有小于 x 的元素 弹出。
  2. 当执行出队 pop_front() 时: 若出队的元素是最大元素,则 双向队列 需要同时 将首元素出队 ,以保持队列和双向队列的元素一致性。

3、代码

class MaxQueue {
    Deque<Integer> queue;
    Deque<Integer> help;
    public MaxQueue() {
        queue = new ArrayDeque<>();
        help = new ArrayDeque<>();
    }
    
    public int max_value() {
        return help.isEmpty() ? -1 : help.peek();
    }
    
    public void push_back(int value) {
        queue.offer(value);
        while(!help.isEmpty() && help.peekLast() < value){
            help.pollLast();
        }
        help.offerLast(value);
    }
    
    public int pop_front() {
        if(queue.isEmpty()) return -1;
        int val = queue.poll();
        if(help.peek() == val){
            help.poll();
        }
        return val;
    }
}

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

1、题目

给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。
在这里插入图片描述

2、题解

解法一、大根堆

维护一个大小为窗口大小的大顶堆,顶堆元素则为当前窗口的最大值。
假设窗口的大小为 M,数组的长度为 N。在窗口向右移动时,需要先在堆中删除离开窗口的元素,并将新到达的元素添加到堆中.

解法二:单调递减队列
    首先,我们可以想到最朴素的做法是模拟滑动窗口的过程,每向右滑动一次都遍历一遍滑动窗口,找到最大的元素输出,这样的时间复杂度是O(nk)O(nk)。考虑优化,其实滑动窗口类似于数据结构双端队列,窗口向右滑动过程相当于向队尾添加新的元素,同时再把队首元素删除。

如何更快的找到队列中的最大值?
    队列中没必要维护窗口中的所有元素,我们可以在队列中只保留那些可能成为窗口中的最大元素,去掉那些不可能成为窗口中的最大元素。
    如果新进来的数字大于滑动窗口的末尾元素,那么末尾元素就不可能再成为窗口中最大的元素了,因为这个大的数字是后进来的,一定会比之前先进入窗口的小的数字要晚离开窗口,因此我们就可以将滑动窗口中比其小的数字弹出队列,于是队列中的元素就会维持从队头到队尾单调递减,这就是单调递减队列

对于队列内的元素来说:

  1. 在队列内自己左边的数就是数组中左边第一个比自己大的元素。
  2. 当被弹出时,遇到的就是数组中右边第一个比自己大的元素 ,只要元素还在队列中,就意味着暂时还没有数组中找到自己右侧比自己大的元素。
  3. 队头到队尾单调递减,队首元素为队列最大值。

算法流程:

  1. 初始化: 双端队列 queue,结果列表 res ,数组长度 len ;
  2. 滑动窗口: 左边界范围 i ∈ [1 − k,len − k] ,右边界范围 j ∈ [0, len − 1] ;
    • 队首元素 deque[0] == 被删除元素 nums[i - 1]:则队首元素出队;
    • 删除 queue 内所有 < nums[j] 的元素,以保持 queue 递减;
    • 将 nums[j] 添加至 queue 尾部;
    • 若已形成窗口:将窗口最大值(即队首元素 deque[0])添加至列表 res ;
  3. 返回值: 返回结果列表 res ;

3、代码

解法一:大根堆

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int[] res = new int[nums.length - k + 1];
        if(k > nums.length || k < 1 || nums.length == 0){
            return new int[0];
        }
        PriorityQueue<Integer> heap = new PriorityQueue<>((o1, o2) -> o2 - o1);
        for(int i = 0;i < k;i++){
            heap.add(nums[i]);
        }
        res[0] = heap.peek();
        for(int i = 0, j = i + k;j < nums.length;i++,j++) {//维护一个大小为 k 的堆
            heap.remove(nums[i]);//删除堆顶元素
            heap.add(nums[j]);//将新到达的元素添加到堆中
            res[i + 1] = heap.peek();
        }
        return res;
    }
}

解法二:单调递减队列

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int len = nums.length;
        Deque<Integer> queue = new ArrayDeque<>(); //双端队列
        if(k > nums.length || k < 1 || nums.length == 0){
            return new int[0];
        }
        int[] res = new int[len - k + 1];
        int index = 0;//数组下标
        //未形成窗口
        for(int i = 0;i < k;i++){
            //一直循环删除到队列中的值都大于当前值,或者删到队列为空
            while(!queue.isEmpty() && queue.peekLast() < nums[i]){
                queue.removeLast();
            }
            //执行完上面的循环后,队列中要么为空,要么值都比当前值大,然后就把当前值添加到队列中
            queue.addLast(nums[i]);
        }

        res[index++] = queue.peekFirst();

        //形成窗口后
        for(int i = k;i < len;i++){
            //i-k是已经在区间 k 外了,如果首位等于nums[i-k],那么说明此时首位值已经不再区间内了,需要删除
            if(queue.peekFirst() == nums[i - k]){
                queue.removeFirst();
            }
            while(!queue.isEmpty() && queue.peekLast() < nums[i]){
                queue.removeLast();
            }
            queue.addLast(nums[i]);
            //把队列的首位值添加到arr数组中
            res[index++] = queue.peekFirst();
        }
        return res;

    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值