【c++】:“无敌的适配器来咯“栈和队列模拟实现以及优先级队列的模拟实现。

这篇具有很好参考价值的文章主要介绍了【c++】:“无敌的适配器来咯“栈和队列模拟实现以及优先级队列的模拟实现。。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

 文章来源地址https://www.toymoban.com/news/detail-789928.html

 

文章目录

  • 前言
  • 一.栈和队列的模拟实现
  • 二.优先级队列
  • 总结

 


前言

栈的介绍和使用:

1. stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。
2. stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
3. stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作:
empty:判空操作
back:获取尾部元素操作
push_back:尾部插入元素操作
pop_back:尾部删除元素操作
4. 标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque。
 
队列的介绍和使用:
1. 队列是一种容器适配器,专门用于在FIFO上下文(先进先出)中操作,其中从容器一端插入元素,另一端提取元素。
2. 队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列。
3. 底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操作:
empty:检测队列是否为空
size:返回队列中有效元素的个数
front:返回队头元素的引用
back:返回队尾元素的引用
push_back:在队列尾部入队列
pop_front:在队列头部出队列
4. 标准容器类deque和list满足了这些要求。默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque。

 

一、栈和队列的模拟实现

容器适配器:

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

前面我们说过stack和queue是一种容器适配器,同样也是作为容器适配器被实现的,所以我们可以直接使用一个公共的容器来当stack的适配器,适配器是用来转换的,将已有的东西进行转换,比如将vector当我们栈的容器实现栈的功能,我们先看一下库中如何描述栈的:

empty模拟,c++,后端,模板方法模式,visualstudio,数据结构,c++

 可以看到库中是用的deque来当stack的容器,并且stack的接口非常简单,所以我们现在就开始实现:

#include <vector>
#include <iostream>
using namespace std;

namespace sxy
{
	template<class T,class Container = vector<T>>
	class stack
	{
	public:
		void push(const T& x)
		{
			_con.push_back(x);
		}
		void pop()
		{
			_con.pop_back();
		}
		T& top()
		{
			return _con.back();
		}
		const T& top() const
		{
			return _con.back();
		}
		size_t size() const
		{
			return _con.size();
		}
		bool empty() const
		{
			return _con.empty();
		}
	private:
		Container _con;
	};
	void test3()
	{
		stack<int> st;
		st.push(1);
		st.push(2);
		st.push(3);
		st.push(4);
		while (!st.empty())
		{
			cout << st.top() << " ";
			st.pop();
		}
	}
}

 因为我们要使用vector做适配器所以包含了vector的头文件,然后我们所有的函数直接使用容器的公共接口即可,比如vector的push_back等等。在模板中我们用了vector的缺省容器,如果你不想用vector,你也可以使用list作为stack的容器,只需要:stack<int,list<int>> 我们的第一步是创建一个容器的对象_con.

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

 对于push我们直接使用容器的尾插即可。

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

 对于pop直接使用尾删即可,因为栈本来就是后进先出的。

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

 top是返回栈顶元素,容器中的back()接口返回的就是栈顶元素。

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

 size()和empty与上述同理,都使用容器的公共接口就能解决。

队列的模拟实现

我们先看一下库中的实现,同样也是用deque作为queue的容器。

empty模拟,c++,后端,模板方法模式,visualstudio,数据结构,c++

 在这里我们要说明一下,由于vector的头删效率极低,所以vector是不能作为queue的容器的。这里可以用list作为queue的容器,也可以用deque作为queue的容器。第一步是创建一个容器的对象_con.

#include <deque>
#include <iostream>
using namespace std;

namespace sxy
{
	template<class T,class Container = deque<T>>
	class queue
	{
	public:
		void push(const T& x)
		{
			_con.push_back(x);
		}
		void pop()
		{
			_con.pop_front();
		}
		T& front()
		{
			return _con.front();
		}
		const T& front() const
		{
			return _con.front();
		}
		T& back()
		{
			return _con.back();
		}
		const T& back() const
		{
			return _con.back();
		}
		size_t size() const
		{
			return _con.size();
		}
		bool empty() const
		{
			return _con.empty();
		}

