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

news2024/11/30 18:51:54

文章目录

  • 一、单调栈
    • 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)

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

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

相关文章

C++项目——集群聊天服务器项目(八)用户登录业务

在第&#xff08;7&#xff09;节中&#xff0c;已经实现用户注册模块&#xff0c;本节来实现用户登录模块 项目流程 1、项目环境搭建 C项目——集群聊天服务器项目(一)项目介绍、环境搭建、Boost库安装、Muduo库安装、Linux与vscode配置_c集群聊天服务器-CSDN博客 2、Jso…

Linux(5)底层分析-常见问题-经典笔试面试题整理

六、底层分析 1、linux 目录 Linux各路径的解释&#xff1a; /bin存放二进制可执行文件(ls,cat,mkdir等)&#xff0c;常用命令一般都在这里/home存放所有用户文件的根目录&#xff0c;是用户主目录的基点&#xff0c;比如用户user的主目录是/home/user&#xff0c;可以用~us…

iOS - Runtime - Class-方法缓存(cache_t)

文章目录 iOS - Runtime - Class-方法缓存(cache_t)1. 散列表的存取值 iOS - Runtime - Class-方法缓存(cache_t) Class内部结构中有个方法缓存&#xff08;cache_t&#xff09;&#xff0c;用散列表&#xff08;哈希表&#xff09;来缓存曾经调用过的方法&#xff0c;可以提高…

A fatal error occurred: MD5 of file does not match data in flash!问题解决

采用的芯片是ESP32-S3-WROOM&#xff0c;16MB FLASH 开发环境是Arduino&#xff0c;烧录到100%后直接报错。 以为是Arduino的问题&#xff0c;用esp-idf开发的程序&#xff0c; 烧录的过程中&#xff0c;也是直接报错如下&#xff1a; esptool.py v4.7.0 Serial port /dev/…

什么是缓冲区溢出攻击?

缓冲区是内存存储区域&#xff0c;在数据从一个位置传输到另一个位置时临时保存数据。当数据量超过内存缓冲区的存储容量时&#xff0c;就会发生缓冲区溢出&#xff08;或buffer overrun&#xff09;。结果&#xff0c;试图将数据写入缓冲区的程序会覆盖相邻的内存位置。 例如…

java组合总和2(力扣Leetcode40)

组合总和 II 力扣原题链接 问题描述 给定一个候选人编号的集合 candidates 和一个目标数 target&#xff0c;找出 candidates 中所有可以使数字和为 target 的组合。 每个数字在每个组合中只能使用一次。 注意&#xff1a;解集不能包含重复的组合。 示例 示例 1: 输入:…

【倪琴神品品鉴】全新倪诗韵神品级古琴

倪琴朱砂神品仲尼&#xff0c;仅此放漏一张&#xff1b;龙池侧签海门倪诗韵制&#xff0c;雁足上方刻“雷音琴坊”方章&#xff0c;凤沼下方有随形章“神品”二字&#xff1b;老木材纹理竖直&#xff0c;共振良好&#xff0c;是难得的佳器&#xff1b;附带倪老师亲笔签名收藏证…

【机器学习之---数学】随机游走

every blog every motto: You can do more than you think. https://blog.csdn.net/weixin_39190382?typeblog 0. 前言 随机游走 1. 概念 1.1 例1 在你的饮食俱乐部度过了一个富有成效的晚上后&#xff0c;你在不太清醒的状态下离开了。因此&#xff0c;你会醉醺醺地在展…

JUC:wait/notify用法并写一个简单消息队列(生产者消费者问题)

文章目录 wait/notifyapiwait vs sleep手写消息队列 wait/notify 这块比较简单&#xff0c;就不在把所有例子都写上了。 要注意区分waitSet和EntryList中的线程&#xff0c;一个获得了锁但是wait释放了锁进入等待notify唤醒状态&#xff0c;一个是正在等待获得锁。 不过相同点…

算法之并查集

并查集&#xff08;Union-find Data Structure&#xff09;是一种树型的数据结构。它的特点是由子结点找到父亲结点&#xff0c;用于处理一些不交集&#xff08;Disjoint Sets&#xff09;的合并及查询问题。 Find&#xff1a;确定元素属于哪一个子集。它可以被用来确定两个元…

