C++入门第九篇---Stack和Queue模拟实现,优先级队列

前言:

我们已经掌握了string vector list三种最基本的数据容器模板,而对于数据结构的内容来说,其余的数据结构容器基本都是这三种容器的延申和扩展,在他们的基础上扩展出更多功能和用法,今天我们便来模拟实现一下C++库中的栈和队列以及优先级队列。

1.适配器:

让我们打开stack模板库的介绍界面,我们会看到这样一个东西:
在这里插入图片描述
stack的模板中的这个Container是什么呢?没错,在这里我们将其称之为适配器,它的作用是,为我们的的栈或者其他数据结构自动匹配底层的容器模板库,如下:

template<class T, class Container = deque<T>>
class stack
{
private:Container _con;
};
int main()
{hbw::stack<int,list<int>> a1;
}

你会发现,我们的私有的成员直接就是适配器类型的,这样当我们的主程序让其适配器为list类型的时候,适配器会去判断当前的stack的功能函数能否复用到list的成员函数中,倘若可以,我们就可以通过list的底层模板函数功能直接实现出stack的功能。
因此,只要我们的容器的底层容器的功能能够匹配我们当前容器的功能,适配器就可以自动适配到对应的容器中,这样的复用大大节省了效率,让我们在开发时不用再去自己构建复杂的函数和对应的容器即可实现,或许我这样说,你还没法理解,我们接下来的栈和队列的模拟实现的代码,你通过去与我C语言实现的栈和队列去对比即可明白。

deque容器:

依旧是拿出来我们的stack的模板参数,你会发现,它在适配器参数上给了个缺省值deque,根据刚才的是适配器的知识点我们知道,这里的container指代的是一个容器类型,故我们的deque也一定是一个容器类型,因此,我们可以先猜测一下,这个deque为何让它作为缺省参数呢?
根据之前的C语言栈的队列篇我曾经说到,栈和队列都是可以同时使用数组和链表来实现的,对应到C++里,也就是说vector或者list都可以作为stack的底层,因此,作为缺省值,这个deque理论上应当具备两者都有的功能,如下:
在这里插入图片描述
没错,从它的成员函数来看,它既可以和vector一样去利用[]访问下标,也可以实现list的头删头插,访问头尾的功能,因此,在这里让deque作为缺省值,确实是合适不过的,但是,它是如何同时具备两种数据结构的特点的呢?下面让我们一起来分析一下它的容器构建原理:

deque容器的介绍:

deque被称为双端队列。虽然叫它队列,但实际上它并不是队列,也就是说它不是仅仅可以尾插头删,只不过叫这个名字,这个首先要明确,别搞混。它是一种从中间向两边延申的结构,在它的模板中,我们看到了deque使用了内存池allocator来存储数据:
在这里插入图片描述
或许说到这里,我们大致猜出来deque的大体模型是怎样的了,在我看来更类似即可几个数组通过某种方式拼接,让这些数组在逻辑上是连续的,deque的具体构造如下图:
在这里插入图片描述

