数据结构:单调栈和单调队列

文章目录

  • 一、单调栈
    • 1.1、栈的思想
    • 1.2、单调栈
      • 1.2.1、单调栈的基本应用:找出数组中每个元素右侧第一个更大的元素
      • 1.2.2、单调栈的基本应用:找出数组中每个元素左侧第一个更大的元素
      • 1.2.3、单调栈拓展
      • 1.2.4、单调栈LeetCode题单
  • 二、单调队列
    • 2.1、队列的思想
    • 2.2、单调队列
      • 单调队列的应用:滑动窗口最大值
  • 三、单调栈和单调队列的区别
    • 示例解释

在学习单调队列或单调栈时,我们要先清楚,为何栈或队列是保持单增或单减,并且这样为何是有效的。比如保持单增,用单调队列的思想考虑的情况下,在遍历的过程中,我们需要解决的问题是寻找第一个比它小的(或者维护窗口中最小的元素),当前元素进队/栈时,如果栈顶或队尾存在比当前元素大的元素时,这些元素都是冗余的,因为当前元素在往后考虑时的作用 会一定更接近往后的元素且更小(更满足我们需要第一个小的要求)并且在单调队列中也更会留在窗口中。(单调栈有不同实现方式和思想,这里只描述了一种,详情请往下看)

一、单调栈

1.1、栈的思想

  栈是一种非常直观且广泛应用的数据结构,其主要特点是后进先出(LIFO,Last In, First Out)。想象一下一摞盘子或书籍,你只能从顶部添加或移除它们。栈可以临时存放一些数据,以便于之后逆序访问它,比如进制转换。
  浏览器的前后进是个很形象的例子:浏览器允许用户后退和前进浏览过的网页。这可以通过两个栈来实现:一个栈用于后退,另一个用于前进。当你访问新页面时,前进栈清空,当前页面压入后退栈。当你点击后退时,从后退栈中弹出,并将其压入前进栈。前进按钮则相反。

1.2、单调栈

  单调栈是一种特殊的栈,其元素按照单调递增或单调递减的顺序排列(根据特殊需求也可以是非减或非增序列)。单调栈用于解决那些需要寻找每个元素左侧或右侧第一个比它大(或小)的元素的问题。当新的元素被尝试加入栈时,会从栈顶开始移除破坏单调性的元素,直到保持栈的单调性为止,然后将新元素入栈。
应用示例:在一个数组中,为每个元素找出其右侧或左侧第一个更大的元素。LeetCode:柱状图中最大的矩形

如果要求的是左侧或右侧的最大/小值(而不是第一个更大/小的),可以用动态规划求解,如LeetCode:接雨水

1.2.1、单调栈的基本应用:找出数组中每个元素右侧第一个更大的元素

  使用单调栈解决这个问题的基本思路是遍历数组,对于每个元素,我们想找到它右侧第一个更大的元素。单调栈可以帮助我们追踪已经遍历过的元素,并保持它们的顺序,以便快速找到每个元素的答案。

  • 初始化一个空栈,用于存放数组元素的索引。
  • 遍历数组中的每个元素:
    • 当栈不为空且当前元素大于栈顶索引对应的元素时,表示找到了栈顶元素右侧的第一个更大元素。此时,将栈顶元素出栈,并记录当前元素为栈顶元素右侧第一个更大的元素。
    • 将当前元素的索引入栈。
  • 对于栈中剩余的元素,它们右侧没有更大的元素。
#include <vector>
#include <stack>
using namespace std;

class Solution {
public:
    vector<int> nextGreaterElement(vector<int>& nums) {
        int n = nums.size();
        vector<int> ans(n, -1); // 初始化结果数组,假设每个元素的右侧没有更大的元素
        stack<int> myStack; // 用于存储索引,栈顶到栈底单调递减

        for (int i = 0; i < n; ++i) {
            // 当前元素大于栈顶元素对应的值时,说明找到了一个更大的元素
            while (!myStack.empty() && nums[i] > nums[myStack.top()]) {
                ans[myStack.top()] = nums[i]; // 更新栈顶元素的下一个更大元素
                myStack.pop(); // 弹出栈顶元素
            }
            // 将当前元素的索引入栈
            myStack.push(i);
        }
        // 对于栈中剩余的元素,它们的右侧没有更大的元素,ans中已经预设为-1,因此无需再操作

        return ans;
    }
};

1.2.2、单调栈的基本应用:找出数组中每个元素左侧第一个更大的元素

  可以直接使用1.2.1的方法反向扫描,反向扫描的右边实际上是原来的左边。如果在一个问题中同时求这俩,那用反向扫描肯定是最便捷的方式。 也可以直接从左往右扫描,如果栈顶元素比当前元素小则弹栈,直到遇到比当前元素大的则是左侧第一个更大元素。(这和单调队列的弹出队列的方式很像,因为比它小的不仅对以后没用,对当前元素来说也没用。)

