c++STL——stack、queue、priority_queue的模拟实现

文章目录

  • stack、queue、priority_queue的模拟实现
    • 使用部分
    • 模拟实现
      • 容器适配器
      • deque的介绍
        • 原理
        • 真实结构
        • deque的迭代器
        • deque的操作
        • deque的优缺点
      • stack的模拟实现
      • 按需实例化
      • queue的模拟实现
      • priority_queue的模拟实现
        • 为何引入仿函数
        • 代码实现

stack、queue、priority_queue的模拟实现

使用部分

对于stackqueue的使用其实是非常简单的。当我们进行文档的查阅的时候,会发现提供的接口其实并不算多,也没有迭代器。这是因为栈只能在栈顶操作数据,队列只能在队尾队头操作数据。不能遍历数据。

priority_queue名称是优先级队列,看着很陌生,实际上我们早已经接触过,其实就是我们以前实现的数据结构——堆。

对于它们三个的使用我们就不进行过多的介绍了,因为接口的使用是非常简单的,且以往我们已经使用和实现过。所以自行查阅文档使用即可:
queue的使用(包括priority_queue)
stack的使用

模拟实现

本篇文章的重点将放在三者的模拟实现上。这三者的实现和以往的模拟实现是有些不同的。

容器适配器

在以往学习list等容器的时候,我们会发现模板参数里面是有内存池的声名:
在这里插入图片描述
allocator< T >,这是内存池的声名。

但是我们仔细翻看stackqueue的时候发现:
在这里插入图片描述
在这里插入图片描述
我们发现模板参数里面竟然声明了一个Container类型,且给有缺省值deque< T >。这里就需要说一下,模板参数也是可以给缺省值的

其实priorty_queue也是一样的。也是使用这样的模板声名方式。

其实也就是说,其实stackqueue其实并不是像我们以前那样自行开辟数组空间或者实现链式队列一样。再整出一些顺序表来实现这些结构。

栈和队列的本质就是线性表,只不过是有限制的线性表。只能在表的固定位置进行访问和修改。而STL库中已经实现好了vectorlist这样比较复杂的线性表,并且是有提供相关的接口进行操作的。

那也就是说,完全可以将已经实现好的线性表再进行一次封装,只提供固定位置的修改和访问操作的接口。那这样子其实就很方便了,就不用再费时费力去手撕一个栈或队列的代码出来。

我们看文档图中被方框圈起来的字样,Container Adaptor其实就是容器适配器的意思,其实就是通过对别的容器进行二次封装后实现的容器。

只不过说,对于默认的适配容器,这里选择了deque这个容器。其实很好理解,因为对于栈来说,使用vector最为方便,对于队列来说,使用list最为方便。(在c语言数据结构部分就已经说过)。但是反过来就不方便了。

基于此,STL库中实现了一种将vectorlist的特性融合的容器,即为deque,我们可以理解为是二者的缝合怪

deque的介绍

我们先来了解一下deque是什么。

原理

deque本质也是个容器,其内部结构比较复杂:

deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高:
在这里插入图片描述

真实结构

这样一看,那和vector也没什么太大的区别嘛。就是多了一端进行插入删除操作。

其实并不是这样子的。实际上,deque是一段由一段的连续空间(缓冲区)存储数据,然后这每一段空间的地址会放在一个叫中控数组的地方:
在这里插入图片描述
大致是这个样子。中控数组存储的是各个缓冲区的地址,各个缓冲区进行存储数据。

这样子是结合了vectorlist的特点进行构造的数据结构。直到结构后,我们就得明白deque时如何访问存储的数据的,这样子才能明白其对应的操作是如何进行的。

deque的迭代器

实现了list后我们肯定就知道,对于deque这么复杂的结构,使用原生指针肯定是不可能实现迭代器的功能的。肯定是需要对其进行封装,我们来看看源码:
在这里插入图片描述
迭代器有四个成员变量,有三个是指向缓冲区数据类型的指针,一个是指向缓冲区的指针。

