【C++】手把手教你写出自己的Stack和Queue类
在上一篇文章中,我介绍了如何模拟实现 list容器,今天我们来实现 栈(Stack)和队列(Queue)。
我将 栈 与队列放置在一起的原因是 这两种数据结构 是十分相似的,将他们放在一起可以相互比较,对照,让我们的学习更加简单。
在开始模拟实现之前,我需要先引入一些新的知识,比如说 优先级队列,容器适配器,deque等,这些对我们的最终效果与还原程度 至关重要,请同学们耐心阅读。
目录
- stack的介绍与使用
- stack 简介
- stack 的基本接口
- queue的介绍与使用
- queue 简介
- queue 的基本接口
- 容器适配器
- 优先级队列
- 优先级队列简介
- 优先级队列的使用
- 优先级队列的模拟实现
- 基本框架
- 构造函数
- push
- pop
- 其他接口
- 仿函数
- deque(了解)
- deque的简介
- deque的缺陷
- 为什么选择deque
- stack与queue模拟实现
- stack
- queue
stack的介绍与使用
stack 简介
stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。
stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作:
empty:判空操作
back:获取尾部元素操作
push_back:尾部插入元素操作
pop_back:尾部删除元素操作标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque
stack 的基本接口
函数说明 | 接口说明 |
---|---|
stack() | 构造空的栈 |
empty() | 检测stack是否为空 |
size() | 返回stack中元素的个数 |
top() | 返回栈顶元素的引用 |
push() | 将元素val压入stack中 |
pop() | 将stack中尾部的元素弹出 |
queue的介绍与使用
queue 简介
- 队列是一种容器适配器,专门用于在FIFO上下文(先进先出)中操作,其中从容器一端插入元素,另一端提取元素。
- 队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列。
- 底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以操作:
empty:检测队列是否为空
size:返回队列中有效元素的个数
front:返回队头元素的引用
back:返回队尾元素的引用
push_back:在队列尾部入队列
pop_front:在队列头部出队列 - 标准容器类deque和list满足了这些要求。默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque。
queue 的基本接口
函数声明 | 接口说明 |
---|---|
queue() | 构造空的队列 |
empty() | 检测队列是否为空,是返回true,否则返回false |
size() | 返回队列中有效元素的个数 |
front() | 返回队头元素的引用 |
back() | 返回队尾元素的引用 |
push() | 在队尾将元素val入队列 |
pop() | 将队头元素出队列 |
容器适配器
适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结),该种模式是将一个类的接口转换成客户希望的另外一个接口。
举个例子,我们经常用到的 转换头 ,网线接头是一种 接口,路由器的接口 是USB口,两者无法直接转换,这是我们通过 转换头 就可以 将一个接口 间接 转换为另一个。
虽然stack和queue中也可以存放元素,但在STL中并没有将其划分在容器的行列,而是将其称为容器适配器,这是因为stack和队列只是对其他容器的接口进行了包装,STL中stack和queue默认使用deque,比如class template
优先级队列
优先级队列简介
对于上面两种常见的数据结构,这里不再过多的介绍,接下来,我们介绍一种更为复杂的结构:优先级队列。
- 优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最的。
- 此上下文类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元素)。
- 优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从特定容器的“尾部”弹出,其称为优先队列的顶部。
- 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭代器访问,并支持以下操作:
empty():检测容器是否为空
size():返回容器中有效元素个数
front():返回容器中第一个元素的引用
push_back():在容器尾部插入元素
pop_back():删除容器尾部元素
- 标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指
定容器类,则使用vector。 - 需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数
make_heap、push_heap和pop_heap来自动完成此操作。
优先级队列的使用
优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。注意:默认情况下priority_queue是大堆。
这里对”堆“这个特殊的树形结构不熟悉的话,可以看我关于堆的博客:
【C语言】堆
函数声明 | 接口说明 |
---|---|
priority queue() / priority queue(first,last) | 构造一个空的优先级队列 |
empty() | 检测优先级队列是否为空 |
top() | 返回优先级队列中最大(最小)元素,即堆顶元素 |
push(x) | 在优先级队列中插入元素x |
pop() | 删除优先级队列中最大(最小)元素,即堆顶元素 |
我们来测试一下:
void Test1()
{vector<int> v{ 3,2,7,6,0,4,1,9,8,5 };priority_queue<int> q1;for (auto& e : v)q1.push(e);cout <<"堆顶元素:"<< q1.top() << endl;}
可以看到q1中的元素按照大堆的方式排列的。
如果我们想要取堆顶元素时是最小值,也是可行的。
这时我们要在构造优先级队列的时候传三个参数。
之所以之前的构造中只需要传一个类型参数,是因为模板中的后两个是缺省参数, 其中第三个参数就是将大堆转化为小堆的关键,在模拟实现中我们会讲到。
priority_queue<int, vector<int>, greater<int>> q2(v.begin(), v.end());
cout << q2.top() << endl;
// #include <functional> // greater算法的头文件
优先级队列的模拟实现
我们知道,堆的底层结构就是 堆(特殊的完全二叉树),因此核心思路就是对 普通的堆的元素进行堆的相关操作 并封装起来即可。
这里再次强调一下,不熟悉堆的同学看一下这篇博客:
【C语言】堆
这里我们先将 默认情况下(大堆)的 优先队列 模拟实现 。
基本框架
我们先将 priority_queue 的基本框架 写一下:
这里我们就可以很直观的感受 适配器 是什么了,模板参数中的第二个参数 就是 适配器,我们可以传vector 或者 list 来实现我们的 优先级队列。
Container模板 在 priority_queue类 中实例化,这时候我们就可以使用 实例化对象的接口,帮助我们实现priority_queue的接口。
namespace yyk
{template<class T,class Container = vector<T>>class priority_queue{public://无参构造priority_queue(){}//有参构造termplate<class InputIterator>template_queue(InputIterator first, InputIterator last){}//数据插入void push(const T& x) {}//删除堆顶元素void pop(){}//获取堆顶元素const T& top(){} //获取元素个数size_t size(){}//判空bool empty(){}private:Container _con;}
}
在具体实现接口之前,我们还要引入堆的几个算法(这里暂时默认调整为大堆):
- 堆的向上调整算法
- 堆的向下调整算法
关于具体分析,这里不再赘述,详情看 堆 的博客。
//堆的向上调整算法void AdjustUp(size_t child){int parent = (child - 1) / 2;while (child > 0){if (_con[parent] <_con[child]){swap(_con[parent]._con[child]);child = parent;parent = (child - 1) / 2;}else{break;}}}//堆的向下调整算法void AdjustDown(size_t parent){int l_child = 2 * parent + 1;while (l_child < _con.size()){if (l_child + 1 < _con.size() && _con[l_child] < _con[l_child+1]){l_child++;}if (_con[parent] < _con[l_child]){swap(_con[l_child], _con[parent]);parent = l_child;l_child = parent * 2 + 1;}else{break;}}}
构造函数
对于有参构造,我们要做两件事:
- 将元素插入空间中
- 利用 建堆算法 将元素排列为 大堆
//有参构造termplate<class InputIterator>template_queue(InputIterator first, InputIterator last){//插入while (first != last){_con.push_back(*first);++first;}//建堆 (具体原理 见 堆 博客)for (int i = (_con.size() - 1 - 1) / 2; i >= 0; i--){AdjustDwon(i);} }
push
我们的push函数也很简单,我们先用 适配器的接口中的尾插函数,向 堆尾插入数据,再使用向上调整算法 重新将 数据 调整为大堆。
void push (const T& x)
{_con.push_back(x);AdjustUp(_con.size()-1);
}
pop
pop()是删除堆顶的元素。
在堆中,删除堆顶元素的时候,为了减少复杂度,我们通常:
- 将堆顶元素与堆中最后一个元素进行交换。
- 删除堆中最后一个元素
- 将堆顶元素向下调整到满足堆的特征为止
void pop()
{swap(_con[0],_con[_con.size()-1]);_con.pop_back();AjustDown();
}
其他接口
其他接口比较简单,我们一并实现。
const T& top(){return _con[0];}size_t size(){return _con.size();}bool empty(){return _con.empty();}
这里对于 top()要注意一下,当我们返回 堆顶引用的时候,最好加const,防止堆顶元素被篡改。
仿函数
至此,我们的优先级基本队列完成了,但是此时内部是一个大堆,如果我们取栈顶元素的时候想要最小的元素,这怎么办?
其实很容易想到,我们将两个堆的调整算法 改成 小堆版本的就可以,只需要改变部分符号就行。
但是,这样做的难度不小,”运算符“是很难作为参数或模板传入的。又或者我们直接写两份 优先级队列,一个大堆版,一个小堆版,不过这样造成了代码的低可读性,复用性。
这个时候就要用到我们的第三个 模板参数。
可以看到,第三个参数简单来说就是这样的,是一个缺省参数:
class Compare =less<T>
补充:Container::value 就是 模板类型参数 T ,typename 的作用是提示 编译器 这是一个类型,对于部分编译器,可以省略typename
我们注意到,这里的缺省参数是less,这就是我们要讲的 仿函数,又叫函数对象,是一种自定义类型。
以Less为例,它是一个类,实例化之后是类型的对象,可以像函数一样使用
template <class T>
class Less
{public:bool operator()(const T& x, const T& y){return x < y;}
};
void test_Less()
{Less less;cout<< less.operator()(1,2) <<endl;//less是类型的对象,可以像函数一样使用cout<<less(1,2)<<endl;
}
也就是说,我们在less类中 讲 "()"重载了,使它等效于 “<” , 同理我们也可以 写出一个 “>” :
template<class T>class Greater{public:bool operator()(const T& x, const T& y){return x > y;}};
有了 仿函数,我们就可以将这个作为类模板 ,以此来控制 运算符,我们想要小堆,就将模板写为Greater,大堆就写Less:
void AdjustUp(size_t child){Compare com; //将Greart/Less模板实例化int parent = (child - 1) / 2;while (child > 0){//if (_con[parent] < _con[child])if (com(_con[parent],_con[child])){swap(_con[parent], _con[child]);child = parent;parent = (child - 1) / 2;}else{break;}}}void AdjustDwon(size_t parent){Compare com;size_t child = parent * 2 + 1;while (child < _con.size()){//if (child + 1 < _con.size() && _con[child] < _con[child+1])if (child + 1 < _con.size() && com(_con[child],_con[child + 1])){++child;}//if (_con[parent] < _con[child])if (com(_con[parent],_con[child])){swap(_con[parent], _con[child]);parent = child;child = parent * 2 + 1;}else{break;}}}
deque(了解)
在模拟实现stack与queue之前,我们还有最后一个定义要了解:deque。
对于优先级队列和 stack,queue,都是适配器实现的,其中 优先级队列是利用 vector/queue的接口包装实现的,而 stack和queue是使用deque的接口包装实现的。
那么deque到底是什么?
deque的简介
deque(双端队列):是一种双开口的"连续"空间的数据结构。
双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高
但是,deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组,其底层结构如下图所示:
这个结构比较复杂,这里只介绍其优缺点,有兴趣的同学可以看《stl原码剖析》。
deque的测试:
void test_deque(){deque<int>dp;dp.push_back(1);dp.push_back(2);dp.push_back(3);dp.push_back(4);dp.push_front(1);dp.push_front(2); dp.push_front(3);dp.push_front(4);//list无法用[]访问//融合了vector 和list的优点,从使用的角度,避开了他们各自的缺点//for (size_t i = 0; i < dp.size(); ++i){cout << dp[i] << " ";}}
}
deque的缺陷
与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是必vector高的。
与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。
但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。
为什么选择deque
stack是一种后进先出的特殊线性数据结构,因此只要具有push_back()和pop_back()操作的线性结构,都可以作为stack的底层容器,比如vector和list都可以;queue是先进先出的特殊线性数据结构,只要具有push_back和pop_front操作的线性结构,都可以作为queue的底层容器,比如list。但是STL中对stack和queue默认选择deque作为其底层容器,主要是因为:
- stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作。
- 在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长
时,deque不仅效率高,而且内存使用率高。
结合了deque的优点,而完美的避开了其缺陷。
stack与queue模拟实现
我们已经了解过了优先级队列的实现,而stack和 queue更加简单,所以这里不再赘述:
stack
#pragma once
#include <vector>
#include<list>
#include<deque>namespace yyk
{//stack是一个Container 适配(封装转换)出来的//给缺省参数 deque(双端队列)template<class T,class Container=std::deque<T>>class stack{//Container 尾认为是栈顶public:void push(const T& x){_con.push_back(x);}void pop(){_con.pop_back();}const T& top(){return _con.size();}bool empty(){return _con.empty();}private:Container _con;};void test_stack(){stack<int, std::vector<int>>st;st.push(1);st.push(2);st.push(3);st.push(4);while (!st.empty()){cout << st.top() << " ";st.pop();}cout << endl;}
}
queue
#pragma once
#include <vector>
#include<list>
#include<deque>namespace yyk
{//stack是一个Container 适配(封装转换)出来的//给缺省参数 deque(双端队列)template<class T, class Container = std::deque<T>>class queue{//Container 尾认为是队尾,头是队头public:void push(const T& x){_con.push_back(x);}void pop(){_con.pop_front();}const T& front(){return _con.front();}const T& back(){return _con.back();}bool empty(){return _con.empty();}private:Container _con;};void test_queue(){queue<int, std::vector<int>>st;//vector 头删效率低,不能头删st.push(1);st.push(2);st.push(3);st.push(4);while (!st.empty()){cout << st.front() << " ";st.pop();}cout << endl;}
至此,关于栈与队列的基础内容就结束了,欢迎讨论。
【C++】手把手教你写出自己的Stack和Queue类相关推荐
- 手把手教你写出几十种让同事无法维护的代码!
对,你没看错 本文就是教你怎么写出让同事无法维护的代码! 01 程序命名 容易输入的变量名.比如:Fred,asdf 单字母的变量名.比如:a,b,c, x,y,z(如果不够用,可以考虑a1,a2,a ...
- 文案圈内的拿破仑:新媒体推广运营文案创作的黄金法则,黎想手把手教你写出黄金文案
正式交大家进行黄金文案创作前,先由艺形艺意工作室创始人黎想给大家分享一下移动互联网的热潮: 2017年移动互联网的热潮是知识付费: 2018年移动互联网的热潮是新媒体推广(自媒体)创作: 2019年移 ...
- 大厂秘籍:谷歌代码规范开放下载,手把手教你写出好代码
这两天和一位大厂的朋友聊天,说起他们今年技术岗晋级答辩要增加代码走读环节,那该如何写出好看又好用的代码? 代码是程序员改变世界的工具,每个程序员都会写代码,但不一定能写出好代码. 如今的大型商业软件系 ...
- 【C++】手把手教你写出自己的vector类
在上一篇博客中,我们学习了vector 的基本使用,以及 迭代器的失效问题: [C++]深入理解vector类(一) 今天我们来模拟实现以下vector类. 目录 成员变量 接口实现 构造函数 迭代器 ...
- 专家手把手教你写出高水平个人简历
如果已经有全职工作了,一定不要把工作经历放在第二:如果您目前还是在校学生,应该把教育背景放在第一.作为在职人员,若把教育背景放在前面,人家会对你很不重视. 1. 页眉部分 (Heading) 1)名字 ...
- html个性签名怎么写,手把手教你写出自己的个性签名
俗话说的好,见字如面,你的签名就是你的一张形象名片.写得一手漂亮的签名非常重要,一看签名就能看出这个人的涵养,能提升你的形象,能在同事和领导面前留下好的印象,还能结交到更多志同道合的朋友. 前一段时间 ...
- 软件测试代码很难?手把手教你写出阿里巴巴注册界面
那么今天的话呢,带大家来学习的是三个内容 文章首发于公众号:程序员阿沐 本节大纲: 01.认识HTML 02.表单的运用 03.阿里巴巴注册界面的实现 我们要去实现阿里巴巴注册界面,一个前端界面. 我 ...
- 手把手教你写出《三子棋》小游戏
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言 一.设计思路 二.具体步骤 1.游戏的逻辑 2.游戏的实现 1)头文件的使用 2)InitBoard(初始化棋盘) 3 ...
- 一篇文章带你认识c语言并手把手教你写出你的第一个程序
写在前面的自我介绍:某211大学计算机科学与技术专业大一新生,目前在洛谷,牛客,leetcode等网站进行刷题,通过自学熟悉c/c++的语法,目前正在自学算法(努力学习ing),以下文章是本人作为一个 ...
最新文章
- kafka后台启动命令
- 用反射写的取属性值和设置属性值得方法
- gmock学习01---Linux配置gmock
- C#合并文件夹图片列表 自定义排版顺序
- PPP认证方式pap chap chap2
- Linux C高级编程——文件操作之库函数
- 工业互联网解决方案创新应用报告(2020)
- mysql concat键值对_mysql中concat函数实现数据库字段合并查询
- mht转html转换器apk,MHT文件转换工具BitRecover MHT Converter
- Autojs 3.0文档学习之设备信息
- 超实用!!MySQL数据库——Amoeba读写分离
- CMD打开Git Bash
- 小米手环6获取auth_key更换第三方表盘(零基础)
- 这4类人去创业和自由职业会死得很惨
- Thumbnails框架图片缩略处理
- 利用Python进行心脏病患者特征分析
- BigBrother的大数据之旅Day 10 hive(1)
- geoCoordMap数据,全国省市,4个直辖市,用于echart gl 3d地图
- http://www.cnblogs.com/zyw-205520/p/4771253.html
- CUMT微机原理复习笔记
热门文章
- 基于深度神经网络的火灾探测声学灭火器控制
- Java8 官方jvm 标准参考 -XX 配置参数详细信息
- Linux查主板槽位使用情况,linux下查看主板内存槽与内存信息
- cent os 7.x上安装oracle 11g
- 计算机网络路由器的配置连接不上,为什么路由器连接不上_我的电脑换了一个路由器怎么就连接不上网络呢...
- 熊孩子乱敲键盘攻破linux桌面,“熊孩子”乱敲键盘就攻破了 Linux 桌面,大神:17 年前我就警告过你们...
- 森林图怎么分析_大地量子 森林火灾 | 遥感+AI 成为森林火灾预防和监测的重要手段...
- BPC BADI开发注意事项
- 初试百度vidpress一键生成视频
- ArrayList源码翻译