#include <vector>
#include <stack>
using namespace std;

class Solution {
public:
    vector<int> leftGreaterElement(vector<int>& nums) {
        int n = nums.size();
        vector<int> ans(n, -1); // 初始化结果数组,假设每个元素的左侧没有更大的元素
        stack<int> myStack; // 用于存储索引,栈顶到栈底单调递减

        for (int i = 0; i < n; ++i) {
            // 当前元素大于栈顶元素对应的值时,说明当前元素是遍历到目前为止的最大元素
            // 这里不需要像找右侧元素那样进行元素的更新,因为我们关心的是左侧元素
            while (!myStack.empty() && nums[i] >= nums[myStack.top()]) {
                myStack.pop(); // 弹出栈顶元素
            }
            // 如果栈不为空,说明找到了当前元素左侧的第一个更大元素
            if (!myStack.empty()) {
                ans[i] = nums[myStack.top()];
            }
            // 将当前元素的索引入栈
            myStack.push(i);
        }

        return ans;
    }
};

1.2.3、单调栈拓展

  单调栈的一次遍历不仅仅只能解决找到第一个更小的问题,它一次遍历就能找到左右两边的信息,不过有一边是等高的,有时候我们可以利用这一个特点来处理问题。这样的拓展使用需要在不同问题中发现,如1.2.4列出的题单。

for (int i = 0; i < n; ++i) {//递减序
     while (!myStack.empty() && nums[i] >= nums[myStack.top()]) {
           //右侧if(nums[i]>nums[myStack.top()]) 则能找到右侧更大,但可能出现相等的情况,可能相等的情况并不影响答案
           //所以需要有这种考虑和想法,以便于后面遇到这样的问题能够思考到,然后利用起来
           myStack.pop(); // 弹出栈顶元素
     }
     if (!myStack.empty()) {
         left_max[i] = nums[myStack.top()];//左边更大一定是正确的
     }
     myStack.push(i);
}

1.2.4、单调栈LeetCode题单

在这里插入图片描述

二、单调队列

2.1、队列的思想

  队列是一种先进先出(First In, First Out,FIFO)的数据结构,其工作原理类似于日常生活中的排队等待。在队列中,元素从一端(通常称为队尾)添加,从另一端(称为队头)进行移除。这种结构确保了元素被处理的顺序正是它们被添加到队列中的顺序,就像人们在商店结账处排队一样:先来的人先得到服务,新来的人排在队伍的末尾。

2.2、单调队列

  单调队列是一种特殊的队列,其元素同样按照单调递增或单调递减的顺序排列。不同于单调栈,单调队列支持在两端进行操作:在队列的一端添加元素,在另一端移除元素。这种结构适用于滑动窗口类的问题,其中窗口在数据序列上滑动,而我们希望快速获取窗口内的最大值或最小值。
应用示例:给定一个数组和一个窗口大小,为每个窗口找出最大值或最小值。

单调队列的应用:滑动窗口最大值

  单调队列解决的是另一个问题:给定一个数组和一个窗口大小,为每个窗口找出最大值。单调队列通过维护一个双端队列(Deque),其中保存可能成为当前窗口最大值的元素索引,确保队列是单调递减的。LeetCode求滑动窗口最大值

  • 初始化一个空的双端队列(Deque)。
  • 遍历数组中的每个元素:
    • 移除队列中所有小于当前元素的索引,因为它们不可能是包含当前元素的窗口的最大值。
    • 检查队头索引是否已经滑出窗口(即队头索引对应的元素不在当前考虑的窗口内),如果是,将其从队头移除。
    • 将当前元素的索引添加到队列尾部。
    • 对于每个窗口,队头索引总是对应该窗口的最大值。且队列里总是会有元素(因为至少当前正在遍历的元素一定在窗口中)。
#include <vector>
#include <deque>
using namespace std;

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        deque<int> myque; // 存储的是nums的索引,保证从大到小排列
        vector<int> ans;
        
        for(int i = 0; i < nums.size(); ++i) {
            // 如果队列不为空且当前元素大于等于队列最后一个元素所对应的值,则弹出队列最后一个元素
            while(!myque.empty() && nums[i] >= nums[myque.back()]) {
                myque.pop_back();
            }
            // 将当前元素索引加入队列
            myque.push_back(i);
            // 确保队列第一个元素始终在当前滑动窗口的范围内
            if(myque.front() <= i - k) {
                myque.pop_front();
            }
            // 当索引达到窗口大小-1时,开始记录结果
            if(i >= k - 1) {
                ans.push_back(nums[myque.front()]);
            }
        }
        return ans;
    }
};

