模拟实现stack queue/dequeue/适配器/优先级队列/仿函数

news2024/10/6 8:35:06

⭐前言:学习C++的STL,我们不仅仅要要求自己能够熟练地使用上层语法,我们还必须要求自己了解其底层原理,不需要了解得太深入,但一定得知道我们写出的各种代码后面,究竟采用了哪种设计思想,为什么要这样设计,有什么好处?这将会帮助我们打下坚实地C++基础,不再是只会表面语法的伪高手!

适配器,也就是适配器模式,它跟迭代器模式一样,属于设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结)的一种,该种模式是将一个类的接口转换成客户希望的另外一个接口。就好比插座的适配器一样。

适配器模式:用现有的东西封装转换出我们想要的东西

迭代器模式:通过封装从而不暴露底层细节,在上层的中按照统一的方式进行使用。

使用适配器模式模拟实现stack

数据结构中的栈,可以通过顺序表,也能通过链表实现,不过大多数情况,都会使用顺序表来实现,但是在STL中,不管哪种方法,我们都可以兼顾,那就是通过类模板即可。

实现stack,很简单,我们只需将vector或list作为适配器,利用适配器里面的接口进行封装转换就能完成stack的模拟实现。

首先先写出基本的框架。对于T,是stack使用的类型,而Container是适配器,就是我们想要stack是适配器是vector还是list,就传进去。而Container的缺省值是deque<T>。

#include<vector>
#include<list>

namespace my_Stack {

	template<class T,class Container = deque<T>>
	class stack
	{
	public:

	private:
		Container _con;
	};

}

接下来就好办了,复用适配器中的接口,实现在栈顶入,栈顶出,取栈顶元素,获取元素个数,判空等待。

①在栈顶插入数据:

		void push(const T& x)
		{
			_con.push_back(x);
		}

②在栈顶出数据:

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

③获取栈顶元素:

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

④判空

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

⑤获取元素个数

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

我们不需要写析构函数,构造函数,拷贝构造函数等待,因为成员函数的类型是适配器推演出来的类型,它会去调用自己本身的构造函数和析构函数!

使用适配器模式模拟实现queue

要实现queue,跟实现stack一样!简单得很。数据结构中的队列,可以通过顺序表,也能通过链表实现,不过大多数情况,都会使用链表来实现,一样地,我们可以通过类模板来兼顾各种情况。

#include<vector>
#include<list>

namespace my_Queue {

	template<class T,class Container = deque<T>>
	class queue
	{
	public:

	private:
		Container _con;
	};

}

跟stack不一样的,无非就是对头出数据,队尾入数据,然后是可以取队头和队尾的数据。

①队尾入:

		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();
		}

了解deque

我们可以发现不管是stack还是queue,它们的适配器的缺省值是deque。使用deque的原因是有以下:

①stack理想的适配器是vector,而vector的缺点是扩容消耗,不支持头插头删和中间插入,因为这样效率很低。

②queue理想的适配器是vector,而list的缺点是不能随机访问。

对于deque,可以兼具vector和list的优点,解决掉缺点。

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

 

deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组,其底层结构如下图所示:

双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象,落在了deque的迭代器身上,其设计结构如下:

从图中我们可以知道,头插尾插的时候,不需要挪动数据。而随机访问,比如每个数组的容量为10,我们要找20,那么就先计算出20是在第几个数组中(用20/10 = 2),然后再算出在这个数组的哪里(找到是在第二个数组后,再20%10 = 0,在第0个下标上)。

deque的缺陷

①与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是比vector高的,但是在中间位置插入,也依然效率低,需要挪动数据。

 

②与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。

 

③但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。

选择deque作为stack和queue的底层默认适配容器的原因:

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不仅效率高,而且内存使用率高

仿函数

仿函数是一个类,它的对象叫做函数对象。函数对象可以像函数一样去使用。仿函数的类要求重载一个函数:operator()()

下面就来实现一下,priority_queue中的仿函数:less和greater。

namespace my_functor
{
	template<class T>
	class less
	{
    public:
		bool operator()(const T& x, const T& y)
		{
			return x < y;
		}
	};


	template<class T>
	class greater
	{
    public:
		bool operator()(const T& x, const T& y)
		{
			return x > y;
		}
	};
}




int main()
{
	my_functor::less<int> lessFunc;

    //这样调用起来,看起来lessFunc是一个函数,但其实它是一个对象
    //因此,类less叫做仿函数,lessFunc叫做函数对象
	lessFunc(1, 2);
	return 0;
}

