priority_queue的模拟实现

目录

基础框架

成员函数

push、adjust_up

pop、AdjustDown

top、empty、size

阶段性测试

通过【迭代器区间构造优先级队列】的构造函数 

接下来增加第三个模板参数Compare

priority_queue的最终整体代码


基础框架

首先要知道C++的priority_queue就是学C语言时的堆heap,没有任何区别。目前还没有讲解仿函数是什么,因此模板参数只有两个,咱们先指定priority_queue是一个大堆,之后对仿函数讲解后,再增加模板参数Compare。

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

namespace mine
{   
    template<class T,class Container=vector<T>>
	class priority_queue//我们指定当前priority_queue的底层是个大堆
	{
	public:
		

	private:
		Container _con;
	};
}

模板参数Container表示priority_queue适配的底层容器,注意不管优先级队列底层的容器在物理空间上是如何组织数据,优先级队列中的数据在逻辑空间上都是以一个大堆或者小堆的形式被组织起来。(具有堆性质的完全二叉树被称作堆)

比如说,如果Container是vector,则priority_queue底层数据的物理结构就是一个vector,数据在物理空间上连续,但数据在逻辑空间上是一个堆(或者说是具有堆性质的完全二叉树)。同理,如果Container是deque,则底层数据的物理结构就是一个deque,而逻辑结构依然是一个堆(或者说是具有堆性质的完全二叉树)。

这里因为没有内置类型的成员,所以不必手动编写默认构造函数,使用编译器自动生成的即可。

成员函数

push、adjust_up

注意push和<<二叉树和堆的相关知识点>>一文中HeapPush接口的原理是一模一样的,甚至可以说优先级队列和该篇文章中的堆heap的原理就是一模一样的,优先级队列代码的实现也和该篇文章中编写的数据结构struct heap几乎一致,所以如果有什么不理解的,建议先看该篇文章,或许能够解决大部分问题。

push用于将数据插进优先级队列。注意前面说过,我们是指定目前的优先级队列为大堆的。我们在push放数据进大堆(即优先级队列)前,如上图push(58)之前,我们心里要清楚,在push之前,这个完全二叉树就已经是一个大堆了(即该队列就已经是一个真正的优先级队列了),priority_queue的数据都被排列好了(即数据间的结构已经是大堆了),为了保证堆的性质【堆总是一棵完全二叉树】不被破坏,所以我们push的58在逻辑空间上,只能插到完全二叉树的如上图的位置,同时因为priority_queue这个容器适配器适配的底层容器一定是支持push_back、operator[]的能通过下标计算父子关系的顺序存储容器(不可能是list,一般是vector或者deque),所以在物理空间上,58一定是插在了底层容器最后一个元素的后一个位置。

代码如下。

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

namespace mine
{   
    template<class T,class Container=vector<T>>
	class priority_queue
	{
	public:
		void push(const T& x)
		{
			_con.push_back(x);
			//未编写完毕
		}

	private:
		Container _con;
	};
}

但push放进新数据58后,即调用_con对象的push_back(58)后,队形(大堆)会被破坏,导致队列不再是优先级队列(即大堆),所以需要AdjustUp向上调整,接下来咱们编写AdjustUp的逻辑。

注意AdjustUp向上调整是有前提的,详情见<<二叉树和堆的相关知识点>>一文。

AdjustUp就是一个循环,每次循环中,如果被调整节点比父亲节点大,则向上调整,然后更新被调整节点和父亲节点的下标;如果被调整节点比父亲节点小,则break跳出循环,此时调整成功,整个优先级队列就是一个大堆了;如果每次向上调整时,被调整节点都比父亲节点大,不会break退出循环,则最高调整到整棵树的根节点。

push并AdjustUp的流程图1如下。 

push并AdjustUp的流程图2如下。

代码如下。

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

namespace mine
{   
    //先指定优先级队列是一个大堆,以大堆为基础编写AdjustUp和AdjustDown的代码
	template<class T,class Container=vector<T>>
	class priority_queue
	{
	public:
		void push(const T& x)
		{
			_con.push_back(x);
			AdjustUp(_con.size() - 1);
		}

