模拟实现STL容器之stack priority_queue以及对deque的介绍

文章目录

  • 前言
  • 1.适配器
  • 2.栈的代码实现
  • 3.deque
    • 1.deque的介绍
    • 2.为什么库中选择deque作为栈和队列的底层容器呢?
  • 4.priority_queue
    • 1.priority_queue介绍
    • 2.模拟实现(仿函数)

前言

本文主将会对栈进行模拟实现,这里实现的侧重点在于C++语言特性,而不是数据结构方面。栈和队列的具体实现早在之前数据结构相关的博客中实现了。这里就不再对队列进行实现了,这里主要以栈为例子介绍一下C++中的适配器,之后会对优先级队列进行模拟实现,同时也会对双端队列和仿函数进行介绍。


1.适配器

在list中我们先实现了正向迭代器,之后用正向迭代器去适配这个反向迭代器。从而将list的迭代器完整实现了,这里有个关键词适配。我们知道栈是的特性是先进后出,我们可以用已经存在容器当作存放栈中数据的载体,通过已有容器的接口去配合栈的特性创造出栈对应的接口。这种做法就是复用已有的代码.这种做法也被称为适配器模式。适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结),该种模式是将一个类的接口转换成客户希望的另外一个接口。

2.栈的代码实现

namespace Ly
{
	template<class T, class Container=vector<T>>
	class  stack
	{
	public:
      void push(const T& x)
	  {
		_con.push_back(x);
	  }

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

我们创建一个类模板,这个模板有两个模板参数。T是用来实例化出栈中存放数据的类型,另一个参数Container是用来实例化出当作栈的容器。模板参数是可以给缺省值的,缺省规则和函数缺省是一样的。我们默认给了一个vector作为默认容器,用于存放栈中的数据。数据插入直接调用对应的容器的尾插即可,删除数据调用对应容器的尾删,其他接口也是类似调用对应容器的接口。栈是不支持遍历的,所以栈没有迭代器。这是根据数据结构的特性决定的。这种处理方式太妙了,我们应该这种学习处理问题的方式。

我们可以同样的方式实现队列,这里就不做演示了。


3.deque

1.deque的介绍

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

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

模拟实现STL容器之stack priority_queue以及对deque的介绍_第1张图片

deque如果要提高头尾插入删除数据的性能,那就得尽量往往中间存储空间地址,这样的话随机访问插入删除数据的性能就会下降,返之亦然。我们知道数组顺序表最大的优点是支持随机访问,头尾删除插入数据效率高。但是中间插入删除效率底。链表是任意位置插入删除效率高,但是不支持随机访问。deque本来是想兼容两者的优点,但是实际上它的性能很尴尬,属于不上不下。它随机访问的效率没有vector高,它任意位置的插入删除效率也没有list高。但是它比vector的中间插入删除数据效率高,比list的随机访问效率高。

但其实deque也是有优势的。与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是必vector高的。 与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。

deque扩容效率高是因为中控器满了以后再开辟一块空间存储存储空间地址就行了,地址不是4字节就是8字节占据的空间很小。同时再扩容的时候拷贝的数据是地址,vector中如果存储的数据都是自定义类型每次扩容拷贝数据都是一个不小的消耗。

模拟实现STL容器之stack priority_queue以及对deque的介绍_第2张图片

2.为什么库中选择deque作为栈和队列的底层容器呢?

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的优点,而完美的避开了其缺陷。

4.priority_queue

1.priority_queue介绍

priority_queue,被称为优先级队列。听名字很唬人,但是本质上它是一个堆。优先级队列也是适配出来的容器,默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆。所有需要用到堆的位置,都可以考虑使用priority_queue。注意:默认情况下priority_queue是大堆。

2.模拟实现(仿函数)

namespace Ly
{  template<class T>
	class less
	{
	 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:
		compare com;
		//向上调整
		void AdjustUp(int child)
		{
			int parents =  (child - 1) / 2;
			while (child>0)
			{
				if (com(_con[parents], _con[child]))
				 {
					swap(_con[parents],_con[child]);
					child=parents;
					parents = (child - 1) / 2;
				 }
				else
				{
					break;
				}
			}
		}
		//向下调整
		void AdjustDown(int parents)
		{
			int child = parents*2+1;
			while (child < _con.size())
			{
				if (child + 1 < _con.size() &&
				 com(_con[child] , _con[child + 1]))
				{
					child++;
				}
				if (com(_con[parents], _con[child]))
				{
					swap(_con[parents], _con[child]);
					parents = child;
					child = 2 * parents;
				}
				else
				{
					break;
				}
			}
		}
		void pop()
		{
			swap(_con[0], _con[_con.size() - 1]);
			_con.pop_back();
			AdjustDown(0);
		}
		void push(const T& val)
		{
			_con.push_back(val);
			AdjustUp(_con.size()-1);
	    }
		const T& top()
		{
			return _con[0];
		}
		bool empty()
		{
			return _con.empty();
		}
		int size()
		{
			return _con.size();
		}
	private:
		Container _con;
	};
}

模拟实现STL容器之stack priority_queue以及对deque的介绍_第3张图片

这里建堆用的向上向下调整就不多说了,适配器之前也介绍过了。这里的重点放在第三个模板参数上。我们知道根据大小关系可以建立起大堆或者小堆。stl库中引入了仿函数,可以由用户自己决定建立的堆是大堆还是小堆。这第3个参数就是用来实例化仿函数的。仿函数不是一个函数是一个类,这个类中重载了()用来比较大小关系。.库中是less重载了<默认建立大堆,greater重载了>是用来建立小堆的。这点要记清楚,这是库中规定好了的。其实这个仿函数有点想C语言中的回调函数中的函数指针,比如qsort中自己写比较写函数。但是C++中觉得C语言中函数指针方式不好用,就摒弃了函数指针,而采用仿函数。在向下或向上调整的函数中涉及到要比较大小,都使用这个less或者greater对象,这样就能由用户自己决定建大堆还是小堆了。

模拟实现STL容器之stack priority_queue以及对deque的介绍_第4张图片
模拟实现STL容器之stack priority_queue以及对deque的介绍_第5张图片
模拟实现STL容器之stack priority_queue以及对deque的介绍_第6张图片

这里再次强调一下库中priority_queue默认是建大堆 less是建大堆重载<, greater是建立小堆重载>。

本文主要的重点是引出了对适配器的介绍这是一种很棒的代码设计方式,值得我们学习。之后介绍了eque和仿函数以及优先级队列,以上内容如有问题,欢迎指正!

你可能感兴趣的:(C++,c++,开发语言,学习,栈,队列)