wps斜线表头并分别打字教程

wps斜线表头怎么做并分别打字&#xff1a; 1、首先选中我们想要设置的表头。 2、接着右键选中它&#xff0c;点击“设置单元格格式” 3、然后点击上方“边框”选项卡。 4、随后选择图示的斜线&#xff0c;点击“确定” 5、设置完成后&#xff0c;我们只要在其中打字就可以在斜…

vue-v-for遍历index与id

一.遍历列表key的作用&#xff08;index作为key&#xff09; 虚拟DOM上有key,是虚拟的&#xff0c;但是真实DOM上没有&#xff0c;key是Vue内部的 当使用index作为key的时候&#xff0c;Vue会根据初识数据生成一个初始的虚DOM&#xff0c; 然后在页面上映射出真实DOM 如果向数据…

vue 窗口内容滚动到底部

onMounted(() > {scrollToBottom() }) // 滚动到底部方法 const scrollToBottom () > {// 获取聊天窗口容器let chatRoom: any document.querySelector(".chat-content");// 滚动到容器底部chatRoom.scrollTop chatRoom.scrollHeight; } 效果 聊天窗口代码…

HashMap关键源码带读

文章目录 目录 文章目录 前言 1 . 成员变量 灵魂五问 第一问: 默认初始化容量为啥是16? 第二问: 最大容量为什么必须是2的幂? 第三问: 链表转红黑树的阈值为什么是8? 第四问: 红黑树转链表的阈值为什么是6? 第五问: 默认加载因子为什么是0.75? 2. 成员方法 eq…

Discuz采集发布插件

Discuz&#xff08;简称DZ&#xff09;是一款知名的开源论坛系统&#xff0c;广泛应用于各类网站社区。对于许多站长来说&#xff0c;保持论坛内容的更新是一项挑战&#xff0c;特别是在内容量庞大的情况下。为了解决这个问题&#xff0c;有一类特殊的插件是用于在Discuz论坛中…

《QT实用小工具·四》屏幕拾色器

1、概述 源码放在文章末尾 该项目实现了屏幕拾色器的功能&#xff0c;可以根据鼠标指定的位置识别当前位置的颜色 项目功能包含&#xff1a; 鼠标按下实时采集鼠标处的颜色。 实时显示颜色值。 支持16进制格式和rgb格式。 实时显示预览颜色。 根据背景色自动计算合适的前景色…

国内好用的chatGPT和AI绘图工具

分享一个比较好用的AI 分享一个比较好用的AI&#xff0c;只是需要开通会员&#xff0c;目前官网的价格是&#xff1a;298&#xff0c;开通之后可以使用chatgpt4、AI绘画、图片融合等等&#xff01;不开通的话是可以免费使用15次的&#xff0c;下面是一些介绍图片&#xff01;链…

安全团队需要重点演练的四大威胁

文章目录 前言一、勒索软件攻击二、第三方风险三、内部威胁四、分布式拒绝服务攻击(DDoS)前言 桌面演练(推演)是一种重要的安全演习形式,参演人员利用演练方案、流程图、计算机模拟、视频会议等辅助手段,针对事先假定的演练情景,讨论和推演应急决策及现场处置,从而促使相…

点点数据K参数加密逆向分析(RPC方案跟加密算法还原)

文章目录 1. 写在前面2. 接口分析3. 断点分析4. RPC调用5. 算法还原 【&#x1f3e0;作者主页】&#xff1a;吴秋霖 【&#x1f4bc;作者介绍】&#xff1a;擅长爬虫与JS加密逆向分析&#xff01;Python领域优质创作者、CSDN博客专家、阿里云博客专家、华为云享专家。一路走来长…

蓝桥杯物联网竞赛_STM32L071_13_定时器

CubeMx配置LPTIM: counts internal clock events 计数内部时钟事件 prescaler 预分频器 updata end of period 更新期末 kil5配置&#xff1a; 中断回调函数完善一下&#xff1a; void HAL_LPTIM_AutoReloadMatchCallback(LPTIM_HandleTypeDef *hlptim){if(cnt ! 10) cnt…