		//大堆版本
		void AdjustUp(size_t pos)
		{
			size_t parent = (pos - 1) / 2;
			while (pos > 0)//这里不能写成parent>=0,即以parent<0为为终止条件,因为parent是无符号整形,不会小于0
			{
				if (_con[pos] > _con[parent])
				{
					std::swap(_con[pos], _con[parent]);
					pos = parent;
					parent = (pos - 1) / 2;
				}
				else
					break;
			}
		}
		

	private:
		Container _con;
	};
}

pop、AdjustDown

前面也说过,我们指定目前的优先级队列是大堆,所以对于优先级队列(本质就是大堆)来说,pop只能删除优先级最高的元素(也就是堆顶元素,即最大值),接下来说说该接口的实现方式。

首先让容器的首元素(位于堆顶的最大值)和最后一个元素交换,然后_con.pop_back()删除最后一个元素,这样优先级最高的元素(最大值)也就被删除了。

流程图如下。

代码如下。

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

namespace mine
{      
    //先指定优先级队列是一个大堆,以大堆为基础编写AdjustUp和AdjustDown的代码
	template<class T,class Container=vector<T>>
	class priority_queue
	{
	public:
		void pop()
		{
			//交换容器首元素和容器的最后一个元素
			swap(_con[0], _con[_con.size() - 1]);
			//然后让容器的_size减一,这边因为拿不到容器的私有变量_size,所以通过pop_back让_size减一
			_con.pop_back();
			//未编写完毕
		}

	private:
		Container _con;
	};

}

但走到这里可以发现堆顶元素13小于56和30,大堆结构被破坏了,因此我们要将堆顶元素13去AdjustDown向下调整。

注意AdjustDown向下调整是有前提的,详情见<<二叉树和堆的相关知识点>>一文。

流程图如下。

代码如下。

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

namespace mine
{   

    //先指定优先级队列是一个大堆,以大堆为基础编写AdjustUp和AdjustDown的代码
	template<class T,class Container=vector<T>>
	class priority_queue
	{
	public:
		void pop()
		{
			//交换容器首元素和容器的最后一个元素
			swap(_con[0], _con[_con.size() - 1]);
			//然后让容器的_size减一,这边因为拿不到容器的私有变量_size,所以通过pop_back让_size减一
			_con.pop_back();
            
			AdjustDown(0);
		}

		//大堆版本
		void AdjustDown(size_t pos)
		{
			//child表示左右孩子节点中较大的孩子,先假设左孩子为较大孩子
			size_t child = 2 * pos + 1;
			while (child < _con.size())
			{
				//如果右孩子存在,并且比左孩子大,则让右孩子成为child
				if (child+1<_con.size()&&_con[child] < _con[child + 1])
				{
					child += 1;
				}
				//如果被调整节点比它孩子节点中更大的孩子要小,则向下调整
				if (_con[pos] < _con[child])
				{
					swap(_con[pos], _con[child]);
					//调整一次后,因为还有继续调整的可能,所以更新父子节点的下标
					pos = child;
					child = 2 * pos + 1;
				}
				else
					//因为被调整节点在调整前,节点的左子树和右子树必须已经是大堆,所以如果被调整节点大于位于堆顶的两个孩子中更大的孩子,则以被调整节点为根的完全二叉树已经是大堆了,无需再调整,退出循环
					break;
			}
		}

	private:
		Container _con;
	};
}

top、empty、size

top用于返回优先级队列中优先级最高的元素,或者说用于返回堆顶元素。对于top来说,返回值一定得加const修饰,否则就可以随意修改堆顶的值,这就无法保证堆中父亲节点总是大于或者小于孩子节点的性质了,或者说无法保证优先级队列的队头元素是优先级最高的元素了。

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

