单调队列解决滑动窗口问题

news2024/11/19 23:33:39

文章目录

  • 单调队列结构解决滑动窗口问题
    • 什么是单调队列?
    • [239. 滑动窗口最大值](https://leetcode.cn/problems/sliding-window-maximum/)
      • 单调队列框架
      • 滑动窗口解题框架
      • 完整的解题代码如下:
      • 我的实现:

单调队列结构解决滑动窗口问题

什么是单调队列?

单调队列其实就是一个队列,只是使用了一点巧妙的方法使得队列中的元素全都是单调递增(或单调递减)的

单调队列主要解决以下问题:

给你一个数组 window,已知其最值为 A,如果给 window 中添加一个数 B,那么比较一下 AB 就可以立即算出新的最值;但如果要从 window 数组中减少一个数,就不能直接得到最值了,因为如果减少的这个数恰好是 A,就需要遍历 window 中的所有元素重新寻找新的最值。

针对这个问题我们可以用优先级队列解决吗?

我们都知道优先级队列是一个特殊的队列,专门用来动态寻找最值得

但是如果只是淡出维护最值得话,优先级队列很专业,队头元素就是最值。

不过,优先级队列无法满足标准队列结构先进先出的时间顺序,因为优先级队列底层利用二叉堆进行动态排序,元素的出对顺序是元素的大小顺序和入队的先后顺序完全没有关系

因此我们用单调队列这种新的队列结构,既能维护队列元素先进先出的时间顺序又能够正确维护队列中所有元素的最值

总结:单调队列这个数据结构主要用来辅助解决滑动窗口相关的问题的。之前有关滑动窗口的问题我们使用的是双指针技巧但是有些稍微复杂的滑动窗口问题是不能只靠两个指针来解决的,需要更先进的数据结构

比如说每当窗口扩大和窗口缩小时,单凭移除和移入窗口的元素即可决定是否更新答案,但是当要从窗口中减少一个元素,我们无法单凭溢出的那个元素更新窗口的最值,除非重新遍历所有元素,但是这样就会增加时间复杂度

239. 滑动窗口最大值

对于这个问题我们可以利用单调队列结构用O(1)时间算出每个滑动窗口中的最大值使得整个算法在线性时间完成

单调队列框架

首先普通的队列的框架一般是这样的

class Queue {
    // enqueue 操作,在队尾加入元素 n
    void push(int n);
    // dequeue 操作,删除队头元素
    void pop();
}

而单调队列的框架也是差不多的

class Queue {
    // enqueue 操作,在队尾加入元素 n
    void push(int n);
    // dequeue 操作,删除队头元素
    void pop();
}

虽然是差不多但是实际的实现跟普通队列还是不一样的

观察滑动窗口的过程很容易发现,实现单调队列必须使用一种数据结构支持在头部和尾部进行插入和删除,而双链表满足这个条件

单调队列的核心思想和单调栈类似,push方法依然在队尾添加元素,但是要把前面比自己小的元素都删除掉

class MonotonicQueue {
// 双链表,支持头部和尾部增删元素
// 维护其中的元素自尾部到头部单调递增
private LinkedList<Integer> maxq = new LinkedList<>();

// 在尾部添加一个元素 n,维护 maxq 的单调性质
public void push(int n) {
    // 将前面小于自己的元素都删除
    while (!maxq.isEmpty() && maxq.getLast() < n) {
        maxq.pollLast();
    }
    maxq.addLast(n);
}

我们可以这样理解:

加入数字的大小相当于人的体重,把前面体重不足的都压扁了,知道遇到更大的两集才停住

在这里插入图片描述

如果每个元素被加入时都是这样的操作,最终单调队列中的元素大小就会保持一个单调递减的顺序,所以max方法可以如下:

class MonotonicQueue {
    // 为了节约篇幅,省略上文给出的代码部分...

    public int max() {
        // 队头的元素肯定是最大的
        return maxq.getFirst();
    }
}

pop方法在队头删除元素n也很好写,但是我们要判断 data.getFirst() == n,这是因为我们想删除的队头元素n可能已经被压扁了,即可能不存在了,所以这时候就不用删除

class MonotonicQueue {
    // 为了节约篇幅,省略上文给出的代码部分...

    public void pop(int n) {
        if (n == maxq.getFirst()) {
            maxq.pollFirst();
        }
    }
}

在这里插入图片描述

滑动窗口解题框架

int[] maxSlidingWindow(int[] nums, int k) {
    MonotonicQueue window = new MonotonicQueue();
    List<Integer> res = new ArrayList<>();
    
    for (int i = 0; i < nums.length; i++) {
        if (i < k - 1) {
            //先把窗口的前 k - 1 填满
            window.push(nums[i]);
        } else {
            // 窗口开始向前滑动
            // 移入新元素
            window.push(nums[i]);
            // 将当前窗口中的最大元素记入结果
            res.add(window.max());
            // 移出最后的元素
            window.pop(nums[i - k + 1]);
        }
    }
    // 将 List 类型转化成 int[] 数组作为返回值
    int[] arr = new int[res.size()];
    for (int i = 0; i < res.size(); i++) {
        arr[i] = res.get(i);
    }
    return arr;
}

在这里插入图片描述

完整的解题代码如下:

/* 单调队列的实现 */
class MonotonicQueue {
    LinkedList<Integer> maxq = new LinkedList<>();
    public void push(int n) {
        // 将小于 n 的元素全部删除
        while (!maxq.isEmpty() && maxq.getLast() < n) {
            maxq.pollLast();
        }
        // 然后将 n 加入尾部
        maxq.addLast(n);
    }
    
    public int max() {
        return maxq.getFirst();
    }
    
    public void pop(int n) {
        if (n == maxq.getFirst()) {
            maxq.pollFirst();
        }
    }
}

/* 解题函数的实现 */
int[] maxSlidingWindow(int[] nums, int k) {
    MonotonicQueue window = new MonotonicQueue();
    List<Integer> res = new ArrayList<>();
    
    for (int i = 0; i < nums.length; i++) {
        if (i < k - 1) {
            //先填满窗口的前 k - 1
            window.push(nums[i]);
        } else {
            // 窗口向前滑动,加入新数字
            window.push(nums[i]);
            // 记录当前窗口的最大值
            res.add(window.max());
            // 移出旧数字
            window.pop(nums[i - k + 1]);
        }
    }
    // 需要转成 int[] 数组再返回
    int[] arr = new int[res.size()];
    for (int i = 0; i < res.size(); i++) {
        arr[i] = res.get(i);
    }
    return arr;
}

细节问题的解决:

在实现单调队列的时候,我们使用了Java的LinkedList,因为链表结构支持在头部和尾部快速增删元素,而在解法代码中的res则使用的ArrayList结构,因为后序会按照索引取元素,所以数组结构更合适

时间复杂度问题的解决:

在push操作中有while循环,时间复杂度还是O(1)吗?

其实单独看push操作的复杂度确实不是O(1),但是算法啊整体的复杂度依然是O(N)线性时间。因为nums中的每个元素最多被push和pop一次,没有任何多余操作,所以整体的时间复杂度还是O(N)。

空间复杂度是多少

空间复杂度就是窗口的大小O(K)

其他问题和思考(也就是单调队列的通用实现)

  1. 本次单调队列类只实现了max方法,你是否能够再额外添加一个min方法,在O(1)的时间返回队列中所有元素的最小值?
  2. 本次单调队列类的pop方法还需要接收一个参数,这显然有悖于标准队列的做法,如何修复这个缺陷?
  3. 如何实现单调队列类中的size方法,返回单调队列中元素的个数(注意,由于每次push方法都可能从底层的q列表中删除元素,所以q中的元素个数并不是单调队列的元素个数)

我的实现:

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        //实现单调链表
        MonotonicQueue mq=new MonotonicQueue();
        List<Integer> res=new ArrayList<>();
        for(int i=0;i<nums.length;i++){
            if(i<k-1){
                //先填满窗口
                mq.push(nums[i]);
            }else{
                //插入新的元素
                mq.push(nums[i]);
                //在结构列表中加入最大值
                res.add(mq.max());
                //删除旧的元素
                mq.pop(nums[i-k+1]);
            }
        }
        ///将结果转成int数组
        int[] result=new int[res.size()];
        for(int j=0;j<res.size();j++){
            result[j]=res.get(j);
        }
        return result;
    }
}

