【C++STL上】栈和队列模拟实现 容器适配器 力扣经典算法秘籍

【C++STL上】栈和队列模拟实现 容器适配器 力扣经典算法秘籍
封面
🔥个人主页:爱和冰阔乐
📚专栏传送门:《数据结构与算法》C++
🐶学习方向:C++方向学习爱好者
⭐人生格言:得知坦然 ,失之淡然
在这里插入图片描述

🏠博主简介

在这里插入图片描述

文章目录


前言

本文从STL容器适配器视角,深度解析栈与队列的设计本质——以双端队列(deque)为底层容器,实现高效头尾操作。结合最小栈、二叉树层序遍历等经典算法题,实战演示栈的"先进后出"与队列的"先进先出"特性,并揭秘优先级队列的堆实现原理。通过模拟实现代码,希望读完本文可以让大家对栈和队列有更深刻理解

一、栈与队列原型简介

1.1 Stack

stack官方文档链接:https://cplusplus.com/reference/stack/stack/?kw=stack

在这里插入图片描述
在stack官方文档说明中介绍其不是容器而是容器适配器,在文档中栈和在C语言中实现的其——先进后出的特点,其使用的是模板+封装实现的,这里使用的缺省参数是deque(在后面我们会介绍),如下是栈的主要接口:
在这里插入图片描述


这些接口我们发现很熟悉,在C语言中便已经实现过了

注意:容器适配器不再拥有迭代器了,举个简单的例子,如果栈和队列拥有了迭代器便不能保证先进先出和先进后出,而是可以通过迭代器的下标+[]即可出入数据

1.2 Queue

队列官方文档链接:https://cplusplus.com/reference/queue/queue/

在这里插入图片描述
同理:在squeue官方文档说明中介绍其不是容器而是容器适配器,在文档中栈和在C语言中实现的其——先进先出的特点,如下是队列的主要接口:
在这里插入图片描述


如若对C语言阶段实现的栈和队列有所遗忘,可以点击我——>后来者居上与先来后到:栈和队列的顺序哲学及算法实战(含源码)

1.3 最小栈的练习

力扣网址:https://leetcode.cn/problems/min-stack/

在这里插入图片描述


题目解释: 在本题中除了需要完成栈的基本接口以外,还要在时间复杂度为O(1)的情况下找到最小的元素

思路: 在这道题中大部分想到的思路便是:
1.通过定义变量来存储最小的值,将第一次插入到栈的数据先存放到变量中(假设第一次插入的数据为最小值)
2.再将后续的数据一个个插入到栈中,依次和变量存储的数据进行比较,如果插入栈中的数据小于变量存放的则变量中的数据更新为新的数据

但是我们如果要出栈的话,存放在变量中的最小元素也要删除,那么在栈中最小的元素是什么?如果我们要遍历栈,那么时间复杂度为O(n)

在前面的算法中我们便知道可以定义两个栈,一个栈_str用来进行入栈和出栈数据的存放与删除,一个栈_minstr用来和_str中插入的数据比大小

:如若_str入的数据小于_minstr中的数据,那么便将该数据插入到_minstr中(第一次数据入_str的同时也要入_minstr),如果_str插入的数据一直大于_minstr存放的数据,那么便不在_minstr中入数据(如果插入的数据与_minstr存放的数据相同是要将该数据存放到_minstr中)

代码演示:

classMinStack{public:MinStack(){}voidpush(int val){ _str.push(val);if(_minstr.empty()||val<=_minstr.top()) _minstr.push(val);}voidpop(){if(_str.top()==_minstr.top()){ _minstr.pop();} _str.pop();}inttop(){return _str.top();}intgetMin(){return _minstr.top();}private: stack<int> _str; stack<int> _minstr;};/** * Your MinStack object will be instantiated and called as such: * MinStack* obj = new MinStack(); * obj->push(val); * obj->pop(); * int param_3 = obj->top(); * int param_4 = obj->getMin(); */

1.4 栈的压入、弹出序列

牛客网址:栈的压入、弹出序列

在这里插入图片描述


题目解释: 给定两个数组,第一个表示进栈的顺序,第二个数组表示出栈的顺序,判断两个数组是否为一个栈的出入栈的顺序

思路: 只要入栈序列能模拟出出栈序列的过程,那么就是匹配的,否则不是

1.入栈序列先入栈(每次入一个数据)
2.每次入栈完后将该数据和出栈序列的数据进行比对,如果匹配则持续出数据(出栈序列指针依次往后走),不匹配则继续将入栈序列的数据入栈(入栈序列指针往后走)
3.入栈序列结束则结束

代码实现:

