<C++> stack queue模拟实现

目录

前言

一、stack的使用

1. 接口说明

2. 例题

二、模拟实现stack

三、queue的使用

四、模拟实现queue

五、deque

总结


前言

LIFO stack

1. 栈是一种容器适配器,专门设计用于在后进先出上下文(后进先出)中运行,其中元素仅从容器的一端插入和提取。

2. stack 作为容器适配器实现,容器适配器是使用特定容器类的封装对象作为其底层容器的类,提供一组特定的成员函数来访问其元素。元素从特定容器的“背面”推出/弹出,这被称为堆栈的顶部

3. stack基础容器可以是任何标准容器类模板,也可以是一些其他专门设计的容器类。容器应支持以下操作:

  • empty
  • size
  • back
  • push_back
  • pop_back


4. 标准容器类,并满足这些要求。默认情况下,如果未为特定类实例化指定容器类,则使用标准容器。vectordequeliststackdeque

        包括queue,它们都是适配器,它们没有使用数组或链表单独实现它们的功能,它们是依靠封装的容器,将数据存在封装的容器内,然后根据要求适配出相应的需求。

        作为容器适配器的stack,它没有迭代器,因为stack的特性,不能支持迭代器的随便访问

        适配器的本质是一种复用,是一种设计模式


一、stack的使用

1. 接口说明

函数说明接口说明
stack()构造空的栈
empty()检查stack是否为空
size()返回stack中元素的个数
top()返回栈顶元素的引用
push()将元素val压入stack中
pop()将stack中尾部的元素弹出
  • 相应的接口十分易懂,我们简单看例子即可 
	std::stack<int> st;st.push(1);st.push(2);st.push(3);st.push(4);while (!st.empty()){cout << st.top() << " ";st.pop();}

2. 例题

例1:155. 最小栈

  • 如果只是增加一个min成员变量是有bug的,当最小值出栈后,min不会更新
  • 创建两个栈,栈1正常存数据,栈2存最小值,当栈1数据出栈时,判断其与栈2栈顶数据是否相等,若相等,则栈1、2都出栈一次;当栈1数据压栈时,判断其与栈2的栈顶数据是否相等,若相等则栈2也压栈相同数据,这是为了防止栈1有多个相同的最小值,栈1出栈最小数据后,栈2对应的最小值没有了,这就会出现问题

 

//用两个栈,一个栈存正常的数据,另一个栈存最小值
class MinStack {
public:MinStack() //初始化列表,默认调用自定义类型的构造函数{}void push(int val) {_st.push(val);if (min_st.empty() || val <= min_st.top()){min_st.push(val);}}void pop() {if (_st.top() == min_st.top()){min_st.pop();}_st.pop();}int top() {return _st.top();}int getMin() {return min_st.top();}private:stack<int> _st;stack<int> min_st;
};

例2.  JZ31 栈的压入、弹出序列

  •  模拟出栈,每次匹配,不同,则入栈,相同则出栈
class Solution {
public:bool IsPopOrder(vector<int>& pushV, vector<int>& popV) {stack<int> st;int pushi = 0, popi = 0;    //pushi控制pushV下标,popi控制popV下标while (pushi < pushV.size()){st.push(pushV[pushi++]);if (st.top() != popV[popi]){continue;}else {//相等则出栈while (!st.empty() && st.top() == popV[popi]){st.pop();++popi;}}}return st.empty();}
};

         逻辑优化

class Solution {
public:bool IsPopOrder(vector<int>& pushV, vector<int>& popV) {stack<int> st;int pushi = 0, popi = 0;while (pushi < pushV.size()){st.push(pushV[pushi++]);while (!st.empty() && st.top() == popV[popi]){st.pop();++popi;}}return st.empty();}
};

例3. 150. 逆波兰表达式求值

  • 创建一个栈,遍历vector
  • 如果是数字就压栈,使用 stoi 接口
  • 如果是操作符就出栈两次,先出栈的为右操作数,后出栈的为左操作数,计算后再将结果压栈
class Solution {
public:int evalRPN(vector<string>& tokens) {stack<int> st;for (auto& str : tokens){if (str == "+" || str == "-" || str == "*" || str == "/"){int right = st.top();st.pop();int left = st.top();st.pop();switch(str[0]){case '+':st.push(left + right);break;case '-':st.push(left - right);break;case '*':st.push(left * right);break;case '/':st.push(left / right);break;}}elsest.push(stoi(str));}return st.top();}
};

补充:如何将中序表达式改为后序表达式?