namespace mine
{      
    template<class T,class Container=vector<T>>
	class priority_queue//我们指定当前priority_queue的底层是个大堆
	{
	public:
		const T& top()
		{
			return _con[0];
		}

		bool empty()const//为了让const的优先级队列的对象也能调用,所以设置为const成员函数
		{
			return _con.empty();
		}
        
		size_t size()const//为了让const的优先级队列的对象也能调用,所以设置为const成员函数
		{
			return _con.size();
		}

	private:
		Container _con;
	};
}

阶段性测试

如下图经过测试可以发现建立大堆成功。

代码如下。

void main()
{
	mine::priority_queue<int>p;
	p.push(3);
	p.push(1);
	p.push(2);
	p.push(5);
	p.push(4);
	p.push(3);
	while (!p.empty())
	{
		cout << p.top()<<' ';
		p.pop();
	}
	cout << endl;

}

通过【迭代器区间构造优先级队列】的构造函数 

代码如下。这里说一下,类模板中的成员函数本来就是函数模板,但如果需要额外的模板参数,函数模板还可以再嵌套函数模板。因为【通过迭代器区间构造】的这个构造函数可能会需要通过不同类型的迭代器区间去构造,比如通过vector的迭代器区间构造priority_queue或者通过list的迭代器区间priority_queue,所以再嵌套了一个函数模板,说简单点就是该函数模板因为有需求,所以再增加了一个模板参数InputIterator控制迭代器的类型。

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

namespace mine
{   
    //先指定优先级队列是一个大堆,以大堆为基础编写AdjustUp和AdjustDown的代码
	template<class T,class Container=vector<T>>
	class priority_queue
	{
	public:
        
        //写了priority_queue的【通过迭代器区间构造】的构造函数后,必须注意是必须手动编写默认构造,不能指望编译器生成,因为它不会生成。
        //默认构造啥也不用做,没有内置类型,自定义类型的成员会在初始化列表阶段自动调用它的默认构造
        priority_queue()
        {}

		//通过迭代器区间构造的构造函数的高效版本,时间复杂度为O(N),因为建堆的时间复杂度为O(N)
		template<class InputIterator>
		priority_queue(InputIterator first,InputIterator last)//last是指向最后一个元素的后一个位置的迭代器
		{
			//先将迭代器区间的值存进priority_queue的底层容器_con
			// 错误的方式:直接调用底层容器的【通过迭代器区间构造】的构造函数,注意_con这个成员对象在priority_queue的构造函数中就已经调用Container的构造函数初始化了,因此不能再次调用Container的【通过迭代器区间构造】这个构造函数
			//_con(first, last);
            
            //正确的方式,手动赋值
			while (first != last)
			{
				_con.push_back(*first);
				first++;
			}

			//然后开始建堆,前面也说过,我们暂且是指定优先级队列是一个大堆的,这里通过【自下而上的向下调整法】建堆,所以要保证AdjustDown是按建大堆的逻辑编写的
			//pos表示最后一个叶子节点的父亲节点的下标
			int pos = (_con.size() - 1 - 1) / 2;//_con.size() - 1表示最后一个叶子节点的下标,(叶子节点的下标-1)/2是计算该叶子节点的父亲节点下标的公式
			while (pos>=0)
			{
				AdjustDown(pos);
				pos--;
			}
		}

		通过迭代器区间构造的构造函数的低效版本,时间复杂度为O(N*logN),因为建堆的时间复杂度为O(N*logN)
		//template<class InputIterator>
		//priority_queue(InputIterator first, InputIterator last)
		//{
		//	while (first != last)//循环所花的时间复杂度为O(N)
		//	{
		//		//push内部的AdjustUp的时间复杂度为O(logN)
		//		push(*first);
		//		first++;
		//	}
		//}
		
		void push(const T& x)
		{
			_con.push_back(x);
			AdjustUp(_con.size() - 1);
		}