具体的指向是怎么样的呢?

在这里插入图片描述
对于firstlast指针,分别指向的是缓冲区的开头数据位置和末尾数据位置的下一个。我们可以认为是以原生指针形式实现的、每一小段缓冲区的迭代器。而cur指针是用来遍历数据的,一旦达到了last的位置就需要遍历到下一段缓冲区了。而node指针是用来找到当前缓冲区所处中控数组的位置的。

缓冲区的指针只需要存储在这个中控器上,存储的位置一定是从中间开始存储一直扩散至两侧。因为需要头插和尾插,放在中间肯定是更方便的。当中控数组位置不够时扩容即可。对于中控数组的扩容代价是一点也不大的,只需要将地址复制到新中控器,将原中控器的指向置空再释放即可。也就是说,使用浅拷贝就可以。

deque的操作

实现好这样的迭代器之后,只需要通过迭代器来进行容器的一系列操作即可。这个结构我们只需做一些简单的了解即可。不用去模拟实现。因为这个结构还是有那么一些的缺陷的。

deque容器中会存放两个迭代器,一个是_start,指向第一个缓冲区,一个是_finish,指向最后一个缓冲区。可能还有有一些别的数据,如缓冲区个数等(即中控器中数据个数)。

尾插操作:就是在_finish这个迭代器的cur指针位置插入后,再将cur指针往后移动。如果当前cur指针和last指针是处于一个位置的时候,就需要移动至下一个缓冲区。也就是将node指针向后移动一个位置。再调整其余指针的位置后再执行上述操作。当然如果中控器位置不够需要重新扩容。

头插操作:过程与尾插相反,需要在_start这个迭代器的cur指针位置插入,只不过我们需要注意的是,对于区间的选择,一般都是倾向于左闭右开,所以尾插的时候cur指针指向的是最后一个数据的下一个位置,而头插的cur指针指向的是第一个数据。

且尾插是在缓冲区中是从前往后插入。头插不一样,是从后往前插入:
在这里插入图片描述
随机访问操作deque是支持随机访问,也就是说它的迭代器是随机类型。可以支持运算符[]的重载。想要访问也是很简单。比如访问第25个数据(从第0个开始),每个缓冲区有10个数据。可以先25 / 10 = 2算出当前是第几个缓冲区,然后再25 % 10 = 5算出位于缓冲区的位置。然后通过中控器访问,指向中控器的map是一个二级指针,经过解引用得到内部数据,就是指向缓冲区的一级指针,再经过解引用就可以得到数据,所以此时deque dq[25] = map[25 / 10][25 % 10] 就可以访问到了,还是很简单的。

置于其他的使用其实很简单,只要前面的容器有认真学习和使用过都会用。因为底层都是将接口进行封装了,我们只需要关心怎么用的。

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

还要说的是是中间位置插入的效率,对于deque来说,除了头部尾部之外的插入其实都很麻烦,这是显而易见的。所以一旦涉及到大量的中间位置的插入,我们都是可以选择使用list,因为任意位置的插入是O(1),且不挪动数据。但是由于其尾插头插效率极高,甚至更甚vector一筹,所以对于栈和队列这种只在一侧或者两侧大量操作的容器,很明显用deque是效率更高。

stack的模拟实现

了解完deque后,我们来实现一下stack,我们直接上代码:

namespace MySpace {template<class T, class Container = deque<T>>class stack {public:stack() {}//一定要写 会走初始化列表 如果是自定义类型会调用其默认构造函数stack(const initializer_list<T>& x){typename initializer_list<T>::iterator it = x.begin();while (it != x.end()) {push(*it); ++it;}} void push(const T& x) {_con.push_back(x);   }void pop() {_con.pop_back();}T& top() {return _con.back();}const T& top() const {return _con.front();}size_t size() const{return _con.size();}bool empty() const {return _con.empty();}private: Container _con;};}

当然,用initializer_list这个构造函数可以不写,只写那个空的即可。我写只是为了创建对象的时候方便一些,便于大量测试。

那个空的构造函数是一定要写的。我们来回顾一下:
因为即使是空的构造函数,成员变量也要走初始化列表的操作,对于内置类型,有传参用传参的,反之用缺省值,如果再没有只能给随机值了。而自定义类型不一样了,会调用其默认构造函数,没有就报错。我们这里的stack是只有一个容器的,这个容器默认是STL 库里面的deque,肯定是有其默认构造函数的。如果这里是我们自己整的容器那我们也要提供默认构造函数。

而后对于入栈出栈以及判空,获取栈顶数据,获取数据个数都是调用的容器内已有的接口。这十分方便。

按需实例化

很多人会好奇,既然是调用的容器的接口,但是对于有些容器来说,有些接口它是没有的。就比如vector是没有pop_front接口的。如果我们在类模板里面调用了这个接口,但是显示实例化类对象的时候传了vector这个容器怎么办呢?

这个不用担心,编译器会报错的。模板最大的特点就是按需实例化,如果有些接口我们不进行调用,写在那里它并不会报错。如果我们调用了那个接口,如果与当前容器不符,编译器是能够与检查的出来的。

queue的模拟实现

queue的实现其实和stack也是很类似的,稍微修改一下就好:

namespace MySpace {template<class T, class Container = deque<T>>class queue {public:queue() {}queue(const initializer_list<T>& x) {typename initializer_list<T>::iterator it = x.begin(); while (it != x.end()) { push(*it);  ++it; } }void push(const T& x) {_con.push_back(x);}void pop() {_con.pop_front();  }T& front() {return _con.front();}const T& front() const{return _con.front();}T& back() {return _con.back();}const T& back() const {return _con.back();}size_t size() const {return _con.size();}bool empty() const {return _con.empty();}private:Container _con;};}

这里就是刚刚说到的那种情况,我们调用了pop_front接口,当然dequelist<是有这样的接口的,所以队列的是新是不能用vector的,这其实也符合之前的判断。因为用vector不适合头删数据。

priority_queue的模拟实现

priority_queue的模拟实现其实早已经实现过,但是在这里我们得引入一些新的用法。我们先来看看文档中是怎么定义的:
在这里插入图片描述
我们发现,除了适配容器之外,模板参数中还多了一个叫Compare的参数,这个是什么呢?

这个其实是仿函数,其实也是一个类。

为何引入仿函数

还记得以前模拟实现堆这个数据结构的时候,我们可能需要通过手动调整向上调整算法和向下调整算法的的比较逻辑,从而达到是建大堆还是建小堆。

但这很明显是非常不合理的。总不能每一次都要手动调整吧。但是在学习c语言的时候是没有太多办法解决这个问题,除了函数指针。

函数指针用来控制比较逻辑其实我们早已见过,就是使用c库内自带的qsort函数时,需要我们自己传一个比较逻辑,这个比较逻辑按照库中的要求就是排成升序。但是我们实现和库要求相反的比较函数,就可以排升序。这些就不再多说,都是以前的知识。

但是c++中是不太喜欢使用函数指针这个东西的,因为函数指针还是比较复杂的,且代码的可读性一般。有没有什么办法能够达到这样的效果:
即我传入一个类似小于号的东西,就建立大堆,传入类似大于号的东西,就建立小堆。然后就使用这个传入的内容去执行比较逻辑,复用这个逻辑建立大堆或者小堆呢?

答案是可以的,标准库里面就是用仿函数实现的。至于为什么要规定小于号建大堆,大于号建小堆这是不用管的,标准库中就是这样玩的。

我们来看看仿函数是怎么写的:

template<class T>
class less {
public:bool operator()(const T& x1, const T& x2) {return x1 < x2;}
};template<class T>
class greater {
public:bool operator()(const T& x1, const T& x2) {return x1 > x2;}
};

当前我们只需要简单的了解一下仿函数怎么简单的实现一个并且会使用即可。

仿函数其实就是一个空的类,没有成员变量,内部有一个对()进行重载的函数,返回值是bool类型。注意这个空类的大小为1个字节,这是为了占位。这在类的大小部分说过。

这也是个类模板,其实很好理解,因为比较的内容可能不只是整形或者浮点型,各式各样的都需要比较。STL内的容器也是可以比较的,因为内部都有重载比较逻辑的函数。

如果是我们自己实现的类比较,我们可以在内部自行实现比较逻辑。但是如果我们现在是传入两个指针指向两个对象,那使用类内的比较逻辑可能就不是我们想要的。解决方法就是:要不然就是在内部重载一个专门针对指针的比较逻辑,要不然就是重载一个仿函数即可。

代码实现

我们来看看代码实现:

namespace MySpace {template<class T>class less {public:bool operator()(const T& x1, const T& x2) {return x1 < x2;}};template<class T>class greater {public:bool operator()(const T& x1, const T& x2) {return x1 > x2;}};template<class T, class Container = vector<T>, class Compare = less<T>> //默认less代表传小于号 建大堆 反之相反class priority_queue {public: priority_queue() {}template <class InputIterator>priority_queue(InputIterator first, InputIterator last) {InputIterator it = first;while (it != last) {push(*it);++it;}}template <class T>priority_queue(const initializer_list<T>& x) { typename initializer_list<T>::iterator it = x.begin(); while (it != x.end()) { push(*it);++it;  } }void push(const T& x) {_con.push_back(x);Adjustup(size() - 1);}void pop() {std::swap(_con[0], _con[size() - 1]);_con.pop_back();AdjustDown(0);}bool empty() const {return _con.empty();}size_t size() const {return _con.size();}T& top() const {return _con[0];}void Adjustup(size_t child) {int Child = child, Parent = (child - 1) / 2;while (Child > 0) {if (_cmp(_con[Parent], _con[Child])) {std::swap(_con[Parent], _con[Child]);Child = Parent;Parent = (Parent - 1) / 2;}else break;}}void AdjustDown(size_t parent) {size_t Parent = parent, Child = 2 * Parent + 1; while (2 * Parent + 1 < size()) {if (Child + 1 < size() && _cmp(_con[Child], _con[Child + 1])) ++Child; if (_cmp(_con[Parent], _con[Child])) {std::swap(_con[Parent], _con[Child]);Parent = Child; Child = Child * 2 + 1;}else break;}}private:Container _con;Compare _cmp;};}

虽然以前实现过这个数据结构,但是把它们放在类里面还是需要进行调整的。比如向上向下调整算法的参数就是被修改的。

从这里我们也看得出来为什么是仿函数了,因为调用的形式就和函数一样,传入两个参数进行比较,表达式的返回值就是比较的结果。和函数是一样的方法,但是我们还能通过外界控制整个逻辑。

当然由于引入了仿函数的机制,所以向上向下调整算法的逻辑可能稍微需要修改一下,和以往我们在c语言数据结构中实现的那个版本还是有一些区别的。但是换汤不换药,我们可以将符号代入进去验证即可。(STL库中实现的是传类less建大堆,反之小堆,默认建大堆)。

总的来说,这个部分的模拟实现还是非常简单的,只需了解一下适配器模式和仿函数的概念即可轻松手撕这样的容器出来。

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

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

相关文章

【深度学习—李宏毅教程笔记】Transformer

目录 一、序列到序列&#xff08;Seq2Seq&#xff09;模型 1、Seq2Seq基本原理 2、Seq2Seq模型的应用 3、Seq2Seq模型还能做什么&#xff1f; 二、Encoder 三、Decoder 1、Decoder 的输入与输出 2、Decoder 的结构 3、Non-autoregressive Decoder 四、Encoder 和 De…

C++镌刻数据密码的树之铭文:二叉搜索树

文章目录 1.二叉搜索树的概念2.二叉搜索树的实现2.1 二叉搜索树的结构2.2 二叉搜索树的节点寻找2.2.1 非递归2.2.2 递归 2.3 二叉搜索树的插入2.3.1 非递归2.3.2 递归 2.4 二叉搜索树的删除2.4.1 非递归2.4.2 递归 2.5 二叉搜索树的拷贝 3.二叉树的应用希望读者们多多三连支持小…

系统架构设计师:流水线技术相关知识点、记忆卡片、多同类型练习题、答案与解析

流水线记忆要点‌ ‌公式 总时间 (n k - 1)Δt 吞吐率 TP n / 总时间 → 1/Δt&#xff08;max&#xff09; 加速比 S nk / (n k - 1) | 效率 E n / (n k - 1) 关键概念 周期&#xff1a;最长段Δt 冲突‌&#xff1a; ‌数据冲突&#xff08;RAW&#xff09; → 旁路/…

强制重装及验证onnxruntime-gpu是否正确工作

#工作记录 我们经常会遇到明明安装了onnxruntime-gpu或onnxruntime后&#xff0c;无法正常使用的情况。 一、强制重新安装 onnxruntime-gpu 及其依赖 # 强制重新安装 onnxruntime-gpu 及其依赖 pip install --force-reinstall --no-cache-dir onnxruntime-gpu1.18.0 --extra…

桌面我的电脑图标不见了怎么恢复 恢复方法指南

在Windows操作系统中&#xff0c;“我的电脑”或在较新版本中称为“此电脑”的图标&#xff0c;是访问硬盘驱动器、外部存储设备和系统文件的重要入口。然而&#xff0c;有些用户可能会发现桌面上缺少了这个图标&#xff0c;这可能是由于误操作、系统设置更改或是不小心删除造成…

2025.04.20【Lollipop】| Lollipop图绘制命令简介

Customize markers See the different options allowing to customize the marker on top of the stem. Customize stems See the different options allowing to customize the stems. 文章目录 Customize markersCustomize stems Lollipop图简介R语言中的Lollipop图使用ggp…

docker-compose搭建kafka

1、单节点docker-compose.yml version: 3 services:zookeeper:image: zookeeper:3.8container_name: zookeeperports:- "2181:2181"volumes:- ./data/zookeeper:/dataenvironment:ZOO_MY_ID: 1ZOO_MAX_CLIENT_CNXNS: 100kafka:image: bitnami/kafka:3.7container_na…

【问题】一招解决vscode输出和终端不一致的困扰

背景&#xff08;闲话Trae&#xff09; Trae是挺好&#xff0c;用了几天&#xff0c;发现它时不时检查文件&#xff0c;一检测就转悠半天&#xff0c;为此我把当前环境清空&#xff0c;就留一个正在调的程序&#xff0c;结果还照样检测&#xff0c;虽然没影响什么&#xff0c;…

Git,本地上传项目到github

一、Git的安装和下载 https://git-scm.com/ 进入官网&#xff0c;选择合适的版本下载 二、Github仓库创建 点击右上角New新建一个即可 三、本地项目上传 1、进入 要上传的项目目录&#xff0c;右键&#xff0c;选择Git Bash Here&#xff0c;进入终端Git 2、初始化临时仓库…

从零开始配置spark-local模式

1. 环境准备 操作系统&#xff1a;推荐使用 Linux 或 macOS&#xff0c;Windows 也可以&#xff0c;但可能会有一些额外的配置问题。 Java 环境&#xff1a;Spark 需要 Java 环境。确保安装了 JDK 1.8 或更高版本。 检查 Java 版本&#xff1a; bash 复制 java -version 如果…

前端~地图(openlayers)绘制车辆运动轨迹(仿高德)

绘制轨迹路线轨迹路线描边增加起点终点图标绘制仿高德方向箭头模仿车辆动态运动动画 车辆运行轨迹 车辆轨迹经纬度坐标 const linePoints [new Point([123.676031, 43.653421]),new Point([123.824347, 43.697124]),new Point([124.197882, 43.946811]),new Point([124.104498…

分布式之CAP原则:理解分布式系统的核心设计哲学

声明&#xff1a;CAP中的P原则都是需要带着的 在分布式系统的设计与实践中&#xff0c;CAP原则&#xff08;又称CAP定理&#xff09;是开发者必须掌握的核心理论之一。它揭示了分布式系统在一致性&#xff08;Consistency&#xff09;、可用性&#xff08;Availability&#x…

IF=40.8|肿瘤免疫:从免疫基因组学到单细胞分析和人工智能

一、写在前面 今天分享的是发表在《Signal Transduction and Targeted Therapy》上题目为"Technological advances in cancer immunity: from immunogenomics to single-cell analysis and artificial intelligence"的文章。 IF&#xff1a;40.8 DOI:10.1038/s41392…

深入理解 Spring @Bean 注解

在 Spring 框架中,@Bean 注解是用于显式地声明一个或多个 Bean 实例,并将其注册到 Spring 容器中的重要工具。与 @Component 系列注解不同的是,@Bean 是方法级别的注解,通常与 @Configuration 注解结合使用。本文将详细介绍 @Bean 注解的功能、用法及其应用场景。 1. @Bean…

Pycharm 如何删除某个 Python Interpreter

在PyCharm中&#xff0c;点击右下角的“Interpreter Settings”按钮&#xff0c;或者通过菜单栏选择“File” > “Settings”&#xff08;macOS用户选择“PyCharm” > “Preferences”&#xff09;。在设置窗口中&#xff0c;导航到“Project: [Your Project Name]” >…

如何改电脑网络ip地址完整教程

更改电脑的网络IP地址以满足特定的网络需求&#xff0c;本文将为您提供一份详细的步骤指南。其实&#xff0c;改变IP地址并不是一件复杂的事&#xff0c;能解决因为IP限制带来的麻烦。以下是操作指南&#xff1a; 方法一&#xff1a;Windows 系统&#xff0c;通过图形界面修改 …

Oracle--SQL性能优化与提升策略

前言&#xff1a;本博客仅作记录学习使用&#xff0c;部分图片出自网络&#xff0c;如有侵犯您的权益&#xff0c;请联系删除 一、导致性能问题的内在原因 系统性能问题的底层原因主要有三个方面&#xff1a; CPU占用率过高导致资源争用和等待内存使用率过高导致内存不足并需…

【go】什么是Go语言中的GC,作用是什么?调优,sync.Pool优化,逃逸分析演示

Go 语言中的 GC 简介与调优建议 Go语言GC工作原理 对于 Go 而言&#xff0c;Go 的 GC 目前使用的是无分代&#xff08;对象没有代际之分&#xff09;、不整理&#xff08;回收过程中不对对象进行移动与整理&#xff09;、并发&#xff08;与用户代码并发执行&#xff09;的三…

【unity实战】Animator启用root motion根运动动画,实现完美的动画动作匹配

文章目录 前言1、动画分类2、如何使用根位移动画&#xff1f; 一、根位移动画的具体使用1、导入人形模型2、导入动画3、配置动画参数4、配置角色Animator动画状态机5、使用代码控制人物前进后退 二、问题分析三、Humanoid动画中的Root Motion机制及相关配置1、Humanoid动画中的…

中间件--ClickHouse-10--海量数据存储如何抉择ClickHouse和ES?

在Mysql数据存储或性能瓶颈时&#xff0c;采用冷热数据分离的方式通常是一种选择。ClickHouse和Elasticsearch&#xff08;ES&#xff09;是两个常用的组件&#xff0c;但具体使用哪种组件取决于冷数据的存储目的、查询模式和业务需求等方面。 1、核心对比 &#xff08;1&…