  • 开辟两个栈,遍历表达式
  • 对于操作数直接入栈1
  • 对于操作符,若栈2不为空或当前操作符比栈顶的优先级高,继续入栈;若栈不为空且当前操作符比栈顶的优先级低或相等,则将栈顶操作符压栈栈1
  • 表达式结束后,依次出栈2里面的操作符

类比:

类比stack容器,我们来看看queue例题 

102. 二叉树的层序遍历

思路:

我们可以用一种巧妙的方法修改广度优先搜索:

        1. 首先根元素入队
        2. 当队列不为空的时候
                2.1 求当前队列的长度 i

                2.2 依次从队列中取 i 个元素进行拓展,然后进入下一次迭代
        它和普通广度优先搜索的区别在于,普通广度优先搜索每次只取一个元素拓展,而这里每次取 i 个元素。在上述过程中的第 i 次迭代就得到了二叉树的第 i 层的i个元素。


class Solution {
public:vector<vector<int>> levelOrder(TreeNode* root) {vector<vector<int>> ret;    //存结果queue<TreeNode*> q;        //存各节点if (!root)return ret;//入队根节点q.push(root);while(!q.empty()){//当前队列存在的数据个数就是该层的数据的个数int currentLeveSize = q.size();ret.push_back(vector<int> ());//当前层的所有结点的所有孩子结点,就是下一层的数据个数for (int i = 0; i < currentLeveSize; i ++ ){auto node = q.front();q.pop();ret.back().push_back(node->val);if (node->left) q.push(node->left);if (node->right) q.push(node->right);}}return ret;}
};

二、模拟实现stack

        .h文件不编译,只展开,如果在

#include "Stack.h"

 之前,写了各种头文件,并且展开了命名空间std,那么程序是可以运行的,.h文件里需要使用的函数都已经在上面展开

        如果将

#include "Stack.h"
using namespace std;

        .h文件写在std之前,那么.h文件里如果使用std里的内容,编译器会报错,这是因为,编译器只会向上查找,为std是在.h文件下面展开的,所以.h文件是找不到std围起的内容

        所以,.h文件和std展开的位置是很重要的。

        我们知道,stack是适配器模式,我们之前写的数据结构只能选择顺序存储或链式存储,并不能相互转换,而适配器模式可以使用模板来随时转换储存结构,这就是大佬实现STL的stack的高处所在。

stack<int, vector<int>> st1;
stack<int, list<int>> st2;

            容器适配器:对容器进行封装,适配出我们想要的 

        对于适配器的栈,不需要构造函数、析构函数,因为它的成员变量是一个自定义类型的容器,自动调用其构造函数、析构函数.

        Container使用缺省模板

#pragma once
#include<iostream>
#include<deque>
using namespace std;namespace my_stack
{//容器适配器,对容器进行封装,适配出我们想要的template<class T, class Container = deque<T>>class stack{public:void push(const T& x){_con.push_back(x);}void pop(){_con.pop_back();}T& top(){//不能用[],因为可能是链表return _con.back();}size_t size(){return _con.size();}bool empty(){return _con.empty();}private:Container _con;};
}

三、queue的使用

FIFO queue
1. 队列是一种容器适配器,专门用于在 FIFO 上下文 ( 先进先出 )中操作,其中从容器一端插入元素,另一端提取元素。
2. 队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类, queue 提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列。
3. 底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操作:
        empty:检测队列是否为空
        size:返回队列中有效元素的个数
        front:返回队头元素的引用
        back:返回队尾元素的引用
        push_back:在队列尾部入队列
        pop_front:在队列头部出队列
4. 标准容器类 deque list 满足了这些要求。默认情况下,如果没有为 queue 实例化指定容器类,则使用标准容器deque

函数说明接口说明
queue()构造空的队列
empty()检测队列是否为空
size()返回队列中有效元素的个数
front()返回队头元素的引用
back()返回队尾元素的引用
push()在队尾将元素val入队列
pop()将队头元素出队列

使用方法同stack 

 四、模拟实现queue

        对于queue来说,STL实现时,就按照了链表容器来实现,不能使用vector容器,因为在pop时,queue调用的是pop_front()函数,而vector没有该函数