		//大堆版本
		void AdjustUp(size_t pos)
		{
			size_t parent = (pos - 1) / 2;
			while (pos > 0)//这里不能写成parent>=0,即以parent<0为为终止条件,因为parent是无符号整形,不会小于0
			{
				if (_con[pos] > _con[parent])
				{
					std::swap(_con[pos], _con[parent]);
					pos = parent;
					parent = (pos - 1) / 2;
				}
				else
					break;
			}
		}

		//大堆版本
		void AdjustDown(size_t pos)
		{
			//child表示左右孩子节点中较大的孩子,先假设左孩子为较大孩子
			size_t child = 2 * pos + 1;
			while (child < _con.size())
			{
				//如果右孩子存在,并且比左孩子大,则让右孩子成为child
				if (child+1<_con.size()&&_con[child] < _con[child + 1])
				{
					child += 1;
				}
				//如果被调整节点比它孩子节点中更大的孩子要小,则向下调整
				if (_con[pos] < _con[child])
				{
					swap(_con[pos], _con[child]);
					//调整一次后,因为还有继续调整的可能,所以更新父子节点的下标
					pos = child;
					child = 2 * pos + 1;
				}
				else
					//因为被调整节点在调整前,节点的左子树和右子树必须已经是大堆,所以如果被调整节点大于位于堆顶的两个孩子中更大的孩子,则以被调整节点为根的完全二叉树已经是大堆了,无需再调整,退出循环
					break;
			}
		}

	private:
		Container _con;
	};
}

在基础框架部分我们说过不必手动编写priority_queue的默认构造,因为编译器会自动生成的默认构造就够用了,但编写完priority_queue的【通过迭代器区间构造】的构造函数后,就不得不再自己编写一个priority_queue的默认构造了,否则像priority_queue<int>q;这样的代码就会因为没有默认构造函数去调用,导致无法生成对象而报错,为什么呢?因为编译器自动生成默认构造的前提是你没有编写任何的构造函数,这里我们编写了priority_queue的【通过迭代器区间构造】的构造函数后,编译器就不会再生成priority_queue的默认构造了。

测试代码1如下图。

代码1如下。

#include"priority_queue.h"
#include<vector>


void main()
{
	vector<int>v;
	v.push_back(4);
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(6);
	v.push_back(5);
	mine::priority_queue<int>p(v.begin(),v.end());

	while (!p.empty())
	{
		cout << p.top()<<' ';
		p.pop();
	}
	cout << endl;

}

测试代码2如下图。

之前我们说过,迭代器类型有可能是原生的指针类型,也有可能不是,但使用起来和指针类型是一模一样的,下图案例的迭代器就是原生指针,这也证明了数组也是有迭代器的。

代码2如下.

#include"priority_queue.h"
#include<vector>

void main()
{
	int a[] = { 9,7,10,11,6,5,8,4,3,1,2 };
	mine::priority_queue<int>p(a,a+sizeof(a)/sizeof(int));//a+sizeof(a)/sizeof(int)指向数组最后一个元素的后一个位置

	while (!p.empty())
	{
		cout << p.top()<<' ';
		p.pop();
	}
	cout << endl;

}

接下来增加第三个模板参数Compare

上面AdjustUp和AdjustDown的代码要么按照建小堆的逻辑去写,要么按照建大堆的逻辑去写,也就是说它们要么只能支持建小堆,要么只能支持建大堆。假如我AdjustUp和AdjustDown写成了建小堆的版本,如果此时我需要通过AdjustUp和AdjustDown去建大堆,那么只能修改代码,将AdjustUp和AdjustDown的代码改成建大堆的版本。那有没有一种方法能在我不修改AdjustUp和AdjustDown的代码的情况下,让AdjustUp和AdjustDown他俩既能支持建大堆,也能支持建小堆呢?这样我就可以控制,如果我优先级队列(本质就是堆)是值大的元素优先级较高,则建大堆,则让AdjustUp和AdjustDown变成建大堆的版本,反之则让AdjustUp和AdjustDown变成建小堆的版本。

