【C++详解】STL-stack、queue的模拟实现,容器适配器,deque双端队列介绍

文章目录


一、stack使用

栈在结构上是一种容器适配器,我们看它的第二个模板参数container,默认是从一个容器deque里获取数据,也就是它是复用其他容器适配转换出来的,至于复用的是什么容器由模板参数决定。区别于之前介绍的list、vector,它们的第二个模板参数都是从内存池里获取数据。

容器适配器底层是不支持迭代器的,所以也不支持范围for,(若支持迭代器随意遍历的话就和栈的后进先出的特点相违背)。

栈的使用非常简单,因为具体细节已经在数据结构初阶介绍过了,小编做了几个测试,代码如下:

二、stack容器适配器方式实现

cpp 复制代码
	template<class T, class Container = vector<T>>
	class stack
	{
	public:
		void push(const T& x)
		{
			_con.push_back(x);
		}

		void pop()
		{
			_con.pop_back();
		}

		bool empty() const
		{
			return _con.empty();
		}

		size_t size() const
		{
			return _con.size();
		}

		const T& top() const
		{
			return _con.back();
		}

		T& top()
		{
			return _con.back();
		}
	private:
		Container _con;
	};

库里面是用deque做的缺省值,我们对deque还不了解,所以先用vector来替代一下这里底层适配器用vector list 都可以,因为stack不用头插头删。

三、queue的使用

队列也是一种容器适配器,具体使用也不过多介绍了,测试如下:

四、queue容器适配器方式实现

cpp 复制代码
	template<class T, class Container = list<T>>
	class queue
	{
	public:
		void push(const T& x)
		{
			_con.push_back(x);
		}

		void pop()
		{
			_con.pop_front();
		}

		bool empty() const
		{
			return _con.empty();
		}

		size_t size() const
		{
			return _con.size();
		}

		const T& back() const
		{
			return _con.back();
		}

		T& back()
		{
			return _con.back();
		}

		const T& front() const
		{
			return _con.front();
		}

		T& front()
		{
			return _con.front();
		}
	private:
		Container _con;
	};

要注意queue只能用list做底层容器,因为vector的不支持头插头删,强行头插头删效率太低。

五、数组结构/链式结构的优缺点

在介绍deque之前我们先来总结一下数组结构与链式结构分别的优缺点,然后能对deque的设计有更深层次的理解。


数组和链表正好就是互补的关系,把各自的有优点都发挥到了极致,我们接下来来看deque是如何另辟蹊径的。

六、deque

deque是双端队列,虽然名字里有队列,但是它不保证数据先进先出。

我们知道vector不支持头插头删,list不支持下标的随机访问,而deque它既支持尾插尾删,头插头删,也支持下标的随机访问,那这意味着deque可以完全替代list和vector了吗?当然不是,这个世界是公平的,上帝给你开了一扇窗,必然会关掉你的一扇门,deque也是这样,它具有局限性,虽然它什么都能做,那么就注定了它做的每一项都不会极致。

deque的结构

deque里有一个中控数组,还有许多buffer数组,中控数组里存的是buffer数组的指针。

中控数组满了也要扩容,但是deque相比于数组结构,它的扩容代价就要低一些,因为扩容时只用拷贝指针数据。因为中控数组的连续存储的所以也支持下标的随机访问,并且每一个buffer数组也是连续的,所以缓存命中率也有保证。而且空间不够了也只开辟一个buffer数组,不像数组一样倍数扩容,所以不会造成很严重的空间浪费。乍一看好像很哇塞,但是事实真是如此吗?想知道答案跟着小编来继续学习吧。

deque迭代器

deque的迭代器是和它的结构融合在一起的,它的遍历与插入删除操作都要依赖它的迭代器。