        为什么不适配vector呢?

        问题也显而易见,vector的pop_front效率太低了

#pragma once
#include<iostream>
#include<deque>
using namespace std;namespace my_queue
{//容器适配器,对容器进行封装template<class T, class Container = deque<T>>class queue{public:void push(const T& x){_con.push_back(x);}void pop(){_con.pop_front();}T& front(){//不能用[],因为可能是链表return _con.front();}T& back(){//不能用[],因为可能是链表return _con.back();}size_t size(){return _con.size();}bool empty(){return _con.empty();}private:Container _con;};
}

五、deque

1. 翻译 

双端队列

Deque(通常发音为“deck”)是 double-ended queue 的不规则首字母缩写。双端队列是具有动态大小的序列容器,可以在两端(其前端或后端)扩展或收缩。

特定的库可以以不同的方式实现 deque,通常作为某种形式的动态数组。但无论如何,它们都允许通过随机访问迭代器直接访问单个元素,并根据需要通过扩展和收缩容器来自动处理存储。

因此,它们提供了类似于
vector的功能,但在序列的开头,而不仅仅是在序列的末尾,都可以有效地插入和删除元素。但是,与vector不同的是,deques 不能保证将其所有元素存储在连续的存储位置:通过偏移指向另一个元素的指针来访问 a 中的元素会导致未定义的行为

vector 和 deque 都提供了非常相似的接口,可以用于类似的目的,但在内部,它们的工作方式完全不同:虽然vector使用单个数组,需要偶尔重新分配以进行增长,但 deque 的元素可以分散在不同的存储块中,容器在内部保留必要的信息,以提供对其任何元素的直接访问,时间恒定且均匀顺序接口(通过迭代器)。因此,deques 在内部比 vector 复杂一些,但这允许它们在某些情况下更有效地增长,尤其是在非常长的序列中,重新分配变得更加昂贵。

对于涉及在开头或结尾以外的位置频繁插入或删除元素的操作,deques 的性能较差,并且迭代器和引用的一致性低于列表和转发列表。

2. 成员函数 

3. 底层设计 

 deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组,其底层结构如下图所示:

双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其 整体连续 以及随机访问的假象,落 在了 deque 的迭代器身上, 因此 deque 的迭代器设计就比较复杂,如下图所示:

4. 缺陷 

         可以看出,双端队列deque表现十分出色,堪称“六边形战士”,但是如此出色的容器,为什么出名度不及vector、string呢?这是因为deque对于涉及在开头或结尾以外的位置频繁插入或删除元素的操作,deques 的性能较差 ,什么都可以,但什么都不精通。

相比vector:

相比list:

  1. 与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是必vector高的。
  2. 与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。
但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构

5. 为什么适配deque?

         stack是一种后进先出的特殊线性数据结构,因此只要具有 push_back() 和 pop_back()操作的线性结构,都可以作为stack的底层容器,比如 vector 和 list 都可以;

        queue是先进先出的特殊线性数据结构,只要具有 push_back和pop_front 操作的线性结构,都可以作为queue的底层容器,比如list

但是STL中对stack和queue默认选择deque作为其底层容器主要是因为:

        1. stack和queue不需要遍历(因此stackqueue没有迭代器),只需要在固定的一端或者两端进行操作。

        2. 在stack中元素增长时,dequevector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率高。

结合了deque的优点,而完美的避开了其缺陷 

所以deque相比vector、list更适合作为stack、queue的容器,stack、queue不会在中间位置插入删除,是高频的头尾操作。

 deque的实现很繁琐,有难度,它的迭代器封装了4个指针

deque设计初衷是替代vector、list的容器,但最终设计出来并没有很大优势,没有它们突出


总结

        综合deque的特点,stack、queue都十分适合采用适配deque容器,这是因为deque的设计特点。适配器不需要自己实现容器,所以stack、queue的代码量非常短,没有难点。下节,我们将学习优先级队列