如果没有这样的方法,则只能通过这样的方式去控制优先级队列是大堆还是小堆:比如将smallAdjustUp、smallAdjustDown和bigAdjustUp、bigAdjustDown这些接口全部实现,如果我需要建大堆就去调用bigAdjustUp和bigAdjustDown去调整堆中数据;反之如果我需要建小堆,则调用smallAdjustUp和smallAdjustDown去调整堆中数据。这种方法虽然也行,但缺点也显而易见,smallAdjustUp和bigAdjustUp的代码除了大于号变成小于号,小于号变成大于号以外,其余所有代码是完全一致的,smallAdjustDown和bigAdjustDown也是如此,这就造成了代码的冗余,十分不优雅。

但万幸是有这样的方法,可以通过仿函数(即函数对象)来控制AdjustUp和AdjustDown到底是建大堆还是建小堆,先介绍一下仿函数是什么。

如下图的isFunc就是一个仿函数,称之为【仿】是因为isFunc本质不是一个函数,而是一个less类的对象,只是该对象使用自己的成员函数operator()时,不仅可以通过isFunc.operator()(1,2),还可以通过isFunc(1,2),后者和使用一个函数的方式完全一致,所以称之为仿函数。

说一下这里的命名空间mine会和priority_queue.h文件中的命名空间mine合并。

代码如下。

#include"priority_queue.h"

namespace mine
{
	//写法1
	/*class less
	{
	public:
		template<class T>
		bool operator()(const T&x,const T&y)const
		{
			return x < y;
		}
	};*/

	//写法2
	struct less
	{
		template<class T>
		bool operator()(const T& x, const T& y)const
		{
			return x < y;
		}
	};

}


void main()
{
	mine::less isFunc;
	cout << isFunc(1, 2);
	cout << endl;
	cout << isFunc.operator()(1, 2);
	cout << endl;
}

接下来咱们在编写一个greater的仿函数。这里和less类这个具体的类不一样,我们换了一种写法,将greater写成了类模板,那么这个类实例化后,该类的对象就是一个仿函数(函数对象)了。

 代码如下。

#include"priority_queue.h"

namespace mine
{
	//写法1
	/*class less
	{
	public:
		template<class T>
		bool operator()(const T&x,const T&y)const
		{
			return x < y;
		}
	};*/

	//写法2
	struct less
	{
		template<class T>
		bool operator()(const T& x, const T& y)const
		{
			return x < y;
		}
	};

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

}


void main()
{
	mine::less isFunc;
	cout << isFunc(1, 2);
	cout << endl;
	cout << isFunc.operator()(1, 2);
	cout << endl;

	mine::greater<int>g;
	cout << g(1, 2);
	cout << endl;
	cout << g.operator()(1, 2);
}

注意上面less是具体的类,而greater却是类模板,这不太统一,上面这样写只是为了演示仿函数的多种写法,所以为了更统一,咱们将less也写成类模板,代码如下。

namespace mine
{
	//写法1
    /*template<class T>
    class less
	{
	public:
		bool operator()(const T&x,const T&y)const
		{
			return x < y;
		}
	};*/

	//写法2
    template<class T>
	struct less
	{
		bool operator()(const T& x, const T& y)const
		{
			return x < y;
		}
	};

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

}

编写完greater和less这两个类模板后,我们就可以通过类模板greater和less实例化出的类的对象,即仿函数(函数对象)来在不改变AdjustUp和AdjustDown代码的情况下控制AdjustUp和AdjustDown他俩到底是建大堆还是建小堆了。

首先为priority_queue这个类模板增加新的模板参数Compare,代码如下。可以看到咱们给了一个缺省值less<T>,注意less是咱们刚编写的类模板,但less<T>是具体的类,因为当priority_queue这个类模板被实例化成priority_queue<T>后,less<T>就成为具体的类了。我们给less<T>类提供了一个成员函数operator(),用于帮我们比较x是否小于y。该函数完全等价于<,举个例子给你看,假如有表达式1<2,如果1小于2,则返回true,反之返回false;而假如有less<T>对象x和表达式x(1,2),如果1小于2,则也返回true,反之返回false,所以说less<T>类的成员函数operator()和<是完全等价的。同理,咱们刚编写的greater这个类模板实例化出具体的类后,该类的operator()和>也是完全等价的。

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