我们的deque支持两个容器的功能,可以说它的功能是全面的,而我们也知道它使用了内存池allocator,这个内存池的就是我们上图中的BUFF子数组,每一个BUFF数组的长度都是统一的,这样方便我们的下标访问执行。
它实现功能大致如下:
1.尾插:
则在最后一个数组之后再开辟一个新的BUFF,将尾插数据放在这个数组的第一位
2.头插:
在第一个BUFF之前再开辟一个BUFF,将头插数据放在这个数组的最后一位
我们发现,这样的处理方式是没有扩容的消耗的,也不需要挪动数据,很高效
3.之间插入:
中间插入时,我们只有两种办法:
1.BUFF进行扩容/控制数据个数
2.局部整体挪动
这两种方法都可以,但是都有各自的缺点,比如,如果我们对BUFF进行扩容,就会影响我们的[]下标访问,根据deque的结构我们可以总结出,deque下标的计算公式是:x=i/10+i%10,当然,这里是我们假设我们的BUFF都为10的情况下,因此,首先/10确定对应的元素在哪个BUFF子数列里,然后%10确定它在这个子数列的第几个,这样,我们就可以精确的锁定位置,从而让下标访问生效。所以,一旦我们去修改BUFF的长度,就会导致我们这个公式直接无效,十分影响[]访问,但倘若挪动数据,则又会消耗大量的时间,因此,两种方案各有取舍,我们可以根据库STL去看看官方是如何实现的。
我们的deque只涉及到中控数组的扩容问题。但是,由于存储的数据是指针,故只要进行浅拷贝即可,因此,它扩容的消耗并不大。
综上,虽然deque兼具vector和list的双重特点,但它却没有将自己的特性优化到极致,我们可以说deque在头插和尾插方面确实有很大的优势,但是它的下标访问和中间的增删都不如vector和list,具体的deque的实现如下:
在这里插入图片描述
它一共有四个指针去控制整个结构,其中cur用来指针的实时位置,first指向一个BUFF的头,last指向这个BUFF的尾部,而node则用来控制中控数组的指针,当cur==last的时候,node自动向下移动一位,从而实现了下标的连续访问。

2.stack模拟实现:

有了前面的知识铺垫,我们已经不需要怎样去解释实现的细节,直接按照代码理解即可

namespace hbw
{template<class T, class Container = deque<T>>//这里通过这个模板参数控制底层的容器是哪个类型,是谁,这个Container即为适配器,不管底层容器是谁,它都可以适配一个后进先出的栈,如果适配器不适用(比如对应的底层的容器不支持上层的功能),则编译器会报错class stack//我们在这里加上了一个缺省参数,deque容器,这就符合了我们倘若不传对应的数据类型,编译器就会为我们自动适配deque容器,deque容器是一个功能很全面的容器,虽然效率上不够极致,但是泛用性强,故用在这里可以支持list和vector两者的全部功能,从而有了作为缺省值的条件{public:void push(const T& x){_con.push_back(x);}void pop(){_con.pop_back();}const T& top(){return _con.back();}bool empty(){return _con.empty();}size_t size(){return _con.size();}private:Container _con;};
}

3.queue模拟实现:

namespace hbw
{template<class T, class Container = deque<T>>class queue{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();}size_t size(){return _con.size();}private:Container _con;};
}

他们两个的本质都是复用,复用底层的函数封装成自己的函数功能,这就是适配器的优点所在,不过,值得注意的是,适配器只能转换为符合当前功能的底层容器,对于不符合功能的容器,编译器是没法通过的

4.优先级队列priority_queue容器:

何为优先级队列,即是一个优先按照升序或者降序存储输出值的容器,我们可以先看一下它的一些模板功能:
在这里插入图片描述
没错,看到它的函数功能,它可以返回顶部的元素,又结合它按照顺序输出数的特性,我们不难看出,它很像我们曾经模拟实现的一个数据结构-堆。因此,它的默认底层容器为vector,也就死用数组作为默认的缺省底层容器
没错,虽然叫它优先级队列,但实际上,它的本质就是一个升序或者降序的堆,既然是堆,我们就可以复用我们之前学过的堆的各种接口,故它的模拟实现如下:
首先,我们依旧使用适配器来作为priority_ queue的底层如下:

  template<class T,class Container=vector<T>,class Comapre=Less<T>>private:Container _con;};

1.push函数:

	void Adjustup(int child)//向上调整{Compare com;int parent = (child-1)/ 2;while (child > 0){if (com(_con[parent],_con[child])){std::swap(_con[child], _con[parent]);child = parent;parent = (parent - 1) / 2;}else{break;}}}
void push(const T& x)
{_con.push_back(x);Adjustup(_con.size()-1);
}

push函数,我们的基本思路就是,将任意数据放入我们的底层容器后,将其向上调整到对应的位置,保证我们的堆的数据之间的关系不会乱,这里的向上调整的函数之前实现过,在这里我们可以复习一遍。

2.pop函数:

void Adjustdown(int parent)//向下调整
{Compare com;int child = 2 * parent + 1;while(child<_con.size()){if (child + 1 < _con.size() &&com(_con[child],_con[child+1])){child++;}if(com(_con[parent],_con[child])){std::swap(_con[child], _con[parent]);parent = child;child = 2 * child + 1;}else{break;}}
}void pop(){std::swap(_con[0],_con[_con.size()-1]);_con.pop_back();//尾删一个Adjustdown(0);}

对于删除来说,我们一定是删除头数据比较有价值,故我们采取的方式是,首先让头尾数据交换位置,然后将尾部数据删除,然后将头数据向下调整到正确的位置,保证堆的数据大小关系不会错误。

3.其余的接口

const T& top()
{return _con[0];
}bool empty()
{return _con.empty();
}size_t size()
{return _con.size();
}T& operator[](size_t i)
{return _con[i];
}

都是一些很简单的接口,我在这里不多解释,读代码就应该能看懂。

4.仿函数(关键!!!!):

在这里插入图片描述
在priority_queue中,我们看到了这样的一个模板参数,compare,它的默认参数值给了一个less,经过尝试,我们知道,这个less实际上就是构建大堆的意思,但是,在这里的这个Compare是什么意思呢?这就是我们要说的仿函数.
那什么是仿函数呢?我们先看一个例子:

template<class T>
class Less//仿函数less
{
public:bool operator()(T& x, T& y){return x < y;}
};template<class T>
class Greater//仿函数greater
{
public:bool operator()(T& x, T& y){return x > y;}
};

仿函数的本质就是一个只封装了一个()运算符重载的函数的类,当我们使用的时候,直接在类名后面带上括号即可调用这个函数,导致我们看到它的形式就类似一个函数调用,但本质上它依旧是一个类,所以称它为仿函数。如下:

void Adjustdown(int parent)//向下调整
{Compare com;int child = 2 * parent + 1;while(child<_con.size()){if (child + 1 < _con.size() &&com(_con[child],_con[child+1])){child++;}if(com(_con[parent],_con[child])){std::swap(_con[child], _con[parent]);parent = child;child = 2 * child + 1;}else{break;}}
}

当我们看到这个compare com时,这个com就是仿函数类,后续比较的时候,直接利用仿函数传入参数就可以直接进行比较,比如这里的com(_con[child],_con[child+1]。

仿函数的优点和用处:

有了仿函数,我们就可以像预处理那样对一些运算方法进行复用和小成本的修改,比如我们想从建立大堆变成建立小堆,就像上面一样,分别写一个大堆一个小堆两个仿函数,想使用哪个直接在模板参数里实例化即可,这样提高了效率。
但是仿函数的用处更多的在于它替换了函数指针,我们不用在写繁杂的函数指针参数去使用回调函数,不仅难写而且易错,而是利用仿函数,调用即可,其实本质上仿函数和回调函数的用处一样的,但是仿函数更加好用和简单。
一般仿函数都写成模板类,让其可以针对任意类型进行函数使用,让其运用的场景更加广泛。

5.构造函数:迭代器数据传入构造建堆

priority_queue()//写一个默认无参的构造函数,让编译器自己生成默认的构造函数构成重载
{}template<class InputIterator>
priority_queue(InputIterator first, InputIterator end)//利用区间进行构造函数:_con(first,end)//首先利用vector可以区间构造的特点,先把数据放入到vector容器中
{int i = 0;for (i = (_con.size() - 2) / 2; i >= 0; i--){Adjustdown(i);}
}

priority_queue支持传入迭代器区间去建堆,其构造的特点就是首先利用底层容器的vector支持迭代器区间构建数组的特点先初始化_con,然后利用向下建堆的方法,从而建立一个堆,但是写下这个构造函数之后,我们的默认构造函数就没有了,也就是说,后续的构造函数都得传区间,这个是不一定,故我们再写一个默认无参或者全缺省的构造函数,这个就是由编译器自动生成的构造函数,由此,我们就可以同时支持区间迭代器构造和默认构造了。

总结:

以上便是我们的stack queue 优先级队列的基本内容,到这里,我们基本已经掌握了STL库的基本容器模板,但我还是要强调的一点,我已经反复强调了,模拟实现模板的目的是让我们更好的去使用模板,从C语言的思维中走出来,尝试利用C++的思维去解题和分析,熟练的利用模板去简化代码和提高开发效率。同时,模拟实现的过程中我们也学到了如迭代器,迭代器自定义封装,内存池,如何忽略空格任意字符识别,如何扩容,迭代器失效,仿函数等一系列更加重要的属于C++的知识点,我认为这些才是关键,因此,我们应该要抓住我们的重点去学习,在我看来,这是最关键的。

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

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

相关文章

superset 后端增加注册接口

好烦啊-- &#xff1a;< 1.先定义modes: superset\superset\models\user.py # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information…

Tars框架 Tars-Go 学习

Tars 框架安装 网上安装教程比较多&#xff0c;官方可以参数这个 TARS官方文档 (tarsyun.com) 本文主要介绍部署应用。 安装完成后Tars 界面 增加应用amc 部署申请 amc.GoTestServer.GoTestObj 名称不知道的可以参考自己创建的app config 点击刷新可以看到自己部署的应用 服…

【阿里云服务器】2023安装宝塔面板8.0.4

文章目录 前言安装宝塔远程链接服务器输入安装宝塔命令放行宝塔端口 一键安装环境附录重装系统Linux系统卸载宝塔方式一方式二 遇见的问题 前言 镜像是CentOS 7.9.4 安装宝塔 远程链接服务器 输入安装宝塔命令 yum install -y wget && wget -O install.sh https://…

2023年亚太杯数学建模A题水果采摘机器人的图像识别功能(基于yolov5的苹果分割)

注&#xff1a;.题中附录并没有给出苹果的标签集&#xff0c;所以需要我们自己通过前4问得到训练的标签集&#xff0c;采用的是yolov5 7.0 版本&#xff0c;该版本带分割功能 一&#xff1a;关于数据集的制作&#xff1a; clc; close all; clear; %-----这个是生成yolov5 数据…

Linux应用开发基础知识——I2C应用编程(十三)

一、无需编写驱动程序即可访问 I2C 设备 APP 访问硬件肯定是需要驱动程序的&#xff0c;对于 I2C 设备&#xff0c;内核提供了驱动程序 drivers/i2c/i2c-dev.c&#xff0c;通过它可以直接使用下面的 I2C 控制器驱动程序来访问 I2C 设备。 i2c-tools 是一套好用的工具&#xff0…

H5(uniapp)中使用echarts

1,安装echarts npm install echarts 2&#xff0c;具体页面 <template><view class"container notice-list"><view><view class"aa" id"main" style"width: 500px; height: 400px;"></view></v…

SQLite 和 SQLiteDatabase 的使用

实验七&#xff1a;SQLite 和 SQLiteDatabase 的使用 7.1 实验目的 本次实验的目的是让大家熟悉 Android 中对数据库进行操作的相关的接口、类等。SQLiteDatabase 这个是在 android 中数据库操作使用最频繁的一个类。通过它可以实现数据库的创建或打开、创建表、插入数据、删…

【MySQL】索引与事务

&#x1f451;专栏内容&#xff1a;MySQL⛪个人主页&#xff1a;子夜的星的主页&#x1f495;座右铭&#xff1a;前路未远&#xff0c;步履不停 目录 一、索引1、使用场景2、使用索引创建索引查看索引删除索引 3、底层数据结构&#xff08;非常重要&#xff09; 二、事务1、概念…

Android设计模式--享元模式

水不激不跃&#xff0c;人不激不奋 一&#xff0c;定义 使用共享对象可有效地支持大量的细粒度的对象 享元模式是对象池的一种实现&#xff0c;用来尽可能减少内存使用量&#xff0c;它适合用于可能存在大量重复对象的场景&#xff0c;来缓存可共享的对象&#xff0c;达到对象…

Qt项目打包发布超详细教程

https://blog.csdn.net/qq_45491628/article/details/129091320

HTML网站稳定性状态监控平台源码

这是一款网站稳定性状态监控平台源码&#xff0c;它基于UptimeRobot接口进行开发。当您的网站遇到故障时&#xff0c;该平台能够通过邮件或短信通知您。下面是对安装过程的详细说明&#xff1a; 安装步骤 将源码上传至您的主机或服务器&#xff0c;并进行解压操作。 在Uptim…

自动化测试中几种常见验证码的处理方式及如何实现?

UI自动化测试时&#xff0c;需要对验证码进行识别处理&#xff0c;有很多方式&#xff0c;每种方式都有自己的特点&#xff0c;以下是一些常用处理方法&#xff0c;仅供参考。 1 去掉验证码 从自动化的本质上来讲&#xff0c;主要是提升测试效率等&#xff0c;但是为了去研究验…

【点云surface】 修剪B样条曲线拟合

1 介绍 Fitting trimmed B-splines&#xff08;修剪B样条曲线拟合&#xff09;是一种用于对给定的点云数据进行曲线拟合的算法。该算法使用B样条曲线模型来逼近给定的点云数据&#xff0c;并通过对模型进行修剪来提高拟合的精度和准确性。 B样条曲线是一种常用的曲线表示方法…

【element优化经验】el-dialog修改title样式

目录 前言 解决之路 1.把默认的这个图标隐藏&#xff0c;官方的api有这个属性&#xff1a;showClose值设置false. 2.title插槽定制&#xff1a;左边定制标题&#xff0c;右边定制按钮区域。 3.背景颜色修改&#xff1a;默认title是有padding的需要把它重写调&#xff0c;然…

基于 STM32Cube.AI 的嵌入式人脸识别算法实现

本文介绍了如何使用 STM32Cube.AI 工具开发嵌入式人脸识别算法。首先&#xff0c;我们将简要介绍 STM32Cube.AI 工具和 STM32F系列单片机的特点。接下来&#xff0c;我们将详细讨论如何使用 STM32Cube.AI 工具链和相关库来进行人脸识别算法的开发和优化。最后&#xff0c;我们提…

Netty实现websocket且实现url传参的两种方式(源码分析)

1、先构建基本的netty框架 再下面的代码中我构建了一个最基本的netty实现websocket的框架&#xff0c;其他个性化部分再自行添加。 Slf4j public class TeacherServer {public void teacherStart(int port) throws InterruptedException {NioEventLoopGroup boss new NioEve…

Day40力扣打卡

打卡记录 包子凑数&#xff08;裴蜀定理 DP&#xff09; 根据裴蜀定理&#xff0c;存在 c gcd(a, b) 使不定方程ax by c满足条件&#xff0c;如果gcd(a, b) 1即a与b互素的情况下&#xff0c;就会 ax by 1&#xff0c;由于为1可以构造后面的无穷数字&#xff0c;故得到结…

Centos7 离线安装 CDH7.1.7

1. 安装CDH的准备工作&#xff08;所有节点都要执行&#xff09; 1.1 准备环境 角色 IP k8s-master 192.168.181.129 k8s-node1 192.168.181.130 k8s-node2 192.168.181.131 1.2 安装JDK # https://www.oracle.com/java/technologies/downloads/#java11 wget rpm -ivh…

亚马逊Listing怎么写!亲身经验分享

亚马逊运营的重要环节之一&#xff0c;listing的攥写&#xff0c;可以决定了产品的搜索排名&#xff0c;用户的点击率和转化率&#xff0c;那么如果你的产品排名或者转化不理想的情况&#xff0c;可以考虑对listing进行优化&#xff0c;在关键词过多和语句流程通顺的情况下&…

js获取时间日期

目录 Date 对象 1. 获取当前时间 2. 获取特定日期时间 Date 对象的方法 1. 获取各种日期时间组件 2. 获取星期几 3. 获取时间戳 格式化日期时间 1. 使用 toLocaleString() 方法 2. 使用第三方库 UNIX 时间戳 内部表示 时区 Date 对象 JavaScript中内置的 Date 对象…