C++ stack,queue,priority_queue容器适配器模拟实现

本文解析了容器适配器的概念,包括stack、queue和priority_queue的实现原理,并介绍了仿函数的应用,最后解释了反向迭代器的设计。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

何为容器适配器?

string,vector和list被我们称为容器,容器适配器就是对容器进行一定的转换,使之满足我们需要的特性。拿vector来说,vector支持任意位置的插入与删除,而stack的特性是后进先出,不支持任意位置的插入与删除,而对vector进行封装,使其满足后进先出的特性,此时的vector就被适配成了stack。

三者的适配容器

在这里插入图片描述
在这里插入图片描述

从文档中得知stack是一个容器适配器,stack的模板参数有两个,一个是存储的数据类型,一个是适配的容器类型,deque这个容器的接口比较多,而且调用接口的时间不会太久在这里插入图片描述
支持任意位置的插入删除,还有头尾的插入删除也支持,如果创建stack和queue对象,默认的适配容器就是deque。

deque的底层较为复杂,但它的优点几乎是结合了list和vector两者的优点

list优点:任意位置的插入与删除复杂度为O(1),按需申请释放空间,没有空间的浪费
list缺点:cpu缓存命中率低,不支持随机访问
vector优点:支持随机访问,cpu缓存命中率高,适合尾插尾删
vector缺点:不适合在头和中间修改数据,扩容的开销大,存在一定的空间浪费
deque优点:头尾数据的修改效率高(不用移动数据),支持随机访问,扩容不用拷贝原来的数据,cpu缓存命中率高
deque缺点:不适合修改中间的数据,虽然支持随机访问,但效率比vector低

结合deque的优点:头尾数据的修改效率较高,cpu缓存命中率高,空间浪费小,扩容不用原来的数据。并且stack和queue不用支持遍历操作,用deque适配的stack和queue是最合适。
在这里插入图片描述
优先级队列也是一个容器适配器,但适配容器不是deque而是vector,为何?优先级队列的底层是一个堆,需要随机访问数据,随机访问数据的频率高于头尾数据的修改,而deque的缺点是:不适合随机数据的访问,因为效率比vector低,所以用vector适配priority_queue是最合适的。

stack模拟实现

在这里插入图片描述
stack主要的接口有这些,而这些接口多是复用deque的接口,所以实现起来很简单

namespace myAdaptors
{
	template <class T, class Container = deque<T>>
	class stack
	{
	public:
		// 构造函数不用写,编译器会自动调用Container的构造
		void push(const T& val) // 向栈顶插入数据,就是deque的尾插
		{
			_con.push_back(val);
		}