namespace mine
{   
    template<class T,class Container=vector<T>,class Compare=mine::less<T>>
	class priority_queue//我们指定当前priority_queue的底层是个大堆
	{
	public:
		

	private:
		Container _con;
	};
}

然后修改AdjustUp和AdjustDown的代码。

这里要知道一点,使用STL官方的priority_queue时,如果想让优先级队列是一个大堆(即如果想让优先级队列中值较大的元素的优先级更高),则需要给模板参数Compare传【通过STL官方的类模板less实例化出】的类,反之如果想让优先级队列是一个小堆(即如果想让优先级队列中值较小的元素的优先级更高),则需要给模板参数Compare传【通过STL官方的类模板greater实例化出】的类。是的你没看错我也没写错,使用STL的priority_queue时,按理来说这样更方便记忆:如果是大堆则给Compare传greater实例化出的类,如果是小堆则给Compare传less实例化出的类。但STL的做法刚好和咱们认为的更方便的做法相反,可以说设计的是非常不友好,但苦于我们是在模拟实现STL的priority_queue,所以即使不好,我们也要按照标准来编写代码。

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

namespace mine
{   

    //写法1
    /*template<class T>
    class less
	{
	public:
		bool operator()(const T&x,const T&y)const
		{
			return x < y;
		}
	};*/

	//写法2
    template<class T>
	struct less
	{
		bool operator()(const T& x, const T& y)const
		{
			return x < y;
		}
	};

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


    template<class T,class Container=vector<T>,class Compare=less<T>>
	class priority_queue
	{
	public:
		//仿函数通用版本的AdjustUp,既可以建大堆,也可以建小堆
		void AdjustUp(size_t pos)
		{
			//定义函数对象
			Compare x;
			size_t parent = (pos - 1) / 2;
			while (pos > 0)//这里不能写成parent>=0,即以parent<0为为终止条件,因为parent是无符号整形,不会小于0
			{
         
				if (x(_con[parent],_con[pos]))
				{
					std::swap(_con[pos], _con[parent]);
					pos = parent;
					parent = (pos - 1) / 2;
				}
				else
					break;
			}
		}


        //仿函数通用版本的AdjustDown,既可以建大堆,也可以建小堆
		void AdjustDown(size_t pos)
		{
			//首先定义函数对象
			Compare x;
			//如果Compare类是通过less类模板实例化的类,则说明建大堆,则child表示左右孩子节点中较大的孩子,先假设左孩子为较大孩子;
			//如果Compare类是通过greater类模板实例化的类,则说明建小堆,则child表示左右孩子节点中较小的孩子,先假设左孩子为较小孩子;
			size_t child = 2 * pos + 1;
			while (child < _con.size())
			{
				//如果Compare类是通过less类模板实例化的类,并且右孩子存在,并且右孩子比左孩子大,则让右孩子成为child
				//如果Compare类是通过greater类模板实例化的类,并且右孩子存在,并且右孩子比左孩子小,则让右孩子成为child
				if (child + 1 < _con.size() && x(_con[child], _con[child + 1]))
				{
					child += 1;
				}
				//如果Compare类是通过less类模板实例化的类,并且被调整节点比它孩子节点中更大的孩子要小,则向下调整
				//如果Compare类是通过greater类模板实例化的类,并且被调整节点比它孩子节点中更小的孩子要大,则向下调整
				if (x(_con[pos], _con[child]))
				{
					swap(_con[pos], _con[child]);
					//调整一次后,因为还有继续调整的可能,所以更新父子节点的下标
					pos = child;
					child = 2 * pos + 1;
				}
				else
					break;
			}
		}
        
	private:
		Container _con;
	};
}