三、单调栈和单调队列的区别

  你会发现单调队列和单调栈的区别在于,是否包含一个滑动窗口,单调队列处理的之前的成员可能会"失效",但是单调栈的成员一直不会失效,因此单调队列有一个“失效”出队的操作。单调栈处理的问题中,一旦元素入栈,它们就保持有效,直到被明确地由一个满足特定条件的后来者替代;而单调队列处理的问题中,元素的有效性不仅受到队列中其他元素的影响,还受到它们是否仍然处于考虑的窗口内的影响。

示例解释

  假设你有一系列人的身高,你需要找到每个人右侧的第一个更高的人(单调栈),或者在一系列长度为k的连续子序列(即窗口)中找到最高的人(单调队列)。

  • 单调栈:当一个新人加入时,如果他比前面的人都高,那么他就成为了前面某些人右侧第一个更高的人。前面比他矮的人都不再重要,因为他们已经找到了比自己高的人。
  • 单调队列:对于每个长度为k的窗口,你想快速知道最高的人。当一个新人加入窗口时,如果他比窗口中的某些人高,那么这些比他矮的人就不可能是该窗口的最高者了。但是,窗口滑动时,最高的人可能会离开窗口,所以你需要记录下一个可能最高的人。

  通过使用单调栈和单调队列,你可以高效地解决这些问题,而不需要对每个元素或每个窗口进行独立的比较。每个元素进栈(队)一次,出栈(队)一次,因此时间复杂度均为O(n)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【chemistry 5】脂类代谢、氨基酸代谢、核酸代谢

&#x1f31e;欢迎来到生物化学的世界 &#x1f308;博客主页&#xff1a;卿云阁 &#x1f48c;欢迎关注&#x1f389;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; &#x1f31f;本文由卿云阁原创&#xff01; &#x1f4c6;首发时间&#xff1a;&#x1f339;2024年3月29日&…

Discourse 用户可以自己修改用户名吗

Discourse 是可以修改用户名的&#xff0c;但用户修改自己的用户名会有时间的限制。 这是因为根据官方的说法就是当用户修改用户名后可能会导致内容的失效等问题。 在默认的安装配置下&#xff0c;用户可以在完成注册后的 3 天自己对用户名进行修改。 3 天以后&#xff0c;用…

【CASS精品教程】CASS11台阶画法大全

文章目录 一、无边台阶二、有边台阶三、圆弧无边台阶四、U型台阶五、曲线U型台阶六、L型台阶一、无边台阶 点击【居民地】→【房屋附属】→【台阶】: 选择【两点边】即可。 两点边的绘制方法是,依次点击四个点,或者点击三个点后空格,注意台阶缺口(有白色线条)为下。 四…

HarmonyOS实战开发-Stage模型下Ability的创建和使用

介绍 本篇Codelab基于Stage模型&#xff0c;对Ability的创建和使用进行讲解。首先在课程中我们将带领大家使用DevEco Studio创建一个Stage模型Ability&#xff0c;并使用UIAbilityContext启动另一个Ability&#xff0c;然后借助Want&#xff0c;在Ability之间传递参数&#xf…

基于SpringBoot的“财务管理系统”的设计与实现(源码+数据库+文档+PPT)

基于SpringBoot的“财务管理系统”的设计与实现&#xff08;源码数据库文档PPT) 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBoot 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 系统总体结构图 系统登录界面图 管理员功能界面图…

Deepspeed、ZeRO、FSDP、ZeRO-Offload、all reduce、reduce-scatter

Transformer为基础的大模型应该如何并行 数据并行。但是如果模型太大放不到一块卡上就没用了。为了解决把参数放到一块卡上的问题&#xff0c;演进出了论文Zero的思想&#xff0c;分为Zero-DP和Zero-R两部分。Zero-DP是解决Data parallel的问题&#xff0c;并行过程中内容不够…

Plecs电力电子仿真专业教程-软件操作

Plecs仿真软件基本操作方法&#xff1a; 从连线中引出线&#xff1a;Ctrl 鼠标左键 设置元件参数&#xff1a;双击元件&#xff0c;进行设置&#xff0c;若要显示参数&#xff0c;则在参数后的方框打勾。 CTRL E ---- 仿真参数设置 Ctrl T -----开始仿真 CtrlF …

如何检查电脑的最近历史记录?这里提供详细步骤

如果你怀疑有人在使用你的计算机,并且你想查看他们在做什么,下面是如何查看是否有访问内容的痕迹。 如何检查我的计算机的最近历史记录 要检查计算机的最近历史记录,应该从web浏览器历史记录开始,然后移动到文件。但是,可以修改或删除浏览器历史记录,也可以隐藏Windows…