		void pop() // 删除栈顶原生,deque的尾删
		{
			_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类型,插入删除,取队头元素,判空,取大小

namespace myAdaptors
{
	template <class T, class Container = deque<T>>
	class queue
	{
	public:
		void push(const T& val) // 入队,deque的尾插
		{
			_con.push_back(val);
		}

		void pop() // 出队,deque的头删
		{
			_con.pop_front();
		}

		const T& front() // 取队头数据
		{
			return _con.front();
		}

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

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

测试代码
在这里插入图片描述

priority_queue模拟实现

先贴出hpp文件中的代码,一些函数会详细讲解

namespace myAdaptors
{
	template <class T>
	struct less
	{
		bool operator()(const T& num1, const T& num2) const
		{
			return num1 < num2;
		}
	};

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

	template <class T, class Container = vector<T>, class Compare = less<T>>
	class priority_queue
	{
		Compare _comfunc;
	public:
		priority_queue() :_con() {};

		priority_queue(const priority_queue& x);

		void swap(priority_queue& x) { std::swap(_con, x._con); }

		template <class InputIterator>
		priority_queue(InputIterator first, InputIterator last); // 用迭代器区间构造

		void push(const T& val); //优先级队列的插入,插入数据后需要对其进行调整
		
		void pop(); // 删除堆顶的元素

		void AdjustUp(size_t pos); // 将pos位置的元素向上调整
	
		void AdjustDown(size_t pos);

		size_t size() // 返回vector的大小
		{
			return _con.size();
		}

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

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

template <class T, class Container, class Compare>
void myAdaptors::priority_queue<T, Container, Compare>::push(const T& val)
{
	_con.push_back(val);
	AdjustUp(_con.size() - 1);
}

template <class T, class Container, class Compare>
void myAdaptors::priority_queue<T, Container, Compare>::pop() // 删除堆顶的元素
{
	if (!empty())
	{
		std::swap(_con[0], _con[_con.size() - 1]);
		_con.pop_back();
		AdjustDown(0);
	}
}

template <class T, class Container, class Compare>
void myAdaptors::priority_queue<T, Container, Compare>::AdjustUp(size_t pos) // 将pos位置的元素向上调整
{
	size_t child = pos;
	size_t parent = (child - 1) / 2;
	while (child > 0)
	{
		if (_comfunc(_con[parent], _con[child])) // 父亲比孩子小
		{
			std::swap(_con[parent], _con[child]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}


template <class T, class Container, class Compare>
void myAdaptors::priority_queue<T, Container, Compare>::AdjustDown(size_t pos)
{
	size_t parent = pos;
	size_t child = pos * 2 + 1;
	while (child < _con.size())
	{
		if (child + 1 < _con.size() && _comfunc(_con[child], _con[child + 1]))
		{
			child++;
		}
		if (_comfunc(_con[parent], _con[child]))
		{
			std::swap(_con[parent], _con[child]);
			parent = child;
			child = child * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

template <class T, class Container, class Compare>
template <class InputIterator>
myAdaptors::priority_queue<T, Container, Compare>::priority_queue(InputIterator first, InputIterator last)
{
	while (first != last)
	{
		push(*first);
		first++;
	}
}

template <class T, class Container, class Compare>
myAdaptors::priority_queue<T, Container, Compare>::priority_queue(const priority_queue& x)
{
	priority_queue tmp(x._con.begin(), x._con.end());
	swap(tmp);
}

priority_queue的底层是一个堆(默认是大堆,向建立小堆就要传仿函数greater),要保证数据满足堆的性质,需要在数据插入后对数据进行调整,使数组满足堆的性质。所以priority_queue的插入在调用适配的插入后,还要调用一次调整函数,调整的函数由我们自己写

插入的数据是在数组的尾部,将连续的数组想象成一个大堆,插入的数是子节点,将子节点与父节点进行比较,如果子节点大于父节点,交换两个节点,更新父子节点再进行这样的比较直到子节点为跟节点或者父节点大于子节点退出循环。

template <class T, class Container, class Compare>
void myAdaptors::priority_queue<T, Container, Compare>::AdjustUp(size_t pos) // 将pos位置的元素向上调整
{
	size_t child = pos;
	size_t parent = (child - 1) / 2;
	while (child > 0)
	{
		if (_comfunc(_con[parent], _con[child])) // 父亲比孩子小
		{
			std::swap(_con[parent], _con[child]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

仿函数

但这又涉及到一个知识点:仿函数,也叫函数对象,即像函数一样使用的对象在这里插入图片描述
在类定义之前,定义了两个类less和greater,这两个类对()函数调用符进行了重载,所以这个类生成的对象可以使用()函数调用符来调用重载后的函数。less类对()重载后的函数是一个比较,小于的比较<,左边小返回true,greater对()的重载则是大于的比较>,左边大返回true。

接着定义类priority_queue时,添加一个模板参数Compare,并用less类作为该参数的默认值,在需要进行比较时用Compare模板参数生成一个比较对象_comfunc,该对象能进行小于的比较,但要注意两个参数的顺序,_comfunc(num1, num2)是num1 < num2为真。之后在调整时使用_comfunc对象进行比较(父亲小于孩子进行交换),如果在构造priority_queue时传greater仿函数,建立的就是小堆。

仿函数在sort中的使用

在这里插入图片描述
sort是库中的排序函数,默认是升序,要排降序就要在参数的两个迭代器后传一个仿函数,库中提供的仿函数是less和greater,但这两个仿函数不能比较自定义类型对象,如果要对自定义对象进行比较就要自己写一个仿函数,说明是对自定义类型对象中的什么进行比较(比如图书管理系统,仿函数可以对价格,图书序列号,出版时间等等这些数据进行排序)。

(还有一个点,sort函数需要传仿函数对象,而不是像priority_queue一样传类型名。)

反向迭代器的定义

除了string还有很多的容器如vector,list等等,容器的数量很多,但不需要对每一个容器定义一个反向迭代器,因为容器有正向迭代器,反向迭代器可以根据正向迭代器适配出来,所以将反向迭代器写成一个模板,用参数接收迭代器。

反向迭代器是根据正向迭代器适配出的,所以模板参数要有一个接收迭代器,还有两个接收数据的引用和指针。成员变量只有一个迭代器,构造函数需要传一个迭代器,来初始化迭代器。由于rend返回begin,rbegin返回end,所以迭代器的解引用操作先–,往前走一步再调用正向迭代器的解引用(rbegin是正向的end,不能对end解引用,需要对正向迭代器–再解引用)。然后是重载+±-,反向迭代器的++是正向的–,–是正向的++。

template <class Iterator, class Ref, class Ptr>
	struct reverse_iterator
	{
	// 三个参数,迭代器,数据的引用,数据的指针
		typedef reverse_iterator<Iterator, Ref, Ptr> self;
		Iterator _it;
		reverse_iterator(Iterator it) :_it(it) {}
		Ref operator*() // 返回的是前一个数
		{
			// 迭代器--不要影响自己,构造临时迭代器
			Iterator tmp = _it;
			return *(--tmp);
		}

		Ptr operator->()
		{
			Iterator tmp = _it;
			return &(*(--tmp));
		}

		self& operator--()
		{
			++_it;
			return *this;
		}

		self& operator--(int)
		{
			self tmp = *this;
			_it++;
			return tmp;
		}

		self& operator++()
		{
			--_it;
			return *this;
		}

		self& operator++(int)
		{
			self tmp = *this;
			_it--;
			return tmp;
		}

		bool operator!=(const self& x)
		{
			return _it != x._it;
		}
	};

以list为例定义反向迭代器
在这里插入图片描述
list的hpp文件先引用迭代器的hpp文件,返回重定义迭代器的名字(如果你的非const迭代器重定义后的名字与迭代器模板的名字一样,先重定义const迭代器)在这里插入图片描述
如果非const迭代器先定义,那么定义const迭代器时,使用的reverse_iteraor这个模板名编译器不知道这是模板名还是刚刚定义的类型,使用产生了歧义所以编译器要报错。在这里插入图片描述

template <class T>
typename myList::list<T>::reverse_iterator myList::list<T>::rbegin()
{
	return reverse_iterator(_head);
}

template <class T>
typename myList::list<T>::reverse_iterator myList::list<T>::rend()
{
	return reverse_iterator(_head->_next);
}

template <class T>
typename myList::list<T>::const_reverse_iterator myList::list<T>::rbegin() const
{
	return reverse_const_iterator(_head);
}

template <class T>
typename myList::list<T>::const_reverse_iterator myList::list<T>::rend() const
{
	return reverse_const_iterator(_head->_next);
}

关于list可以看我之前的博客list模拟实现

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值