classSolution{public:/** * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可 * * * @param pushV int整型vector * @param popV int整型vector * @return bool布尔型 */boolIsPopOrder(vector<int>& pushV, vector<int>& popV){ size_t popi=0; stack<int> st;for(auto e:pushV){//入栈 st.push(e);//入栈序列与出栈序列匹配while(!st.empty()&&st.top()==popV[popi]){//将数据出栈,如果持续的出数据导致st中为空,无法取栈顶元素 st.pop();//让popi走向下一个位置 popi++;}}return st.empty();}};

1.5 二叉树的层序遍历

力扣网址:二叉树的层序遍历

在这里插入图片描述


题目解释:要求将二叉树的每一层放到二维数组的每一行上组层二维数组,这里的二维数组的每一行便不再相等(不是标准的二维数组)

那么怎么将输入的二叉树的节点放到二维数组对应的位置上?

思路: 广度优先遍历:

1.先将二叉树的根(3)入到队列里面,然后将其出出来,再将其的孩子入进去(9,20)

2.将9出出来,再将9的孩子节点出来,因为9没有孩子节点,所以不用出

3 .将20出出来,再将20的孩子节点出出来

但是如果9有孩子节点,那么队列中会同时存在两层不同的数据,那么我们怎么区分?

这里我们想到可以使用最小栈的思路,定义两个队列,第一个队列存放树的节点的指针,第二个队列存该数据是第几层的:

1.我们在q1队列入数的根节点(3),3对应的是第一层,因此q2队列存放1
2.将3和1出队列,再将3的孩子9,20入队列,因为根节点对应的是第一层,因此其孩子节点只需++,即第二层
3.将9出出来,因为其没有孩子节点,所以q2不入数据,q1出20,将20的孩子节点15,,7入数据,15,7对应的是第三层

但是由于定义了两个队列,有所繁琐,下面我们只使用一个队列看看如何搞定!

思路2:定义变量levelSize代表当前层的数据个数,我们每一次出数据都将该层的数据全部出出来(levelSize为0代表该层的数据出完了)

1.将根节点3入队列,levelSize为1,将3出队列,孩子节点9,20入队列,levelSize更新为2

2.将9出队列levelSize变为1,再将20出队列,levelsize为0,再将其孩子节点15,7入队列,levelSize更新为2,因此队列的size就是下一层的数据个数

代码实现:

/** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode() : val(0), left(nullptr), right(nullptr) {} * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} * }; */classSolution{public: vector<vector<int>>levelOrder(TreeNode* root){//二维数组 vector<vector<int>> vv; queue<TreeNode*>q;int levelsize=0;//当前层的数据个数,控制一层一层出if(root){ q.push(root); levelsize=1;}while(!q.empty()){//当前层的数据个数,控制一层一层出 vector<int> v;while(levelsize--){ TreeNode*front=q.front(); q.pop(); v.push_back(front->val);if(front->left) q.push(front->left);if(front->right) q.push(front->right);} vv.push_back(v);//当前层出完,下一层都进队列了,队列的size就是下一层的数据个数 levelsize=q.size();}return vv;}};

二、模拟实现栈

2.1 容器适配器

在传统实现容器时我们都使用模板,如下:

在这里插入图片描述


但在这里我们便使用适配器,那么什么是适配器?

适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结),该种模式是将一个类的接口转换成客户希望的另外一个接口。

在这里插入图片描述

在日常生活中由于家里的供电标准是120v,但是实际上我们手机或者电灯充电的是3v/5v,那么为什么我们的设备没有因为电量过大导致炸了(或者我们摸下充电器裸露的一部分,120v的电压我们碰到会怎么样)?这便是因为我们使用了充电器将电压转换成我们需要的功率

那么什么是设计模式?设计模式可以理解为孙子兵法,将各种打仗的方式进行了总结,迭代器也是一种设计模式

2.2栈的实现

在认识到适配器后,我们思考下栈的特点便是在栈顶插入删除元素这些操作,在我们已经学到的容器中vector和list支持在一段插入删除数据,因此其通过封装便可得到栈,这里我们传Container(容器),好处便是栈的类型不再写死,而是可以传vector/list都可以,通过Container适配转换出要的stack

代码实现:

#include<vector>#include<list>//栈这个容器可以使用其他容器进行封装转换得到栈//栈顶插入/删除即可//vector/list都可以在一段插入和删除数据,那么我们通过封装vector/list得到栈namespace hxx {//用Contaier适配转换出要的栈template<classT,classContainer==vector<T>>classstack{public://栈的构造函数便不需要写,因为Container是自定义类型,会调用Container的构造函数//我们统一把尾部当成栈顶voidpush(const T& x){ _con.push_back(x);}voidpop(){ _con.pop_back();}const T&top(){return _con.back();} size_t size()const{return _con.size();}boolempty()const{return _con.empty();}private: Container _con;};}

三、模拟实现队列

队列的实现与栈的实现大同小异,但是我们需要注意队列的实现不能通过封装vector来适配出队列

1.队列删除队头元素对应的是pop_front,但是在vector中没有该接口

2.vector头插和头删的效率低下

代码实现:

#pragmaoncenamespace hxx {//Container适配转换出queue,队列需要在一段进,另外一端出,这里我们不能使用vector进行适配,因为队列是删除队头元素,vector中没有pop_front接口template<classT,classContainer=list<T>>classqueue{public:voidpush(const T& x){ _con.push_back(x);}//删除队头元素voidpop(){ _con.pop_front();}//获取队头元素const T&front(){return _con.front();}//获取队尾元素const T&back(){return _con.back();} size_t size()const{return _con.size();}boolempty()const{return _con.empty();}private: Container _con;};}

三、总结

坚持到这里,已经很棒啦,希望读完本文可以帮读者大大更好了解栈和队列的底层逻辑!!!如果喜欢本文的可以给博主点点免费的攒攒,你们的支持就是我前进的动力🎆

资源分享:栈和队列模拟实现源码

Read more

Java字符串算法核心攻略

Java字符串算法核心攻略

Java字符串算法核心攻略 提示:在 ASCII 码(美国信息交换标准代码)中,大小写英文字母和数字的十进制数值范围如下: * 大写字母 (A - Z):65 到 90 * ‘A’ 是 65 * ‘Z’ 是 90 * 小写字母 (a - z):97 到 122 * ‘a’ 是 97 * ‘z’ 是 122 * 数字是 (0-9):48 到 57 * ‘0’ 是 48 * ‘9’ 是 57 一、必会的字符串方法 刷题前先把这些方法练熟,后面会反复用到。 1.

By Ne0inhk
【动态规划】买卖股票相关问题

【动态规划】买卖股票相关问题

一、买卖股票的最佳时机含手续费 714. 买卖股票的最佳时机含手续费 题目描述: 题目分析: 1、状态分析 其实一共就只有两种状态,不是处于 持有 状态,就是处于 未持有 状态。 * f[i]表示第i天结束后,处于 持有 状态时,最大的利润 * g[i]表示第i天结束后,处于 未持有 状态时,最大的利润 2、状态转移方程 对于f[i],有两种情况可以到达这种状态: * 在第 i-1 天时就已处于 持有 状态,然后第 i 天啥也不干,仍然保持 持有 状态。此时的最大利润就为 f[i - 1]

By Ne0inhk
从冒泡到模拟q sort函数——初见排序算法的探索和思考

从冒泡到模拟q sort函数——初见排序算法的探索和思考

国庆中秋喜相连,万家团圆乐同庆。 各位小伙伴们大家好,我是此方,在此,先祝大家双节快乐! 我们都知道排序有很多种:例如冒泡排序,插入排序,快速排序,等等很多种。 而冒泡排序,是各种计算机语言中最经典的一种排序算法。 今天我将从冒泡排序开始,到实现qsort函数的模拟。逐层深入,探索排序问题。 并给出鄙人的一些拙见。 上正文: 一,冒泡排序:最经典的排序算法 假如有一个十元素整型数组,他是完全倒着排序的:就像这样 now,我们要按照从小到大的顺序将这十个数字重新排列。 如果我们想要用冒泡排序:那么他的逻辑应该是这样的: 首先让最左边的数字和他右边的数字比较:9>8,将9和8互换位置: 让9继续和他右边的数字比较,再互换位 以此类推:9不断的比较——>移动——>再比较:最后;会到达最右边,这样,我们就让最大的数字9放在了最低位置 然后是8,接下来是7,6,5.

By Ne0inhk
算法思想之深度优先搜索(DFS)、递归以及案例(最多能得到多少克黄金、精准核酸检测、最富裕的小家庭)

算法思想之深度优先搜索(DFS)、递归以及案例(最多能得到多少克黄金、精准核酸检测、最富裕的小家庭)

深度优先搜索(DFS)、递归 * 深度优先搜索(Depth First Search,DFS)是一种用于遍历或搜索树或图的算法。在 DFS 算法中,从起始节点开始,沿着一条路径尽可能深地访问节点,直到到达叶子节点或者无法继续前进为止。然后退回到最近的一个有未探索节点的分支节点,继续探索其他路径,直到所有节点都被访问过为止。 * 深度优先搜索常常用于解决以下类型的问题:深度优先搜索是一种简单而强大的算法,可以解决许多实际问题。 * 图遍历:在无向图或有向图中寻找特定节点之间的路径、判断图的连通性等。 * 连通性问题:判断图中是否存在环、判断图的强连通分量等。 * 组合问题:生成排列、组合或子集等组合型问题。 * 寻路问题:求解从起始点到目标点的最短路径或所有可行路径。 * 递归问题:通过递归实现深度优先搜索,例如二叉树的遍历等。 小华最多能得到多少克黄金 * 题目描述小华按照地图去寻宝,地图上被划分成 m 行和 n 列的方格,横纵坐标范围分别是 [0, n-1] 和 [0, m-1]。在横坐标和纵坐标的数位之和不大于 k

By Ne0inhk