        最后,如果小帅的本文哪里有错误,还请大家指出,请在评论区留言(ps:抱大佬的腿),新手创作,实属不易,如果满意,还请给个免费的赞,三连也不是不可以(流口水幻想)嘿!那我们下期再见喽,拜拜!

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Shopee个人能入驻开店吗?Shopee一个站点可以开几个店铺?

Shopee允许每个用户在同一个站点上开设多个店铺&#xff0c;这为商家提供了更多的经营灵活性和选择。同时&#xff0c;Shopee也鼓励个人用户入驻开店&#xff0c;提供便捷的入驻方式和丰富的支持工具。下面来看看具体介绍。 shopee个人能入驻开店吗 与许多电子商务平台相比&a…

基于vue 2.0的H5页面中使用H5自带的定位,高德地图定位,搜索周边商户,覆盖物标记,定位到当前城市

基于vue的H5页面中使用高德地图定位&#xff0c;搜索周边商户&#xff0c;覆盖物标记 首先安装高德地图插件 npm i amap/amap-jsapi-loader --save地图承载容器 <template><div id"container"></div> </template>地图容器样式 <style…

uniapp——项目day05

购物车页面 结算区域 把结算区域封装为组件 1. 在 components 目录中&#xff0c;新建 my-settle 结算组件&#xff1a; 2. 初始化 my-settle 组件的基本结构和样式&#xff1a; <template><view class"my-settle-container">结算组件</view> …

通讯协议学习之路(实践部分):UART开发实践

通讯协议之路主要分为两部分&#xff0c;第一部分从理论上面讲解各类协议的通讯原理以及通讯格式&#xff0c;第二部分从具体运用上讲解各类通讯协议的具体应用方法。 后续文章会同时发表在个人博客(jason1016.club)、CSDN&#xff1b;视频会发布在bilibili(UID:399951374) 本文…

第十九章绘图

Java绘图类 Graphics 类 Grapics 类是所有图形上下文的抽象基类&#xff0c;它允许应用程序在组件以及闭屏图像上进行绘制。Graphics 类封装了Java 支持的基本绘图操作所需的状态信息&#xff0c;主要包括颜色、字体、画笔、文本、图像等。 Graphics 类提供了绘图常用的方法&a…

新增文件收藏夹、回收站、终端等功能,1Panel开源面板v1.8.0发布

2023年11月13日&#xff0c;现代化、开源的Linux服务器运维管理面板1Panel正式发布v1.8.0版本。 在这一版本中&#xff0c;1Panel新增文件收藏夹、回收站、终端功能&#xff0c;面板设置时支持设置面板监听地址。此外&#xff0c;1Panel开源项目组还进行了60多项功能更新和问题…

【云栖2023】姜伟华:Hologres Serverless之路——揭秘弹性计算组

本文根据2023云栖大会演讲实录整理而成&#xff0c;演讲信息如下&#xff1a; 演讲人&#xff1a;姜伟华 | 阿里云计算平台事业部资深技术专家、阿里云实时数仓Hologres研发负责人 演讲主题&#xff1a;Hologres Serverless之路——揭秘弹性计算组 实时化成为了大数据平台的…

《视觉SLAM十四讲》-- 后端 1(上)

文章目录 08 后端 18.1 概述8.1.1 状态估计的概率解释8.1.2 线性系统和卡尔曼滤波&#xff08;KF&#xff09;8.1.3 非线性系统和扩展卡尔曼滤波&#xff08;EKF&#xff09;8.1.4 小结 08 后端 1 前端视觉里程计可以给出一个短时间内的轨迹和地图&#xff0c;但由于不可避免的…

WebMvcConfigurer配置详解

一、简介 WebMvcConfigurer配置类其实是Spring内部的一种配置方式&#xff0c;采用JavaBean的形式来代替传统的xml配置文件形式进行针对框架个性化定制&#xff0c;可以自定义一些Handler&#xff0c;Interceptor&#xff0c;ViewResolver&#xff0c;MessageConverter。基于ja…

idea2023启动springboot项目如何指定配置文件

方法一&#xff1a; 方法二&#xff1a; 举例&#xff1a;

split loop

// refactoringmotherfucker.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 //#include <iostream> #include <vector> #include <memory>// before refactoring of split loop class People { public:People(double _age,double _…

互斥量保护资源

一、概念 在多数情况下&#xff0c;互斥型信号量和二值型信号量非常相似&#xff0c;但是从功能上二值型信号量用于同步&#xff0c; 而互斥型信号量用于资源保护。 互斥型信号量和二值型信号量还有一个最大的区别&#xff0c;互斥型信号量可以有效解决优先级反转现 象。 …