	private:
		Container _con;
	};
	void test2()
	{
		queue<int> qe;
		qe.push(1);
		qe.push(2);
		qe.push(3);
		qe.push(4);
		while (!qe.empty())
		{
			cout << qe.front() << " ";
			qe.pop();
		}
	}
}

 队列的实现与栈不同的是队尾插入队头出,所以对于pop接口和top接口而言是不一样的:

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

 队列要头删所以用头删的接口。

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

 这里提供了队头元素的返回和队尾元素的返回,因为库中也是实现的。

empty模拟,c++,后端,模板方法模式,visualstudio,数据结构,c++

 这样栈和队列的模拟实现就完成了,对于栈和队列我们只需要明白适配器的使用,只要学到了这个后面很多的实现都变简单了。

二、优先级队列

什么是优先级队列呢?

1. 优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。
2. 此上下文类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元素)。
3. 优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从特定容器的“尾部”弹出,其称为优先队列的顶部。
4. 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭代器访问,并支持以下操作:
empty():检测容器是否为空
size():返回容器中有效元素个数
front():返回容器中第一个元素的引用
push_back():在容器尾部插入元素
pop_back():删除容器尾部元素
5. 标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector。
6. 需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数make_heap、push_heap和pop_heap来自动完成此操作。
 

下面我们来看看库中的优先级队列:

empty模拟,c++,后端,模板方法模式,visualstudio,数据结构,c++empty模拟,c++,后端,模板方法模式,visualstudio,数据结构,c++ 我们可以看到优先级队列的实现除了容器外还多了一个Compare,这个是什么呢?这个其实是一个仿函数,这里的仿函数是来解决优先级队列是小的优先级高还是大的优先级高,我们默认是大的优先级高也就是大堆。

#include <deque>
using namespace std;
#include <iostream>

namespace sxy
{
	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 = deque<T>,class Compare = less<T>>
	class priority_queue
	{
	public:
		void adjust_up(int child)
		{
			Compare com;
			size_t parent = (child - 1) / 2;
			while (child > 0)
			{
				//if (_con[parent] < _con[child])
				if (com(_con[parent],_con[child]))
				{
					swap(_con[parent], _con[child]);
					child = parent;
					parent = (child - 1) / 2;
				}
				else
				{
					break;
				}
			}
		}
		void adjust_down(int parent)
		{
			Compare com;
			size_t child = 2 * parent + 1;
			while (child < _con.size())
			{
				//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[parent], _con[child]);
					parent = child;
					child = 2 * parent + 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);
		}
		T& top()
		{
			return _con.front();
		}
		const T& top() const 
		{
			return _con.front();
		}
		size_t size() const
		{
			return _con.size();
		}
		bool empty() const
		{
			return _con.empty();
		}
	private:
		Container _con;
	};
	void test1()
	{
		priority_queue<int> pq;
		pq.push(1);
		pq.push(16);
		pq.push(14);
		pq.push(19);
		pq.push(17);
		pq.push(12);
		while (!pq.empty())
		{
			cout << pq.top() << " ";
			pq.pop();
		}
	}
}

第一步是创建一个容器的对象_con. 

 我们一个函数一个函数依次来分析:

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

 首先是push,由于优先级队列本质还是一个队列,所以遵循先进先出的原则。插入的时候在队尾插入即可,这个时候我们不能结束,因为要排优先级,所以我们要将最后插入的这个数依次向前调整。听到这里是不是很熟悉呢?没错!这其实就是我们之前讲过的堆,而这个adjust_up函数也和我们当时实现的向上调整一模一样,所以在这里我们就不在详细的介绍如何调整了,具体的可以去看我的那篇关于堆的文章。

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

 对于一个已经排好序的队列我们是不可以直接出数据的,为了保证数据的有序性我们先讲第一个元素和最后一个元素交换,这样我们就把要删除的第一个队顶元素移到了队尾,然后直接尾删就删掉了原先的队头元素,刚刚我们将最后一个元素换到了对头,所以我们要让对头这个数据依次向下调整来保持有序性。

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

 由于优先级队列只提供对头元素的返回所以我们只用容器的front接口即可。