class MonotonicQueue{
    //底层是双链表
    Deque<Integer> maxq=new LinkedList<>();
    //在队尾插入元素,如果队尾前面有小于被插入元素的数要被删除,直到遇到比它大的就不用再删除了
    public void push(int n){
        while(!maxq.isEmpty()&&maxq.getLast()<n){
            maxq.pollLast();
        }
        //到了可以插入的位置,插入需要被插入的元素
        maxq.addLast(n);
    }
    //最大值就是队头元素
    public int max(){
        return maxq.getFirst();
    }
    //在对头删除元素,如果队头元素不是n的话就不需要删除了,因为可能已经被删除掉了
    public void pop(int n){
        if(n==maxq.getFirst()){
            maxq.pollFirst();
        }
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/502430.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CVE-2023-27524 Apache Superset Auth Bypass|附检测工具

漏洞描述 Apache Superset是一个开源数据可视化和探索工具。Apache Superset 版本&#xff08;包括 2.0.1&#xff09;中的会话验证攻击。没有根据安装说明更改默认配置的SECRET_KEY的安装允许攻击者验证和访问未经授权的资源。这不会影响更改了SECRET_KEY配置默认值的Superse…

JAVA快速开发框架 一键生成表单模板代码

从计算机诞生开始&#xff0c;虽然编程的形式随着硬件及软件的不断进步而不停迭代&#xff0c;但是从事计算机技术行业的人员始终与编写代码的任务紧密联系在一起。因此如何提高软件开发的效率和质量&#xff0c;一直是软件工程领域的重要问题之一。 这一方面是由于在不同软件…

MQ(面试问题简析)学习笔记

文章目录 1. 为什么使用消息队列2. 消息队列有什么优缺点3. Kafka、ActiveMQ、RabbitMQ、RocketMQ 有什么优缺点&#xff1f;4. 如何保证消息队列的高可用4.1 RabbitMQ 的高可用性4.2 Kafka 的高可用性 5. 如何保证消息不被重复消费&#xff08;如何保证消息消费的幂等性&#…

1、Cloudsim和Workflowsim仿真环境下载

1、WorkflowSim的下载和安装 workflowsim下载地址 2、Cloudsim的下载和安装 cloudsim官网 cloudsim4.0安装包地址 2、Cloudsim如何工作 Cloudsim如何工作&#xff1f;原版内容 cloudsim配置 下面这是CloudsimExamples1的代码&#xff1a; package org.cloudbus.…

论文导读 | 大语言模型上的精调策略

随着预训练语言模型规模的快速增长&#xff0c;在下游任务上精调模型的成本也随之快速增加。这种成本主要体现在两方面上&#xff1a;一&#xff0c;计算开销。以大语言模型作为基座&#xff0c;精调的显存占用和时间成本都成倍增加。随着模型规模扩大到10B以上&#xff0c;几乎…

SpringBoot启用web模拟测试(一)

添加依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.5.10</version> </dependency> 模拟端口 虚拟请求测试 Slf4j RestController RequestMappin…

java后端面试大全,java后端面试宝典

JAVA_LEARNING_CONTENT JAVA后端面试大全&#xff0c;java后端面试宝典 一个分布式锁的解决方案&#xff0c;另一个是分布式事务的解决方案 -2 flink 链接&#xff1a;flink参考文章 -1 linux of view 参考链接&#xff1a; linux常见面试题 linux查看占用cup最高的10个进…

机电设备故障ar远程维修软件缩短生产线中断时间

电机属于工业生产中的关键设备之一&#xff0c;处于长期运转阶段&#xff0c;因此电机容易出现故障&#xff0c;极易增加企业生产成本&#xff0c;影响生产计划。引进AR远程维修技术效果显著。 AR远程维修技术是一种将虚拟信息与实际场景相结合的技术。当电机出现故障时&#x…

基于AT89C51单片机的交通灯设计与仿真

点击链接获取Keil源码与Project Backups仿真图&#xff1a; https://download.csdn.net/download/qq_64505944/87763760?spm1001.2014.3001.5503 源码获取 主要内容&#xff1a; 设计一个能够控制十二盏交通信号灯的模拟系统,:利用单片机的定时器定时&#xff0c;令十字路口…

云原生-kubesphere容器平台

https://www.yuque.com/leifengyang/oncloud/gz1sls 多租户&#xff1a;可以用户自定义注册进来&#xff0c;可以给用户分配一些集群操作权限&#xff0c;来操作集群 多集群&#xff1a;有生产环境的k8s集群和测试环境的k8s集群。这多个集群都是需要管理的&#xff0c;可以安装…

Error: (‘IM002‘, ‘[IM002] [Microsoft][ODBC 驱动程序管理器] 未发现数据源名称并且未指定默认驱动程序‘)

这是使用pypyodbc访问access数据库时常见的一个错误。 大致可以分为以下几个原因&#xff1a; 1.驱动程序不全&#xff1b; 2.你的驱动源名称错误&#xff1b; 3.python位数与驱动位数不同&#xff0c;这也可以粗暴的归类为原因1. 那么如何解决&#xff1f; 找到对应的驱…

ce人造指针

人造指针 找出什么访问 重开会改变edi 记录传入的edi 视图 内存区域ctrlr 可读可写 在0079B000前找空的 可以用只读 fullaccess(00xxxxxx,4)有可能处于保护&#xff0c;不起效果 在申请地址造指针 全局用 registersymbol() unregistersymbol(mana_ecx) ceaa od …

谷歌云 | 授权用户访问您在 Cloud Run 上的私有工作负载的 3 种新方法

【本文由 Cloud Ace 云一整理】 越来越多的组织正在Cloud Run上构建应用程序&#xff0c;这是一个完全托管的计算平台&#xff0c;可让您在 Google 的基础架构之上运行容器化应用程序。想想 Web 应用程序、实时仪表板、API、微服务、批量数据处理、测试和监控工具、数据科学推…

【ADS867x】双极输入范围 14 位 500kSPS 4/8 通道、单电源 SAR ADC

器件特性 具有集成模拟前端的 14 位模数转换器 (ADC)具有自动和手动扫描功能的 4 通道、8 通道多路复用器通道独立可编程输入&#xff1a; 10.24V、5.12V、2.56V、1.28V、0.64V10.24V、5.12V、2.56V、1.28V 5V 模拟电源&#xff1a;1.65V 到 5V I/O 电源恒定的阻性输入阻抗&am…

Android Dialog之DialogFragment详解与使用

一、介绍 在Android开发过程中&#xff0c;经常会有弹窗业务&#xff0c;在正常的弹窗业务中&#xff0c;常用到的是Dialog&#xff0c;Dialog的原理也是通过将view&#xff0c;添加到Dialog中。Dialog自身是一个独立的窗口&#xff0c;和Activity一样&#xff0c;有自己的wind…

C++面试题

试题一 请问如下函数调用了什么构造函数和析构函数&#xff0c;以及调用的顺序是什么&#xff1f; 如果 fun函数里写成 return s1 s2 有什么变化&#xff1f; string fun(strings1, string s2) {string tmp s1 s2;return tmp; } int main() {string s fun(s1, s2);retur…

AI来势汹汹,这份「生存计划」请查收!

AIGC即人工智能生产内容&#xff0c;最近可太火了&#xff0c;但是火了这么久&#xff0c;有些人都没明白到底为什么火&#xff1f;甚至不明所以觉得“AI替代XX”&#xff0c;小编认为没必要焦虑&#xff0c;一起来看一下吧。 AI工具们一日千张图、3小时写一本书、2分钟构建一个…

论文导读 | 大语言模型中应用到的强化学习算法

摘要 在最近取得广泛关注的大规模语言模型&#xff08;LLM&#xff09;应用强化学习&#xff08;RL&#xff09;进行与人类行为的对齐&#xff0c;进而可以充分理解和回答人的指令&#xff0c;这一结果展现了强化学习在大规模NLP的丰富应用前景。本文介绍了LLM中应用到的RL技术…

《NFT区块链进阶指南一》Remix部署Solidity ERC721合约(NFT合约)到Etherscan

文章目录 一、部署合约1.1 无构造参数合约部署1.2 有构造参数合约部署 二、合约详情三、部署提示 本篇为NFT区块链高级部分&#xff0c;在阅读之前需了解&#xff1a;Remix、Metamask、Etherscan、Solidity、Openzeppelin、ERC721合约 一、部署合约 1.1 无构造参数合约部署 智…

maven入门学习

简介 maven是基于ant升级的&#xff0c;apache的自动化构建工具、项目管理工具 Maven – Welcome to Apache Maven maven使用pom.xml进行配置 maven项目可以更方便的实现导jar包、拆分项目 idea默认集成了maven 下载安装 下载maven&#xff0c;在官网&#xff08;Maven –…