C++: 优先级队列的模拟实现和deque

目录

 一、优先级队列

 1.1优先级队列 priority_queue介绍

1.2优先级队列的使用

1.3priority_queue的模拟实现

二、deque

2.1deque介绍

2.2deque的优缺点

2.3为什么选择deque作为stack和queue的底层默认容器


 一、优先级队列

 1.1优先级队列 priority_queue介绍

 1.11 优先级队列是一种容器适配器,根据严格的弱排序,它的第一个元素总是它所有元素中最大  的。

适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结),这种设计模式将一个类的接口转换成客户希望的另一个接口。比方说,在stack里面,把push接口转换成调用另一个容器的push_back,这个容器的类放在私有里,是根据初始化来定的,你希望这个底层容器是vector,那么在调用stack的push时,实际调用的就是vector的push_back进行插入元素操作。stack只是一个壳子,vector才是存储数据的底层,只是取数据和其他操作,得按照stack的特点进行。

容器适配器:虽然stack和queue也可以存放元素,但在STL中并没有将其划分在容器的行列,而是将其称之为容器适配器,因为stack和queue只是对其他容器进行了包装,STL中stack和queue默认底层容器使用deque。

deque后文介绍。

1.12 此上下文环境类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元素)

1.13 优先队列被实现为容器适配器,容器适配器就是将特定容器封装作为它的底层容器,queue提供一组特定的成员函数来访问其元素。元素从特定容器的“尾部”弹出,其称之为优先队列的顶部。

1.14 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。底层容器应该可以通过随机访问迭代器访问,并支持以下操作:

empty(): 检测容器是否为空

size(): 返回容器的元素个数

front(): 返回容器的第一个元素

push_back(): 尾插一个元素进容器

pop_back(): 尾删一个元素

1.15 标准容器类vector和deque满足这些要求,如果没有指定特定容器类实例化priority_queue,一般使用vector作为其底层容器。

1.16 需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数make_heap、push_heap和pop_heap来自动完成此操作。

1.2优先级队列的使用

优先级队列默认使用vector作为其底层存储结构的容器,在vector上又加了堆算法,令vector成为堆结构。因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。注意:默认情况下priority_queue是大堆。

priority_queue常用的函数和接口说明                                                                          

函数接口接口说明
priority_queue()/priority_queue(first,last)构造函数,第一个是构造一个空的优先级队列,第二个是用迭代器指向的区间进行构造
empty()队列是否是空
push(x)在优先级队列中插入一个元素x
pop()删除优先级队列中的堆顶元素
top()返回优先级队列中的堆顶元素

1.3priority_queue的模拟实现

为了防止前面数据结构的建堆都忘了,代码我会注释一下的。

#pragma once
#include <iostream>
using namespace std;
#include <assert.h>
#include <vector>
#include <list>

namespace ting
{
    //创建一个仿函数,为元素的大小比较提供
	template<class T>
	struct less
	{
        //()操作符重载,这样less实例化的对象(如compare)在调用该函数时,就直接compare(),
        //括号里填参数就好。通过()操作符重载,把一个类对象当作函数来使用,非常方便。
		bool operator()(const T& p1, const T& p2)
		{
			return p1 < p2;
		}
	};

	template<class T, class container = vector<int>,class Compare = less<T>>
	class priority_queue
	{
		Compare _compare;
	public:
		priority_queue(const Compare& comFunc = Compare())//Compare是一个类,后面
//没写类名意思是匿名对象,然后用comFunc给了它名字。
//得到名字的comFunc传给_comFunc,如果是less就less,Greater就 
 //Greater,不传就默认用Compare(),默认是比小                                      
			:_compare(comFunc)
		{
		}


		template<class InputIterator>
		priority_queue(InputIterator first, InputIterator last, const Compare& comFunc = Compare())
			:_compare(comFunc)
		{
			while (first != last)
			{
				_cn.push_back(*first);
				++first;
			}

			//向下调整建大堆
			for (int i = (_cn.size() - 1 - 1) / 2; i >= 0; --i)
			{
				AdjustDown(i);//从最后一颗树的父亲节点开始调,每次调一颗树
			}
		}

		void AdjustDown(size_t parent)//向下调整,从父亲节点开始调
		{
			size_t child = 2 * parent + 1;//孩子节点是父亲节点的2倍+1,设定为左孩子
			while (child < _cn.size())//比到最后一个孩子节点
			{
                //如果右孩子比左孩子大,左孩子就被右孩子取代。
				if (child + 1 < _cn.size() - 1 && _compare(_cn[child],_cn[child+ 1]))
				{
					++child;
				}
                //父亲节点的值和两个孩子中更大的值比较,父亲节点小就交换位置,继续往下比
				if (child < _cn.size() && _compare(_cn[parent], _cn[child]))
				{
					swap(_cn[parent], _cn[child]);
					parent = child;
					child = 2 * parent + 1;
				}
				else//如果父亲没有比孩子节点再小了,那后面的已经是一个堆了,不用再调整
				{
					break;
				}
			}

		}

		void AdjustUp(size_t child)//向上调整,从最后一个孩子节点开始调,所以是对尾插的元素进
        //行调整
		{
           
			size_t parent = (child - 1) / 2;
			while (child > 0)
			{
				if (_compare(_cn[parent], _cn[child]))//如果父亲节点小于孩子,交换位置
				{
					swap(_cn[parent], _cn[child]);
					parent = child;
					child = 2 * parent + 1;
				}
				else//如果父亲节点不小于,那放在这不用管了。
				{
					break;
				}
			}

			
		}
		bool empty()
		{
			return _cn.empty();
		}
		size_t size()
		{
			return _cn.size();
		}

		void push(const T& x)//尾插时,这里已经是一个堆了,所以前面的不用调,从最后一个开始
		{
			_cn.push_back(x);
			AdjustUp(_cn.size()-1);//最后一个节点是孩子节点,适用于向上调整
		}

		void pop()
		{
			assert(!empty());
			swap(_cn[0], _cn[_cn.size() - 1]);
			_cn.pop_back();//删除头节点以后,最后面的节点到第一个,就要从第一个开始调

			AdjustDown(0);//所以是向下调整
		}

		const T& Top()
		{
			return _cn[0];
		}



	private:
		container _cn;
	};


	void test1()
	{

		int a[] = { 1, 4, 2, 7, 8, 9 };
		priority_queue<int> pq(a, a + 6);

		while (!pq.empty())
		{
			cout << pq.Top() << " ";
			pq.pop();
		}
	}
}

二、deque

2.1deque介绍

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

deque并不是真正连续的空间,而是由一小段连续的小空间拼接而成,实际deque类似于一个动态的二维数组。

它先开辟一块指针数组,每一个指针都指向一块大小相等的空间(比如40个字节),平常插入删除元素就在指针指向的空间里操作,而且一般是从中间的空间开始用。这样当这块指针空间也被填满时,就重新动态开辟一块连续的空间。

双端队列底层是一块假象的连续空间,实际上是分段连续的,指针指向的空间是连续的。但指针指向某一块连续空间,和前一个指针指向的连续空间之间未必是连续的。后一个指针指向的连续空间是从堆区开辟的。要用的时候才开辟,这怎么连续?

2.2deque的优缺点

与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是比vector高的。

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

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

2.3为什么选择deque作为stack和queue的底层默认容器

1.stack和queue不需要遍历,因此stack和queue也没有迭代器,它们只需要在固定的一段进行操作就好。

2.在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);deque中的元素增长时,deque不仅效率高,而且内存使用率高。

这结合了deque的优点,而且完美避开了其缺点。

  • 29
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值