Redis 和 Mysql 数据库数据如何保持一致性????

1、前言 我们在实际项目中经常会使用到Redis缓存用来缓解数据库压力&#xff0c;但是当更新数据库时&#xff0c;如何保证缓存及数据库一致性&#xff0c;一般我们采用延时双删策略。 目前系统中常用的做法是一个查询接口&#xff0c;先查询Redis&#xff0c;如果不存在则查询…

Vue——案例01(查询用户)

一、案例实现页面 二、案例实现效果 1. 查询效果 2. 年龄升序 3. 年龄降序 4. 原顺序 三、案例实现思路 1. 定义界面所需标签样式 <div id"app"><h2>查询用户:</h2><input type"text" placeholder"请输入名字"/><b…

RabbitMQ高级笔记

视频链接&#xff1a;【黑马程序员RabbitMQ入门到实战教程】 文章目录 1.发送者的可靠性1.1.生产者重试机制1.2.生产者确认机制1.3.实现生产者确认1.3.1.开启生产者确认1.3.2.定义ReturnCallback1.3.3.定义ConfirmCallback 2.MQ的可靠性2.1.数据持久化2.1.1.交换机持久化2.1.2.…

Python基础之函数

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言1.收集函数2.收集关键字函数3.分配关键字函数4.调用自己编写的模块函数5.匿名函数lambda6.lambda函数和filter函数联用7.lambda函数和map函数联用 前言 1.收集…

SQL109 纠错4(组合查询,order by..)

SELECT cust_name, cust_contact, cust_email FROM Customers WHERE cust_state MI UNION SELECT cust_name, cust_contact, cust_email FROM Customers WHERE cust_state IL ORDER BY cust_name;order by子句&#xff0c;必须位于最后一条select语句之后

社交网络的未来:Facebook如何塑造数字社交的下一章

引言 社交网络已成为我们生活中不可或缺的一部分&#xff0c;而Facebook作为其领军者&#xff0c;一直在塑造着数字社交的未来。本文将深入探讨Facebook在未来如何塑造数字社交的下一章&#xff0c;并对社交网络的发展趋势进行展望和分析。 1. 引领虚拟社交的潮流 Facebook将…

Nuxt v4 即将到来!2024 年 Nuxt 发展方向和想法

在 2023 年里&#xff0c;Nuxt 发生了很多事情。 Sbastien 和 Daniel 分享了他们对 Nuxt 所取得的成就以及下一步的发展方向的看法。 2023 年回顾 - Sbastien 2023 年 1 月&#xff0c;Daniel 提出了 Nuxt&#xff1a;2023 年愿景。我们实现了设定的大部分目标。其中一些缺失…

怎么把学浪的课上传到网盘

如何把学浪的课程上传到百度网盘&#xff0c;这是一个老大难的问题&#xff0c;视频的m3u8的key加密&#xff0c;作为非专业人士不知道如何下载&#xff0c;这里就教大家如何将学浪的课程下载下来&#xff0c;上传到自己网盘&#xff0c;随时随地的观看&#xff0c;或者分享给自…

element plus的el-image图片发布到nginx不显示

问题&#xff1a; <el-image alt""src"/img/month-b.png" class"card-icon"style"width: 89px;height: 89px;right: -7px;top: -5px;"/> 部署到nginx二级路由访问地址是&#xff1a; http://192.168.1.207/divided/# 这时候使用…

学浪的视频怎么导出到手机?

学浪视频如何下载成mp4&#xff0c;这是一个老大难的问题&#xff0c;本文就教大家如何将学浪的视频下载到本地&#xff0c;让大家随时随地观看&#xff0c;想在电脑上观看学浪视频或者想在手机上观看学浪视频都可以 工具我打包在下面的链接里面 链接&#xff1a;https://pan…

代码随想录|Day28|贪心03|1005.K次取反后最大化的数组和、134.加油站、135.分发糖果

1005.K次取反后最大化的数组和 思路&#xff1a; 优先取反 绝对值最大的负数如果没有负数&#xff0c;不断取反 绝对值最小的数&#xff0c;直到次数 K 耗尽 取反最小数有一个优化技巧&#xff1a; 如果 K 为偶数&#xff0c;则取反 K 次后&#xff0c;正负不变。如果 K 为奇数…

iOS - Runloop的运行逻辑

文章目录 iOS - Runloop的运行逻辑1. 苹果官方的Runloop执行图2. Mode里面的东西2.1 Source02.2 Source12.3 Timers2.4 Observers 3. 执行流程3.1 注意点 4. Runloop休眠 iOS - Runloop的运行逻辑 1. 苹果官方的Runloop执行图 2. Mode里面的东西 2.1 Source0 触摸事件处理pe…