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

news2024/11/18 6:43:55

前言:

我们已经掌握了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.coloradmin.cn/o/1251239.html

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

相关文章

CDA一级备考思维导图

CDA一级备考思维导图 第一章 数据分析概述与职业操守1、数据分析概念、方法论、角色2、数据分析师职业道德与行为准则3、大数据立法、安全、隐私 CDA一级复习备考资料共计七个章节&#xff0c;如需资料&#xff0c;请留言&#xff0c;概览如下图&#xff1a; 第一章 数据分析…

P11 如何写一个C++类 Log日志基础

01 前言 到目前为止&#xff0c;我们学了类 class&#xff0c;本期我们要尝试着从头开始写一个类。 本期不会讲的太深。我们不会写非常复杂的类&#xff0c;我们要会完成一个基本的 log 类 02 为什么使用 log 首先我们先分析一下我们的需求&#xff0c;这个 log 类到底是什么…

安捷伦E4404B频谱分析仪,100 Hz 至 6.7 GHz

E4404B是安捷伦ESA-E系列频谱分析仪&#xff0c;它是一款能够适应未来发展需求的中高端频谱分析仪解决方案。该系列在频谱分析仪的测量速度、动态范围、精度和功率分辨能力等方面&#xff0c;都为类似价位的产品树立了性能标杆。其灵活的平台设计使得研发、制造和现场服务工程师…

HCIA-RS基础-距离矢量路由协议

前言&#xff1a; 动态路由协议根据寻径方式可以分为距离矢量路由协议和链路状态路由协议。本文将详细介绍距离矢量路由协议的原理&#xff0c;并阐述其中一个重要概念——路由环路&#xff0c;同时介绍如何避免路由环路的方法。通过学习本文&#xff0c;您将能够深入理解距离矢…

探秘:性能测试中最常见的陷阱与解决方案!

概述一下性能测试流程&#xff1f; 1.分析性能需求。挑选用户使用最频繁的场景来测试。确定性能指标&#xff0c;比如&#xff1a;事务通过率为100%&#xff0c;TOP99%是5秒&#xff0c;最大并发用户为1000人&#xff0c;CPU和内存的使用率在70%以下2.制定性能测试计划&#x…

Acrel-2000电力监控系统在上海大世界保护修缮工程项目中的应用

摘要&#xff1a;安科瑞生产厂家1876150/-6237黄安南 介绍上海大世界电力监控系统&#xff0c;采用智能电力仪表采集配电现场的各种电参量和开关信号。系统采用现场就地组网的方式&#xff0c;组网后通过现场总线通讯并远传至后台&#xff0c;通过Acrel-2000型电力监控系统实现…

CCC联盟——UWB MAC(二)

在上一篇文章中对CCC联盟UWB MAC框架进行了介绍&#xff0c;在本文中&#xff0c;将MAC层的时间网格进行简单介绍。 2、MAC时间网格&#xff08;Time Grid) DK UWB测距协议属于一对多&#xff08;One to Many, O2M)测距协议。发起者&#xff0c;每次发送4帧&#xff0c;接收N帧…

浅谈如何成为一名优秀教师

你是不是也有一个梦想&#xff0c;想要成为一位优秀的教师&#xff0c;让孩子们如沐春风&#xff0c;收获满满&#xff1f;那么&#xff0c;今天就让我来给你分享一下成为优秀教师的秘诀吧&#xff01; 热爱教育&#xff0c;点燃激情 成为优秀教师&#xff0c;首先要有对教育的…

C语言 - Linux基础使用

Linux 1. Linux简介 Linux是操作系统 Linux中所有的程序与硬件设备对Linux系统而言都是一个文件或文件夹 1.1 Linux发行版本 发型版本的名称/版本有发行方决定 Red Hat Enterprise Linux 5/6/7Suse Linux Enterprise 12Debian Linux 7.8Ubuntu Linux 14.10/15.04 1.2 Red H…

Vue与UserEcharts、DataV的协同

文章目录 引言一、Vue.js简介二、ECharts和UserEcharts1.ECharts简介2.UserEcharts&#xff1a;Vue和ECharts的结合 三、DataV简介四、Vue与DataV的结合1.DataV的Vue插件2.Vue和DataV的数据交互 结论我是将军&#xff0c;我一直都在&#xff0c;。&#xff01; 引言 接着上一篇…