没有仿函数时,在AdjustUp和AdjustDown函数的if判断语句中比较父子节点大小时,是通过类似于if(x<y){...},如果x小于y,则返回true,继续执行{}中的代码,反之返回false,不执行后序代码。假如我现在想改变条件,让x大于y时进入{}执行代码,则平常的方式就是将<改成>,如if(x>y){...}。但有了仿函数后看看我们是如何灵活的控制大于号小于号,前面也说过,less<T>的对象完全等价于<,比如有less<T>x,则x(1,2)等价于1<2,greater<T>的对象完全等价于>,比如有greater<T>x,则x(1,2)等价于1>2,然后现在又有Compare这个类型模板参数,我们能通过控制Compare是less<T>还是greater<T>,以此让x(1,2)等价于1<2还是1>2。

注意这里不仅仅可以给Compare传仿函数(函数对象)的类型,只要是可调用对象的类型,统统可以传给Compare。所以完成上述任务不仅仅可以通过仿函数(函数对象),还可以通过函数指针等等可调用对象,通过函数指针的比较大小的方式类似于下图,注意这里只是大致的代码,代码并不成熟。

编写完毕,开始测试

给模板参数Compare传greater<int>类时,优先级队列是小堆,如下图所示。

给模板参数Compare传less<int>类时,优先级队列是大堆,如下图所示。

走到这里,我们的priority_queue就讲解结束了。

priority_queue的最终整体代码

代码如下。

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

namespace mine
{

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

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

	
	template<class T,class Container=vector<T>,class Compare=mine::less<T>>
	class priority_queue
	{
	public:

		//写了priority_queue的【通过迭代器区间构造】的构造函数后,必须注意是必须手动编写默认构造,不能指望编译器生成,因为它不会生成。
		//默认构造啥也不用做,没有内置类型,自定义类型的成员会在初始化列表阶段自动调用它的默认构造
		priority_queue()
		{}

		//通过迭代器区间构造的构造函数的高效版本,时间复杂度为O(N),因为建堆的时间复杂度为O(N)
		template<class InputIterator>
		priority_queue(InputIterator first,InputIterator last)//last是指向最后一个元素的后一个位置的迭代器
		{
			//先将迭代器区间的值存进priority_queue的底层容器_con
			// 错误的方式:直接调用底层容器的【通过迭代器区间构造】的构造函数,注意_con这个成员对象在priority_queue的构造函数中就已经调用Container的构造函数初始化了,因此不能再次调用Container的【通过迭代器区间构造】这个构造函数
			//_con(first, last);

			//正确的方式,手动赋值
			while (first != last)
			{
				_con.push_back(*first);
				first++;
			}

			//pos表示最后一个叶子节点的父亲节点的下标
			int pos = (_con.size() - 1 - 1) / 2;//_con.size() - 1表示最后一个叶子节点的下标,(叶子节点的下标-1)/2是计算该叶子节点的父亲节点下标的公式
			while (pos>=0)
			{
				AdjustDown(pos);
				pos--;
			}
		}

		通过迭代器区间构造的构造函数的低效版本,时间复杂度为O(N*logN),因为建堆的时间复杂度为O(N*logN)
		//template<class InputIterator>
		//priority_queue(InputIterator first, InputIterator last)
		//{
		//	while (first != last)//循环所花的时间复杂度为O(N)
		//	{
		//		//push内部的AdjustUp的时间复杂度为O(logN)
		//		push(*first);
		//		first++;
		//	}
		//}
		
		void push(const T& x)
		{
			_con.push_back(x);
			AdjustUp(_con.size() - 1);
		}

		//大堆版本
		//void AdjustUp(size_t pos)
		//{
		//	size_t parent = (pos - 1) / 2;
		//	while (pos > 0)//这里不能写成parent>=0,即以parent<0为为终止条件,因为parent是无符号整形,不会小于0
		//	{
		//		if (_con[pos] > _con[parent])
		//		{
		//			std::swap(_con[pos], _con[parent]);
		//			pos = parent;
		//			parent = (pos - 1) / 2;
		//		}
		//		else
		//			break;
		//	}
		//}