结构: 首先它有两个迭代器:start和finish,每个迭代器有四个指针,cur是最重要的,它指向的是当前迭代器访问到了哪个位置,另外三个指针都是为了维护它而创造的,first指向当前迭代器访问的数据所在的buffer数组的第一个数据,last指向的buffer数组的最后一个数据的下一个位置,node指向当前buffer数组在中控数组的位置,是二级指针。(中控数组里的数据是指针)
所以迭代器解引用 返回的是cur指向的数据,迭代器++ 就是cur++,当把一个buffer遍历完后再cur++就不同了,另外三个指针也要改变,首先node++,把node的值给first,last为first加一个buffer的数据个数,这样就从下一个buffer的第一数据开始继续遍历了。迭代器比较相等 本质是比较cur的值是否相等。
尾插/尾删 :若finish的cur指向数据所在的buffer数组没满,直接尾插,若满了需要到下一个buffer里插入,deue尾插的效率比vector和list都高,因为它扩容不用拷贝旧数据,也不用mallic结点。尾删很好实现,小编就不浪费时间了。
头插/头删 :如果要头插一定需要开一个buffer数组,因为第一个数据一定在buffer数组的第一个位置,这里也解释了为什么我们不从中控数组的开头开始放数据,如果从开头放的话头插不仅要开buffer数组,还要对中控数组扩容。头插要从新开buffer的尾开始,从后往前插,因为deque也是一种线性表,这样数据的逻辑结构才是连续的。
下标访问:我们要访问数据要找到它在第几个buffer和在这个buffer的第几个位置,用/和%就可以计算,比如我们要访问第12个,若每个buffer有8个数据,那么12/8为1代表在第一个buffer,12%8为4代表在第一个buffer的第四个位置。而这里可能会出现一种情况,当第一个buffer数组不满时,也就是我们头插了数据但是没有把第一个buffer数组插满,这是需要把第一个buffer数组补满再计算,比如我们还是访问第12个,而第一个buffer数组插入了两个数据,这是需要12 + (cur - first) 也就是 12 + 6为18,以18来作为/和%的数据。总的来看deque的下标访问由于需要/ %计算,效率会比vector低,但是还是比list高多了。

deque总结

1、适合头尾插入删除,效率很高。

2、下标随机访问效率也不错,但是处理大量数据效率略低于vector。

3、中间位置插入删除效率低,需大量挪动数据,和vector差不多。

4、相比vector(下标随机访问效率高)和list(任意位置插入删除效率高),它的优点都不够极致,但是他在头尾的插入删除效率上做到了极致(vector要扩容拷贝数据,list要频繁申请结点),所以它非常适合作为栈和队列的默认适配容器。

在实际工作中,vector 和 list 交替使用就足以解应付绝大部分场景了,若遇到频繁插入删除和少量下标访问可以用deque。

以上就是小编分享的全部内容了,如果觉得不错还请留下免费的关注和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~

相关推荐
刚入坑的新人编程33 分钟前
暑期算法训练.3
c++·算法
科大饭桶1 小时前
数据结构自学Day8: 堆的排序以及TopK问题
数据结构·c++·算法·leetcode·二叉树·c
minji...1 小时前
数据结构 栈(2)--栈的实现
开发语言·数据结构·c++·算法·链表
zh_xuan1 小时前
c++ 模板元编程
开发语言·c++·算法
猪蹄手1 小时前
单例模式详细讲解
开发语言·c++·单例模式
R-G-B3 小时前
【46】MFC入门到精通——MFC显示实时时间,获取系统当前时间GetCurrentTime()、获取本地时间GetLocalTime()
c++·mfc·mfc显示实时时间·mfc获取系统当前时间·getcurrenttime·getlocaltime
鹅毛在路上了3 小时前
CMake-gdb调试,解决LLVM ERROR: out of memory
c++·gdb·llvm
tomly20203 小时前
【小米训练营】C++方向 实践项目 Android Player
android·开发语言·c++·jni
大专生学编程3 小时前
QT简介和QT环境搭建
c++·qt
feiyangqingyun4 小时前
Qt/C++开发监控GB28181系统/视频点播没有ssrc问题的处理/兼容各种设备和应用场景需求
c++·qt·gb28181