基于协作搜索算法优化概率神经网络PNN的分类预测 - 附代码

基于协作搜索算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于协作搜索算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于协作搜索优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神…

卧室装修干货|榻榻米设计的3要点。福州中宅装饰,福州装修

榻榻米布局 1️⃣传统式布局 在房间中央设置书桌和衣柜&#xff0c;两侧留出走道空间。这种布局适合房间面积适中的房间。 2️⃣靠窗布局 将书桌靠窗放置&#xff0c;衣柜则放在书桌对面&#xff0c;这种布局可以充分利用自然光线&#xff0c;同时也节省空间。 3️⃣高低床…

C语言—二维数组

一、二维数组的创建 int arr[3][4];char arr[3][5];double arr[2][4]; 数组创建&#xff1a;“[ ]”中要给一个常量&#xff0c;不能使用变量 二、二维数组的初始化 int arr[3][4]{1,2,3,4};int arr[3][4]{{1,2},{4,5}};int arr[][4]{{2,3},{4,5}}; 前面的为行&#xff0c…

MySQL 索引相关问题,建议搭建好环境,真实操作一下索引应用到的各种场景

文章目录 什么是 B-tree 和 Btree &#xff1f;B-Tree 和 BTree的区别&#xff1f;MySQL 联合唯一索引是BTree&#xff0c;会带来什么原则&#xff1f;主键索引和单字段唯一索引有什么区别吗什么是 聚簇索引和非聚簇索引 &#xff1f;创建一个三百万数据量的表格&#xff0c;方…

邮政快递查询,邮政快递单号查询,用表格导出查询好的物流信息

批量查询邮政快递单号的物流信息&#xff0c;以表格的形式导出查询好的物流信息。 所需工具&#xff1a; 一个【快递批量查询高手】软件 邮政快递单号若干 操作步骤&#xff1a; 步骤1&#xff1a;运行【快递批量查询高手】软件&#xff0c;并登录 步骤2&#xff1a;点击主界…

GNSS介绍

GNSS介绍 1. GNSS概述2 GNSS工原理3 GNSS的关键技术3.1 RTK技术3.2 惯性导航技术 4 GPS导航电文的格式4.1 第一数据块4.2 第二数据块4.3 第三数据块 5 NMEA语句5.1 GGA5.2 GSA5.3 GSV5.4 RMC5.5 GLL5.6 VTG 6 各导航系统不同频段的工作频率7 LTE&#xff0c;GNSS&#xff0c;WI…

C语言,通过数组实现循环队列

实现循环队列最难的地方就在于如何判空和判满&#xff0c;只要解决了这两点循环队列的设计就没有问题。接下来我们将会使用数组来实现循环队列。 接下来&#xff0c;为了模拟实现一个容量为4的循环队列&#xff0c;我们创建一个容量为4 1 的数组。 接下来我们将会对这个数组…

ACL权限

ACL权限 目录&#xff1a; 1. 什么是ACL 2. 操作步骤 1. 什么是ACL ACL是Access Control List的缩写&#xff0c;即访问控制列表 每个项目成员在有一个自己的项目目录&#xff0c;对自己的目录有完全权限 项目组中的成员对项目目录也有完全权限 其他人对项目目录没有…

Selenium技巧大揭秘:动态数据、分页和Cookie的获取利器

背景&#xff1a; ​ 昨天我们讲了讲关于seleium的一些基础操作&#xff0c;今天讲讲如何将seleium和爬虫结合起来&#xff0c;可以使用selenium获取网页的动态加载数据&#xff0c;可以使用selenium获得cookie&#xff0c;这两个是比较常用的。我将一一展开。 实战案例&…

SAP Smartforms打印报错Error in spool C call : spool overflow

处理方式&#xff1a; SAP打印时提示&#xff1a; Error in spool C call : spool overflow (假脱机请求溢出&#xff0c;通俗一点打印池已满) 解决办法&#xff1a; SE38 首先运行程序RSPO1041 再运行RSPO1043&#xff0c;话不多说上图。