仿函数的好处

还记得冒泡排序吗,在C语言中,一般情况下,我们写的排序算法,一般都是写死的,即只能用于降序,或用于升序,但这样显示不好,那么有没有办法让冒泡排序可以灵活点呢?站在上层的角度,每次调用冒泡排序的时候,我们可以多传一个参数进去,这个参数可以改变冒泡排序的排序形式!这时候,仿函数闪亮登场!

template <class T,class Compare>//添加泛型和仿函数
void BubbleSort(T* a, int n,Compare com)//多添加一个函数对象
{
	for (int j = 0; j < n; ++j)
	{
		int exchage = 0;
		for (int i = 1; i < n - j; ++i)
		{
			//if (a[a[i] < a[i-1])
			if(com(a[i-1] , a[i])//不需要写死,排序方式交给函数对象即可!
			{
				swap(a[i - 1], a[i]);
				exchage = 1;
			}
		}
		if (exchage == 0)
		{
			break;
		}
	}
}

此时调用的时候,就应该是这样的:

要升序是吧,简单!

	int a[] = { 6,2,5,4,8,9,3,1,7 };
	my_functor::less<int> lessFunc;
	BubbleSort(a, sizeof(a) / sizeof(int), lessFunc);//有名对象
    BubbleSort(a, sizeof(a) / sizeof(int), my_functor::less<int>());//匿名对象

要降序是吧,好说!

	my_functor::greater<int> greaterFunc;
	BubbleSort(a, sizeof(a) / sizeof(int), greaterFunc);

总的来说仿函数的好处就是,可以将函数改造成函数对象,有了这个函数对象,我们可以把它写进类模板里面,为代码程序添加更多功能。

优先级队列priority_queue

优先级队列不是队列,它跟普通的队列不一样,普通的队列是先进先出,但是优先级队列是具有优先级的数据先出。因此优先级队列本质上就是一个堆,它是一种容器适配器(用vector来适配),根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的(即默认是大的数据优先级高,大堆)。

模拟实现优先级队列priority_queueq

优先级队列是一个堆,因此其核心是会建堆,以及调整堆排序。关于堆排序的相关知识,已经在二叉树的这篇文章中有了。二叉树----堆排序。因此这里就不再进行解析。

需要说明的是:因为我们在建堆的时候,可能会需要用到它的迭代器区间构造,因此需要实现一下,这就导致我们需要写一个无参构造,但这个无参构造并不需要写任何东西,因为适配器的对象会调用自己的初始化列表。

需要注意的是,根据文档:优先级队列中less表示的是大堆,即升序,greater表示的小堆,即降序。

namespace my_priority_queue
{
    //less类,仿函数
	template<class T>
	class less
	{
	public:
		bool operator()(const T& x, const T& y)
		{
			return x < y;
		}
	};

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

	template<class T,class Container = vector<T>,class Compare = less<T>>
	class priority_queue
	{
	public:

		//迭代器区间构造,不需要实现迭代器
		template<class InputIterator>
		priority_queue(InputIterator first,InputIterator last)
			:_con(first,last)//调用vector或其它适配器的迭代器区间构造
		{
			//建堆,采用向下调整比较好
			for (int i = (_con.size() - 2) / 2; i >= 0; --i)
			{
				adjust_down(i);
			}
		}
    
        //无参构造,会去调用适配器的初始化列表
		priority_queue()
		{}

        //向下调整
		void adjust_up(size_t child)
		{
			Compare com;
			size_t parent = (child - 1) / 2;
			while (child > 0)
			{
				//less-->x < y -->parent < child  默认大堆 
				if(com(_con[parent],_con[child]))  
				{
					swap(_con[child], _con[parent]);
					child = parent;
					parent = (child - 1) / 2;
				}
				else
				{
					break;
				}
			}
		}

        //向上调整
		void adjust_down(size_t parent)
		{
			Compare com;
			size_t child = parent * 2 + 1;
			while (child < _con.size())
			{

				//默认左孩子大于右孩子  less-->x < y -->child < child+1  
				if (child + 1 < _con.size() && com(_con[child] , _con[child + 1]))
				{	
					child++;
				}
				
				//less-- > x < y-->child < parent
				if (com(_con[parent] , _con[child]))
				{
					swap(_con[parent], _con[child]);
					parent = child;
					child = parent * 2 + 1;
				}
				else
				{
					break;
				}
			}
		}

        //插入数据
		void push(const T& x)
		{
			_con.push_back(x);

			//向上调整
			adjust_up(_con.size() - 1);
		}

        //将堆顶元素pop掉
		void pop()
		{
			swap(_con[0], _con[_con.size() - 1]);
			_con.pop_back();
			//向下调整
			adjust_down(0);
		}

        //取堆顶元素
		const T& top() const
		{
			return _con[0];
		}

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

		size_t size() const
		{
			return _con.size();
		}
	private:
		Container _con;
	};
}

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

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

相关文章

口碑巨制《流浪地球2》,再燃中国科幻电影新高度!

2019年&#xff0c;中国本土科幻电影《流浪地球》以炸裂之势吸引一众目光。上映26天&#xff0c;票房突破45亿&#xff0c;强势开启中国科幻电影的元年。如今时隔4年&#xff0c;《流浪地球2》再度登陆春节档&#xff0c;票房口碑双丰收&#xff0c;上映四天票房破13亿、淘票票…

Android渗透测试12:IDA动态调试so

0x00 前言 上一篇分享了使用 Android studio 和 Jeb 对 Apk 文件直接进行动态调试&#xff0c;本文将分享使用 IDA pro 调试 so 。 调试的 apk 文件还是使用 CTF案例4 的文件&#xff0c;已经上传到知识星球&#xff0c;可自行下载 本文涉及技术&#xff1a; IDA pro 工具使…

论文解读 - 城市自动驾驶车辆运动规划与控制技术综述 (第4部分)

文章目录&#x1f697; IV. Mothon Planning&#xff08;运动规划&#xff09;&#x1f7e2; D. Graph Search Methods&#xff08;图搜索算法&#xff09;&#x1f7e5; 1) Lane Graph&#xff08;车道图&#xff09;&#x1f7e7; 2) Geometric Methods&#xff08;几何方法&…

AtCoder Beginner Contest 287 A-G 赛时思路+正解

一把给我加到1219了&#xff0c;青大小蒟蒻表示很开心。 A - Majority 题意 问你"For""For""For"字符串数量是否比"Against""Against""Against"数量多。 思路 mapmapmap暴力即可。 A题代码 B - Postal Card 题意…

电脑技巧:教你关闭Win11内存压缩,解决电脑卡顿的问题

很多朋友都注意到&#xff0c;Win11默认开启了内存压缩功能。内存压缩顾名思义&#xff0c;可以压缩内存中的数据&#xff0c;让内存占用更少&#xff0c;同时减少Swap频次&#xff0c;带来更高的I/O效率。 但与此同时&#xff0c;压缩数据需要耗费CPU资源&#xff0c;一些朋友…

Dr4g0n-b4ll靶机总结

Dr4g0n-b4ll靶机渗透测试总结 靶机下载地址: https://download.vulnhub.com/dr4g0nb4ll/Dr4g0n-b4ll.zip 打开靶机,使用nmap扫描靶机的ip和所有开放的端口 可以看到靶机开放了80端口和22端口 根据80端口打开网站 信息收集,目录爆破 在robots.txt下发现一串base64编码 eW91IG…

编写循环(RH294)

循环这东西你早就懂的不是么就像python里的for一样在ansible中 使用loop关键字来实现迭代简单循环简单循环中一般使用loop关键字来开始循环使用循环变量item来存储每个迭代过程中使用的值举个例子 栗子啊首先让我们拿出两个任务片段- name: Postfix is runningservice:name: po…

索引15连问

前言 大家好&#xff0c;我是田螺。 金三银四很快就要来啦&#xff0c;准备了索引的15连问&#xff0c;相信大家看完肯定会有帮助的。 公众号&#xff1a;捡田螺的小男孩 1. 索引是什么&#xff1f; 索引是一种能提高数据库查询效率的数据结构。它可以比作一本字典的目录&am…

从C语言的使用转换到C++(下篇)——刷题、竞赛篇

目录 一、CSTL的简介 二、STL的使用详解 2、1 STL之动态数组vector的使用 2、2 STL之集合set的使用 2、3 STL之映射map的使用 2、4 STL之栈stack的使用 2、5 STL之队列queue的使用 2、6 STL之unordered_map和unordered_set的使用 三、总结 标题&#xff1a;从C语言的使用转换…

还不会SpringBoot项目模块分层?来这手把手教你

文章目录前言&#x1f34a;缘由⏲️本文阅读时长&#x1f3af;主要目标&#x1f468;‍&#x1f393;试用人群&#x1f381;快速链接&#x1f369;水图正文&#x1f96b;1.IDEA新建项目&#x1f32d;2.创建子模块-dependencies(依赖层)&#x1f3af;重点&#x1f36a;3.创建子模…

【寒假小练】day2

前言 日积跬步&#xff0c;能至千里。 水平有限&#xff0c;不足之处望请斧正。 选择题 1、以下程序运行后的输出结果是( ) #include <stdio.h> void fun(char **p) {int i;for(i 0; i < 4; i) {printf("%s", p[i]); } int main() {char *s[6] {"…

Python 本地django外部网络访问

目录 一、前提 1、确定在本地可以访问 二、 本地django项目外部网络访问 1、在settings中配置允许所有服务器访问&#xff08;局域网访问&#xff09; 2、Host配置 3、使用内网穿透工具&#xff08;ngrok&#xff09;&#xff08;外部网络访问&#xff09; &#xff08;…

Acwing---1224. 交换瓶子

交换瓶子1.题目2.基本思想3.代码实现1.题目 有 N 个瓶子&#xff0c;编号 1∼N&#xff0c;放在架子上。 比如有 5个瓶子&#xff1a; 2 1 3 5 4 要求每次拿起 2 个瓶子&#xff0c;交换它们的位置。 经过若干次后&#xff0c;使得瓶子的序号为&#xff1a; 1 2 3 4 5 对于这…

【第26天】SQL进阶-查询优化- performance_schema系列实战二:锁问题排查(MDL锁)(SQL 小虚竹)

回城传送–》《32天SQL筑基》 文章目录零、前言一、什么是MDL锁二、什么时候适合加MDL锁三、 实战演练3.1 数据准备&#xff08;如果已有数据可跳过此操作&#xff09;3.2 开启第一个会话&#xff0c;显式开启一个事务&#xff0c;并执行一个update语句不提交3.3 开启第二个会话…

机器自动翻译古文拼音 - 十大宋词 - 水调歌头 明月几时有 苏轼

水调歌头明月几时有 北宋苏轼 明月几时有&#xff0c;把酒问青天。 不知天上宫阙&#xff0c;今夕是何年。 我欲乘风归去&#xff0c;又恐琼楼玉宇&#xff0c;高处不胜寒。 起舞弄清影&#xff0c;何似在人间&#xff1f; 转朱阁&#xff0c;低绮户&#xff0c;照无眠。 不应…

idea 配置tomcat 运行jsp项目

1、复用idea打开jsp项目 2、添加tomcat配置 3、点击后会出现配置框,这里画框的地方都选上&#xff0c;版本选择1.8&#xff0c;其他的信息内容默认后&#xff0c;点击确认 4、点击 File->Project Structure,弹出界面选择Project&#xff0c;这里sdk选择1.8&#xff0c;语言选…

#7反转链表#

反转链表 1题目链接 链接 2思路 思路1(暴力): 定义两个指针或者三个指针 这里选择三个指针 清晰一点 头部 头部的下一个 头部的下一个的下一个 n1 n2 n3 做好n2和n1的连接: n2->nextn1 然后: n2n1 n3n2 n3n3->next 相当于三个指针都往…

JAVA混合使用函数式接口(BiPredicate和Consumer)、泛型、lambda表达式、stream流,优化List求交集和差集后的通用处理

文章目录前言项目场景两个List求交集和差集BiPredicate和Consumer基本介绍优化目标一步步优化代码最后前言 本文主要讲的是一个小的功能代码的优化案例&#xff0c;用到的知识点主要包括函数式接口&#xff08;BiPredicate和Consumer&#xff09;、泛型、lambda表达式、stream…

100天精通Python(数据分析篇)——第73天:Pandas文本数据处理方法之查找、替换、拼接、正则、虚拟变量

文章目录每篇前言一、Python字符串内置方法1. 文本查找2. 文本替换3. 文本拼接4. 正则提取二、Pandas实现文本查找1. str.startswith(字符串)2. str.endswith(字符串)3. str.index(字符串, start0, endlen(string))4. str.rindex(字符串, start0, endlen(string))5. str.find(字…

工具技巧和读文档 | 读函数式编程接口文档 | 匿名内部类 | lambda表达式 |IDEA

Function接口&#xff0c;函数式接口 按入参返回值分类&#xff0c;大概分为4种类型&#xff0c;再加上多个入参就又多了Bi开头的两种。 有CtrlP的时候不懂参数列表该写啥&#xff0c;就先CtrlALT看下入参类型的相关实现类&#xff01; 一些实用的快捷键&#xff1a;Ctrl P看参…