想要看明白向上调整接口需要先介绍一下仿函数:

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

 我们可以看到仿函数的实现其实就是创建一个类然后里面封装一个()的运算符重载,比如我们的less小于,对于一个数是否小于另一个数我们可以直接写为:Compare com com(x,y),这时候肯定会有人说有什么用呢,用其他方式也能替代。如果这样想那就错了,仿函数作为STL的六大接口之一又怎么会没用呢?下面我们就能见识到仿函数的重要性:

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

 对于向上调整函数的实现我们先创建一个仿函数对象,然后计算孩子节点的父节点,当孩子节点大于0时进入循环,在这里我们是不用判断child>=0的,因为等于0这个孩子就变成所有节点的祖先了还哪有父节点呢?然后我们直接用仿函数判断如果父亲小于孩子(因为默认是大堆)节点就进行向上调整,向上调整就是将父节点和孩子节点交换,然后让孩子节点到以前父节点的位置上,在重新算父节点这样就能依次向上连续调整,一旦发现父节点大于孩子节点满足大堆的条件我们就break即可。在这里如果没有这个仿函数,我们该如何随意控制优先级队列是大堆还是小堆呢?所以仿函数真的很重要。

        void adjust_down(int parent)
		{
			Compare com;
			size_t child = 2 * parent + 1;
			while (child < _con.size())
			{
				//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[parent], _con[child]);
					parent = child;
					child = 2 * parent + 1;
				}
				else
				{
					break;
				}
			}
		}

 向下调整函数是将父节点依次和左右孩子节点相比较,默认是大堆,如果父节点小于孩子节点就交换,由于向下调整的时候我们需要看孩子节点哪个更大,父节点与大的那个孩子节点交换,所以我们先默认孩子节点是左孩子,由于孩子节点是依次向下调整的,所以循环条件为孩子小于size()就进入循环,进入循环后我们首先判断右孩子是否存在,因为一个完全二叉树是有可能没有右孩子的。如果右孩子存在并且左孩子小于右孩子我们就让孩子节点++变成右孩子。当父节点小于孩子节点的时候我们就进行调整,先交换父节点和孩子节点,然后让父节点到刚刚孩子节点的位置上去,再重新计算孩子节点。当父节点不小于孩子节点时就不需要调整直接break即可。

下面我们验证一下我们写的优先级队列:

empty模拟,c++,后端,模板方法模式,visualstudio,数据结构,c++

 因为默认是大堆所以我们看到确实排序完成了,下面我们换个适配器验证一下小堆。

empty模拟,c++,后端,模板方法模式,visualstudio,数据结构,c++

 下面我们简单的介绍一下什么是deque:

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

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

empty模拟,c++,后端,模板方法模式,visualstudio,数据结构,c++

双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其 整体连续 以及随机访问的假象,落 在了 deque 的迭代器身上,因此deque的迭代器设计就比较复杂,如下图所示:
empty模拟,c++,后端,模板方法模式,visualstudio,数据结构,c++

那deque是如何借助其迭代器维护其假想连续的结构呢?看下图:

empty模拟,c++,后端,模板方法模式,visualstudio,数据结构,c++

我们可以看到deque的实现非常的复杂,虽然插入删除比vector方便了不少并且对于空间的扩容也优于vector,但却没有极致的优点,所以只能沦为容器适配器。

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作为其底层容器,主要是因为:
1. stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作。
2. 在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率高。
结合了deque的优点,而完美的避开了其缺陷。

 

总结

本节相对比较简单,因为很多都是我们之前学过的知识,本篇文章的重点在于了解并且会熟悉的使用适配器,我们会发现有了适配器是非常方便的,之前我们用C语言写栈和队列的时候随便就一两百行代码,而用c++实现栈和队列不到60行就能搞定。deque的出现本来是为了替代vector和list的,结果却成为了stack和queue的容器适配器,这是因为deque没有一个极致的优点,不像vector那样可以随意访问元素,也不能像list那样任意位置插入删除的效率高。下一篇要讲的内容是反向迭代器的实现与适配(干货满满哦~)。

 