		//仿函数通用版本的AdjustUp,既可以建大堆,也可以建小堆
		void AdjustUp(size_t pos)
		{
			//定义函数对象
			Compare x;
			size_t parent = (pos - 1) / 2;
			while (pos > 0)//这里不能写成parent>=0,即以parent<0为为终止条件,因为parent是无符号整形,不会小于0
			{
				//如果Compare类是通过类模板less实例化出的类,则此时建大堆,如果父亲节点小于被调整节点,则被调整节点向上走
				//如果Compare类是通过类模板greater实例化出的类,则此时建小堆,如果父亲节点大于被调整节点,则被调整节点向上走
				if (x(_con[parent],_con[pos]))
				{
					std::swap(_con[pos], _con[parent]);
					pos = parent;
					parent = (pos - 1) / 2;
				}
				else
					break;
			}
		}

		void pop()
		{
			//交换容器首元素和容器的最后一个元素
			swap(_con[0], _con[_con.size() - 1]);
			//然后让容器的_size减一,这边因为拿不到容器的私有变量_size,所以通过pop_back让_size减一
			_con.pop_back();

			AdjustDown(0);
		}

		//大堆版本
		//void AdjustDown(size_t pos)
		//{
		//	//child表示左右孩子节点中较大的孩子,先假设左孩子为较大孩子
		//	size_t child = 2 * pos + 1;
		//	while (child < _con.size())
		//	{
		//		//如果右孩子存在,并且比左孩子大,则让右孩子成为child
		//		if (child+1<_con.size()&&_con[child] < _con[child + 1])
		//		{
		//			child += 1;
		//		}
		//		//如果被调整节点比它孩子节点中更大的孩子要小,则向下调整
		//		if (_con[pos] < _con[child])
		//		{
		//			swap(_con[pos], _con[child]);
		//			//调整一次后,因为还有继续调整的可能,所以更新父子节点的下标
		//			pos = child;
		//			child = 2 * pos + 1;
		//		}
		//		else
		//			//因为被调整节点在调整前,节点的左子树和右子树必须已经是大堆,所以如果被调整节点大于位于堆顶的两个孩子中更大的孩子,则以被调整节点为根的完全二叉树已经是大堆了,无需再调整,退出循环
		//			break;
		//	}
		//}

		//仿函数通用版本的AdjustDown,既可以建大堆,也可以建小堆
		void AdjustDown(size_t pos)
		{
			//首先定义函数对象
			Compare x;
			//如果Compare类是通过less类模板实例化的类,则说明建大堆,则child表示左右孩子节点中较大的孩子,先假设左孩子为较大孩子;
			//如果Compare类是通过greater类模板实例化的类,则说明建小堆,则child表示左右孩子节点中较小的孩子,先假设左孩子为较小孩子;
			size_t child = 2 * pos + 1;
			while (child < _con.size())
			{
				//如果Compare类是通过less类模板实例化的类,并且右孩子存在,并且右孩子比左孩子大,则让右孩子成为child
				//如果Compare类是通过greater类模板实例化的类,并且右孩子存在,并且右孩子比左孩子小,则让右孩子成为child
				if (child + 1 < _con.size() && x(_con[child], _con[child + 1]))
				{
					child += 1;
				}
				//如果Compare类是通过less类模板实例化的类,并且被调整节点比它孩子节点中更大的孩子要小,则向下调整
				//如果Compare类是通过greater类模板实例化的类,并且被调整节点比它孩子节点中更小的孩子要大,则向下调整
				if (x(_con[pos], _con[child]))
				{
					swap(_con[pos], _con[child]);
					//调整一次后,因为还有继续调整的可能,所以更新父子节点的下标
					pos = child;
					child = 2 * pos + 1;
				}
				else
					break;
			}
		}

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

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

		size_t size()const
		{
			return _con.size();
		}
	private:
		Container _con;
	};


}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值