【C++】容器适配器

📕 容器适配器介绍

概念

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

比如 stack 和 queue ,这两个容器是在其他容器的基础上实现的,对其他容器的接口进行封装处理,进而实现stack 和 queue 的功能。从下面 C++ 手册中的截图,可以看出 stack、queue默认是用 deque(双端队列)实现的,它们并不是用数组或者链表实现,而是直接使用已有的容器。

请添加图片描述

deque 的简单介绍

deque,双端队列,既有数组的优点(支持随机访问),又具备链表的优点(插入删除效率高),其底层实现如下图。
首先,有一个指针数组(该数组又被称作中控器),该数组里面存放的元素的数据类型的是指针,每一个指针指向一段连续的空间,每一段空间的大小都是固定的。要访问数据,首先要在 map 数组里面找到数组里的元素(指针),例如 map[4],代表数组里第五个数据,这是一个指针,然后通过指针访问其指向的空间,这块空间里面存储的才是数据。

在这里插入图片描述

那么如何实现迭代器呢?如下图所示,迭代器 iterator 里面有四个指针,分别是 cur,first,last,node。有first 、last 这是因为中控器的每一个元素(指针),指向的空间都是不一样的,必须要设置 first、last 指针来防止越界访问。至于 node 就更好理解了,这是为了只要当前访问的是中控器里的哪一个元素。
在这里插入图片描述

至于其插入数据,也不难理解。双端队列,顾名思义,可以在两端进行插入、删除操作。如下图,在队头插入的时候,直接插入到 start 迭代器的 cur 指针的前一个位置即可,如果 start 迭代器指向的这个数组满了,那么就开辟一块新空间,让中控器(map数组)里面上一个元素(指针)指向这块空间,再将数据插入这一块新空间。尾插同理。(map数组是从中间开始使用的,就是为了方便头插)
在这里插入图片描述
虽然 deque 听起来高大上,兼具链表和顺序表的优点,但是实际应用的时候使用双端队列的情况却并不多,所以简单了解即可。

📕 stack

如下,使用 vector、list ,对其接口进行简单封装(使用到的接口,在 vector 、list 里面都是一同名的),就成了stack。但是为了减少重复冗余,这里将底层容器(vector、list )传模板参数,使用的时候写出来是用 vector 还是 list 就行就行。

#pragma once
#include<iostream>
#include<vector>
#include<list>

using namespace std;

namespace simulate
{
	template<class T, class Container = vector<T>>
	class stack
	{
	public:

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

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

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

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

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

	private:
		Container _con;
	};

}

📕 queue

queue 和 stack 类似,也是使用 list 和 vector 实现,但是 queue 默认是用 list 会更好一点,因为频繁的出队,本质上是在头删,对于 vector 而言,开销较大。


#pragma once
#include<iostream>
#include<vector>
#include<list>

using namespace std;

namespace simulate
{
	template<class T, class Container = list<T>>
	class queue
	{
	public:

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

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

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

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

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

	private:
		Container _con;
	};
}

📕 priority_queue

优先级队列是一种容器适配器,根据一些严格的弱排序标准,专门设计使其第一个元素始终是它包含的最大元素。

简单地说,priority_queue 就是类似于堆,可以有大堆、小堆,用哪个完全取决于自己。利用priority_queue 获取元素的时候,只能获取堆顶元素,也就是优先级队列的“优先级最高”的元素。

可是,如何判别容器里的元素,谁的优先级更高呢?本质上其实还是用大于、小于号去比较,如果 priority_queue< int > ,容器里的元素是 int 类型的,就是根据 int 数据的大小;如果是 priority_queue< string > ,那么容器里的元素就是 stirng 类型的,无疑, string 类型里面是重载了 > 、 < 符号的,所以,只需要直接调用这两个重载即可,至于其内部如何实现,根据什么来判断string 类的对象的大小,我们并不关心。再假设,如果是自定义类型,就要自己实现大于、小于的重载, 根据什么来判断 “优先级” 也就是自己定义了。

如下,以向上调整算法为例,判断优先级那里本质上就是对元素进行大小比较。

		void adjust_up(int child)
		{
			Comapre com;

			int parent = (child - 1) / 2;
			while (child > 0)
			{
				if (_con[parent] < _con[child])   // 判断优先级
				{
					swap(_con[child], _con[parent]);
					child = parent;
					parent = (child - 1) / 2;
				}
				else
				{
					break;
				}
			}
		}

根据堆的存储结构,这里使用 vector 进行封装无疑更加适合。同时,向下调整和向上调整两个算法都需要自己实现,并不困难。(可以参考这篇文章:堆的实现

但是,由于要控制底层是大堆还是小堆,所以又需要传模板参数(默认是大堆),将这个参数叫做 Compare 。这里使用仿函数通过其返回值来判断是实现大堆还是小堆,仿函数本质上是一个类,但是可以像函数一样使用它,这是因为重载了 operator()(const T& x,const T& y) 。实例化出一个对象com之后,可以直接 com(x,y) ,调用了 operator()(const T& x,const T& y) 这个方法。

如下,原本的 _con[parent] < _con[child] ,变成了 com( _con[parent] , _con[child] ) 。而 com 是模板类 Compare 实例化出的一个对象,根据传输的参数,就可以实现大堆还是小堆。

#pragma once
#include<vector>
using namespace std;

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

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

	template<class T, class Container = vector<T>, class Comapre = less<T>>
	class priority_queue
	{
	public:
		void adjust_up(int child)
		{
			Comapre com;

			int parent = (child - 1) / 2;
			while (child > 0)
			{
				//if (_con[parent] < _con[child])
				if (com(_con[parent], _con[child]))
					//if (Comapre()(_con[parent], _con[child]))
				{
					swap(_con[child], _con[parent]);
					child = parent;
					parent = (child - 1) / 2;
				}
				else
				{
					break;
				}
			}
		}

		void adjust_down(int parent)
		{
			size_t child = parent * 2 + 1;
			while (child < _con.size())
			{
				Comapre com;

				//if (child + 1 < _con.size() 
				//	&& _con[child] < _con[child + 1])
				if (child + 1 < _con.size()
					&& com(_con[child], _con[child + 1]))
				{
					++child;
				}

				//if (_con[parent] < _con[child])
				if (com(_con[parent], _con[child]))
				{
					swap(_con[child], _con[parent]);
					parent = child;
					child = parent * 2 + 1;
				}
				else
				{
					break;
				}
			}
		}

		void push(const T& x)
		{
			_con.push_back(x);
			adjust_up(_con.size() - 1);
		}

		void pop()
		{
			swap(_con[0], _con[_con.size() - 1]);
			_con.pop_back();
			adjust_down(0);
		}

		const T& top()
		{
			return _con[0];
		}

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

		bool empty()
		{
			return _con.empty();
		}
	private:
		Container _con;
	};

}

入队,就是把数据放到堆的末尾,然后对其向上调整;出队,就是将堆顶元素和堆尾元素交换,然后将堆顶元素向下调整。这和堆的操作是一摸一样的,因为优先级队列底层就是一个堆。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

努力努力再努力.xx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值