到了这里,关于【c++】:“无敌的适配器来咯“栈和队列模拟实现以及优先级队列的模拟实现。的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【简化程序设计】C++STL“容器适配器“之栈和队列

    🎉博客主页:小智_x0___0x_ 🎉欢迎关注:👍点赞🙌收藏✍️留言 🎉系列专栏:C++初阶 🎉代码仓库:小智的代码仓库 【本节目标】: stack的介绍和使用 stack的模拟实现 queue的介绍和使用 queue的模拟实现 priority_queue的介绍和使用 priority_queue的模拟实现 容器适配器 deuqe的介绍

    2024年02月15日
    浏览(33)
  • [C++] STL_priority_queue(优先级队列) 的使用及底层的模拟实现,容器适配器,deque的原理介绍

    priority_queue文档介绍 翻译: 1. 优先队列是一种 容器适配器 ,根据严格的弱排序标准, 它的第一个元素总是它所包含的元素中最大的。 2. 此上下文类似于 堆 , 在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元素)。 3. 优先队列被实现为容器适配

    2024年02月04日
    浏览(40)
  • 【C++】手撕 栈 & 队列(适配器)

    目录 一,stack 1,stack的介绍 2,stack 框架 3,push(const T x) 4,pop() 5,top() 6,size() 7,empty() 8,stack 测试 9,源代码 二,queue 1,queue的介绍 2,queue 框架 3,push(const T x) 4,pop() 5,front() 6,back() 7,size() 8,empty() 9,queue 测试 10,源代码 三,总结 1,stack 是一种容器适配器,专门用

    2024年04月15日
    浏览(30)
  • 【C++】STL——反向迭代器的模拟实现:迭代器适配器

    反向迭代器的使用相信大家都已经比较熟悉了,那我们这篇文章具体讲什么呢? 🆗,这篇文章我们重点来讲一下 反向迭代器的模拟实现 。 那为什么我们之前不和正向迭代器放在一块讲呢?为什么要等到我们讲完了容器适配器再来讲反向迭代器的模拟实现呢? 那这个问题我

    2024年02月08日
    浏览(41)
  • C++初阶:容器适配器介绍、stack和queue常用接口详解及模拟实现

    介绍完了list类的相关内容后:C++初阶:适合新手的手撕list(模拟实现list) 接下来进入新的篇章,stack和queue的介绍以及模拟: stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。 stack是作为容器适配器

    2024年02月19日
    浏览(41)
  • 【C++】STL反向迭代器模拟实现,迭代器适配器,迭代器类型简单介绍

    本篇主要讲反向迭代器的模拟实现。 能够加深各位对泛型的理解。 前面我那篇string介绍里面已经提到过反向迭代器是啥了,如果点进来的同学还不知道,可以看看:[string介绍](https://blog.csdn.net/m0_62782700/article/details/130796914? spm=1001.2014.3001.5501) 迭代器,可以在不暴露底层实现细

    2024年02月16日
    浏览(40)
  • 【C++】STL中stack,queue容器适配器的模拟实现(使用deque容器)

    🌏博客主页: 主页 🔖系列专栏: C++ ❤️感谢大家点赞👍收藏⭐评论✍️ 😍期待与大家一起进步! 虽然stack和queue中也可以存放元素,但在STL中并没有将其划分在容器的行列,而是将其称为容器适配器,这是因为stack和队列只是对其他容器的接口进行了包装,STL中stack和

    2024年02月15日
    浏览(48)
  • 【C++航海王:追寻罗杰的编程之路】priority_queue(优先队列) | 容器适配器你知道哪些?

    目录 1 - priority_queue的介绍和使用 1.1 - priority_queue的介绍 1.2 - priority_queue的使用 1.3 - priority_queue的模拟实现 2 - 容器适配器 2.1 - 什么是适配器 2.2 - STL标准库中stack和queue的底层结构 2.3 - deque的介绍 2.3.1 - deque的原理介绍 2.3.2 - deque的缺陷 2.4 - 为什么选择deque作为stack和queue的底

    2024年04月10日
    浏览(43)
  • 深入篇【C++】手搓模拟实现list类(详细剖析底层实现原理)&&模拟实现正反向迭代器【容器适配器模式】

    1.一个模板参数 在模拟实现list之前,我们要理解list中的迭代器是如何实现的。 在vector中迭代器可以看成一个指针,指向vector中的数据。它的解引用会访问到具体的数据本身,++会移动到下一个数据位置上去,这些都是因为vector具有天生的优势:空间上是连续的数组,这样指

    2024年02月15日
    浏览(44)
  • 【C++】STL——容器适配器priority_queue(优先级队列)详解 及 仿函数的介绍和使用

    这篇文章我们接着上一篇的内容,再来学一个STL里的容器适配器—— priority_queue (优先级队列) 1.1 priority_queue的介绍 我们上一篇文章学了 queue (队列),那优先级队列也是在 queue 里面的: 和 queue 一样, priority_queue 也是一个容器适配器,那他和 queue 有什么区别呢?我们一

    2024年02月07日
    浏览(41)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包