C++ priority_queue的使用及模拟实现

                                       

好多天没有更新博客了,最近一直在忙着学linux和自己的专业课(因为博主不是科班的~)。现在在学校感觉时间有些紧,等到期末考完了,博主就全身心把linux博客总结并分享给大家~

       优先级队列也是STL库中非常实用的一个容器。底层实现和堆很相似,这个容器又和之前讲的string、vector、list...模板参数上有些区别,今天来和老铁们一起见识一下这个容器~

目录

priority_queue的介绍

 priority_queue的使用

construct

priority_queue() 

priority_queue(first, last)

我们在上面看到了它有三个模板参数,那么我们应该怎么把控呢?

empty()

top()

push()

pop()

size()

swap() 

 priority_queue模拟实现

Compare仿函数实现

less

greater

priority_queue()

priority_queue(InputIterator first, InputIterator last)

void adjust_down(int parent)

void push(const T& x)

void adjust_up(int child)

void pop()

const T& top() const

size_t size()

bool empty()

完整代码

priority_queue.h

Date.h

测试代码

 总结

注意


priority_queue的介绍

优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。

注意:
默认情况下priority_queue是大堆。

我们先来看一下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来自动完成此操作。

 priority_queue的使用

construct

priority_queue() 

 无参构造一个空的优先级队列。

举个栗子:

#include<iostream>
#include<functional>
#include<queue>
using namespace std;

int main()
{
	/*explicit priority_queue(const Compare & comp =Compare(),
	        const Container & ctnr = Container()); */
	priority_queue<int> q; 
	return 0;
}

我们调试一下看看情况:

通过调试,我们发现这种无参构造就构造出来了一个空的优先级队列。 

priority_queue(first, last)

使用迭代器区间来完成构造初始化,这个迭代器是InputIterator(普通迭代器)。说明我们可以传vector、list...这样的迭代器,只要里面元素类型匹配就行。

举个栗子:

int main()
{
	/*template <class InputIterator>
         priority_queue (InputIterator first, InputIterator last,
                         const Compare& comp = Compare(),
                         const Container& ctnr = Container());*/

	//传vector迭代器
	vector<int> v = { 1,2,3,4,5 }; //C++11支持的语法
	priority_queue<int> q1(v.begin(),v.end());

	//传list迭代器
	list<int> l = { 10,20,30,40,50 };//C++11支持的语法
	priority_queue<int> q2(l.begin(), l.end());

	//普通数组
	int arr[] = { 5,4,3,2,1 };
	priority_queue<int> q3(arr, arr + 5);
	return 0;
}

我们调试一下看看情况:

        我们通过调试发现,用vector、list、甚至是数组(传地址,注意地址也是按照左闭右开的规则)都可以来初始化优先级队列!

        默认情况下,这个队列是建大堆,所以前两种情况在底层存储时的顺序是和我们传的顺序是有变化的。第三种方式传的时候本身就是大堆,所以底层存储没有变化。

我们在上面看到了它有三个模板参数,那么我们应该怎么把控呢?

class T:表示存储的元素的类型。

class Container = vector<T>:表示底层使用什么容器来实现优先级队列,当我们不传时,默认使用vector。

class Compare = less<typename Container::value_type>:仿函数,我们不传时,默认是建大堆。如果想要建小堆,需要显示指明,并引入头文件 #include<functional>

举个栗子:

int main()
{
	vector<int> v1 = { 1,2,3,4,5 };
	priority_queue<int, vector<int>, greater<int>> q1(v1.begin(), v1.end());

	vector<int> v2 = { 50,40,30,20,10 };
	priority_queue<int, deque<int>, greater<int>> q2(v2.begin(), v2.end());
	return 0;
}

我们调试一下看看情况:

priority_queue<int, vector<int>, greater<int>> :根据我们曾经学习的缺省值传参语法,我们传参时给模板参数从左往右来传。

第二个模板参数是我们显示指定使用容器vector<来实现堆>,第三个模板参数的是仿函数,greater这里是指明priority_queue内部实现建立小堆(我们通过监视窗口heap里面的数据建堆就可以发现)。 

下面的也是一样~

empty()

判断优先级队列是否为空,如果为空就返回真(1);如果不为空就返回假(1)。

举个栗子:

int main()
{
	priority_queue<int> q1;
	cout << q1.empty() << endl;

	vector<int> v = { 1,2,3,4,5 };
	priority_queue<int> q2(v.begin(), v.end());
	cout << q2.empty() << endl;
	return 0;
}

运行结果:

我们发现,q1队列为空,则输出1;q2队列不为空,则输出0。

top()

 返回优先级队列中堆顶的数据,实际上就是底层数组的第一个元素。

举个栗子:

int main()
{
	vector<int> v = { 1,2,3,4,5 };
	priority_queue<int> q(v.begin(), v.end()); //默认是建大堆
	cout << q.top() << endl;
	return 0;
}

运行结果:

由于底层是建立大堆,5是这几个数据中最大元素,一定是堆顶数据。所以输出了5。 

push()

往优先级队列中插入元素val。(插入后可能底层数据关系发生变化,因为要保证堆的规则成立)

举个栗子:

int main()
{
	priority_queue<int> q;
	q.push(1);
	q.push(2);
	q.push(5);
	q.push(4);
	q.push(3);
	return 0;
}

我们来调试一下看看情况:

通过监视窗口,我们看到3次push把数据插入到了优先级队列中了。

我们来画图展示这个插入过程,顺便带大家复习一下建堆的知识:

pop()

删除优先级队列中堆顶的元素。 

举个栗子:

int main()
{
	priority_queue<int> q;
	q.push(1);
	q.push(2);
	q.push(5);
	q.push(4);
	q.push(3);
	q.pop();
	return 0;
}

我们调试一下来看看情况:

很显然,经过pop()后,堆顶元素5就被删除了,并且同时底层重新建堆。

我们来画图 展示一下这个过程:

size()

返回优先级队列中元素的个数。

举个栗子:

int main()
{
	priority_queue<int> q;
	q.push(1);
	q.push(2);
	q.push(3);
	cout << q.size() << endl;
	return 0;
}

运行结果:

swap() 

交换两个优先级队列中的数据。 

举个栗子:

int main()
{
	priority_queue<int> q1;
	q1.push(5);
	q1.push(4);
	q1.push(3);
	q1.push(2);
	q1.push(1);

	priority_queue<int> q2;
	q2.push(50);
	q2.push(40);
	q2.push(30);
	q2.push(20);
	q2.push(10);
	q1.swap(q2);
	return 0;
}

我们来调试一下看看情况:

我们通过比较swap前后的数据就可以发现两个优先级队列中的数据是交换成功了。 

 priority_queue模拟实现

Compare仿函数实现

less

	//仿函数
	template<class T>
	struct less //大堆
	{
		bool operator()(const T& x, const T& y) const
		{
			return x < y;
		}
	};

	//特化 -- 针对比如日期类这样的自定义类型 ->new出来的Date对象走这个
	template<>
	struct less<Date*>
	{
		bool operator()(const Date*& x, const Date*& y)const
		{
			return *x < *y;
		}
	};

    //仿函数
    template<class T>
    struct less //大堆。是不是less对应的大堆感觉是有点怪?在实现的时候对应的是小于号
    {
        bool operator()(const T& x, const T& y) const //重载(),来实现两个数大小的比较,返回值是bool值,来实现判断大小。
        {
            return x < y;
        }
    };

    //特化 -- 针对比如日期类这样的自定义类型 ->new出来的Date对象走这个
    template<>
    struct less<Date*>
    {
        bool operator()(const Date*& x, const Date*& y)const
        {
            return *x < *y;
        }
    };

greater

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

	//特化 -- 针对比如日期类这样的自定义类型 ->new出来的Date对象走这个
	template<>
	struct greater<Date*>
	{
		bool operator()(const Date*& x, const Date*& y) const
		{
			return *x > *y;
		}
	};

   //greater仿函数实现 

   template<class T>
    struct greater //建小堆 ->在下面对应大于号比较
    {
        bool operator()(const T& x, const T& y) //对()进行重载实现。
        {
            return x > y;
        }
    };

    //特化 -- 针对比如日期类这样的自定义类型 ->new出来的Date对象走这个
    template<>
    struct greater<Date*>
    {
        bool operator()(const Date*& x, const Date*& y) const
        {
            return *x > *y;
        }
    };

priority_queue()

//无参的构造函数
priority_queue()
{}

//无参的构造函数
priority_queue() 可以初始化成一个空的优先级队列
{}

priority_queue(InputIterator first, InputIterator last)

template<class InputIterator>
priority_queue(InputIterator first, InputIterator last)
	:_con(first, last) //先把数据录入_con中,实际上调用了_con的构造函数
{
	//建堆
	for (int i = (_con.size() - 1 - 1) / 2; i >= 0; i--)
	{
		adjust_down(i);
	}
}

//构造函数 -->使用了模板

template<class InputIterator>
priority_queue(InputIterator first, InputIterator last) //使用迭代器区间进行构造
    :_con(first, last) //先把数据录入_con中,实际上调用了_con的构造函数
{
    //建堆
    for (int i = (_con.size() - 1 - 1) / 2; i >= 0; i--)
    {
        adjust_down(i); 把_con里面的数据进行建堆,在这里采用向下建堆
    }
}

void adjust_down(int parent)

void adjust_down(int parent)
{
	Compare com;//仿函数对象
	int child = parent * 2 + 1;//默认是左孩子
	while (child < _con.size()) //_con.size()-1的位置就是最后一个数据下标的位置了。child == _con.size()就会越界!
	{
		//if (child + 1 < _con.size() && _con[child + 1] > _con[child])
		if (child + 1 < _con.size() && com(_con[child], _con[child+1]))
		{
			child += 1;
		}
		if (com(_con[parent], _con[child]))
		{
			swap(_con[parent], _con[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void adjust_down(int parent)
{
    Compare com;//仿函数对象
    int child = parent * 2 + 1;//默认是左孩子
    while (child < _con.size()) //_con.size()-1的位置就是最后一个数据下标的位置了。child == _con.size()就会越界!
    {
        //if (child + 1 < _con.size() && _con[child + 1] > _con[child])
        if (child + 1 < _con.size() && com(_con[child], _con[child+1])) //使用仿函数来进行比较,在这里会自动调用com里的operator()。

如果使用的是less,里面实现时是用小于号,也就是后面的那个数大,就返回真;

如果使用的是greater,里面实现时是用大于号,也就是前面的那个数大,就返回真;
        {
            child += 1;
        }
        if (com(_con[parent], _con[child]))
        {
            swap(_con[parent], _con[child]);
            parent = child;
            child = parent * 2 + 1;
        }
        else
        {
            break;
        }
    }
}

之前我写过一篇关于常见排序的博客,里面有对堆排序的详细介绍,也包括上面的实现逻辑,在这里我就不再赘述了~

常见排序算法激烈讲解_暴走的橙子~的博客-CSDN博客

void push(const T& x)

void push(const T& x)
{
	_con.push_back(x);
	adjust_up(_con.size() - 1);//插入的数据向上调整
}

void adjust_up(int child)

void adjust_up(int child)
{
	Compare com; //定义一个仿函数对象
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (com(_con[parent], _con[child]))
		{
			swap(_con[parent], _con[child]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

void adjust_up(int child)
{
    Compare com; //定义一个仿函数对象,方便实现下面两个数据的比较
    int parent = (child - 1) / 2;
    while (child > 0)
    {
        if (com(_con[parent], _con[child]))//使用仿函数来进行比较,在这里会自动调用com里的operator()。

如果使用的是less,里面实现时是用小于号,也就是后面的那个数大,就返回真;

如果使用的是greater,里面实现时是用大于号,也就是前面的那个数大,就返回真;

        {
        .........
}

void pop()

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

void pop() //删除时,不能直接删除堆顶数据,这样会把原来的堆关系全部打乱。
{
    swap(_con[0], _con[_con.size() - 1]);  //先交换
    _con.pop_back(); //再删除末尾的数据
    adjust_down(0); //调整

我们再来看看这删除的逻辑:


const T& top() const

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

size_t size()

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

bool empty()

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

以上三个函数都是调用了容器_con里面的接口,所以说表面看priority_queue是一个新的STL,实际上还是复用像vector、deque这样的容器~只不过在外面加了一层仿函数的封装以及堆排序算法的逻辑。

完整代码

priority_queue.h

#pragma once
#include<iostream>
#include"Date.h"
#include<vector>
#include<deque>
#include<algorithm>
using namespace std;

namespace cyq
{
	//仿函数
	template<class T>
	struct less //大堆
	{
		bool operator()(const T& x, const T& y) const
		{
			return x < y;
		}
	};

	//特化 -- 针对比如日期类这样的自定义类型 ->new出来的Date对象走这个
	template<>
	struct less<Date*>
	{
		bool operator()(const Date*& x, const Date*& y)const
		{
			return *x < *y;
		}
	};

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

	//特化 -- 针对比如日期类这样的自定义类型 ->new出来的Date对象走这个
	template<>
	struct greater<Date*>
	{
		bool operator()(const Date*& x, const Date*& y) const
		{
			return *x > *y;
		}
	};

	template<class T,class Container=vector<T>,class Compare=less<T>>
	class priority_queue
	{
	private:
		void adjust_down(int parent)
		{
			Compare com;//仿函数对象
			int child = parent * 2 + 1;//默认是左孩子
			while (child < _con.size()) //_con.size()-1的位置就是最后一个数据下标的位置了。child == _con.size()就会越界!
			{
				//if (child + 1 < _con.size() && _con[child + 1] > _con[child])
				if (child + 1 < _con.size() && com(_con[child], _con[child+1]))
				{
					child += 1;
				}
				if (com(_con[parent], _con[child]))
				{
					swap(_con[parent], _con[child]);
					parent = child;
					child = parent * 2 + 1;
				}
				else
				{
					break;
				}
			}
		}
		void adjust_up(int child)
		{
			Compare com; //定义一个仿函数对象
			int parent = (child - 1) / 2;
			while (child > 0)
			{
				if (com(_con[parent], _con[child]))
				{
					swap(_con[parent], _con[child]);
					child = parent;
					parent = (child - 1) / 2;
				}
				else
				{
					break;
				}
			}
		}
	public:
		//无参的构造函数
		priority_queue()
		{}

		template<class InputIterator>
		priority_queue(InputIterator first, InputIterator last)
			:_con(first, last) //先把数据录入_con中,实际上调用了_con的构造函数
		{
			//建堆
			for (int i = (_con.size() - 1 - 1) / 2; i >= 0; i--)
			{
				adjust_down(i);
			}
		}
		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);
		}
		const T& top() const
		{
			return _con[0];
		}
		size_t size()
		{
			return _con.size();
		}
		bool empty()
		{
			return _con.empty();
		}
	private:
		Container _con;
		};
}

Date.h

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

class Date
{
	friend ostream& operator<<(ostream& out, const Date& d);
public:
	Date(int year=1,int month=1,int day=1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
	// 建立大堆时,需要用户在自定义类型中提供<的重载
	bool operator<(const Date& d) const
	{
		return _year < d._year ||
			(_year == d._year && _month < d._month) ||
			(_year == d._year && _month == d._month && _day < d._day);
	}
	// 建立小堆时,需要用户在自定义类型中提供>的重载
	bool operator>(const Date& d) const
	{
		return _year > d._year ||
			(_year == d._year && _month > d._month) ||
			(_year == d._year && _month == d._month && _day > d._day);
	}
private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "-" << d._month << "-" << d._day;
	return out;
}

测试代码

int main()
{
	cyq::priority_queue<int> q1;
	q1.push(1);
	q1.push(2);
	q1.push(3);
	q1.push(4);
	q1.push(5);

	vector<int> v = { 50,40,30,20,10 };
	cyq::priority_queue<int> q2(v.begin(), v.end());
	q2.pop();
	cout << q2.top() << endl;

	cyq::priority_queue<int,vector<int>,greater<int>> q4;
	q4.push(5);
	q4.push(4);
	q4.push(3);
	q4.push(2);
	q4.push(1);

	cyq::priority_queue<Date> q3;
	q3.push(Date(2022, 4, 31));
	q3.push(Date(2022, 5, 1));
	q3.push(Date(2021, 5, 1));
	q3.push(Date(2021, 4, 31));
	return 0;
}

通过调试窗口来看:

 总结

1、less对应建大堆,也是默认的。 我们在实现仿函数时,内部用小于号 <

greater对应建小堆,我们在实现仿函数时,内部用大于号 >

2、我们要在priority_queue中存放自定义类型时,用户需要在自定义类型中提供> 或者< 的重载。最好也可以把仿函数自己实现,因为有时候像存储Date*这样的类型时,可能会出现问题,例子在下面~(库里面可没有*解引用Date的函数方法实现)

建大堆时:大堆,需要用户在自定义类型中提供<的重载  口诀:大堆->less->小于号

建小堆时:小堆,需要用户提供>的重载 口诀:小堆->greater->大于号

3、表面看priority_queue是一个新的STL,实际上还是复用像vector、deque这样的容器~只不过在外面加了一层仿函数的封装以及堆排序算法的逻辑。

如果老铁们还想进一步了解堆这个算法可以看看博主曾经花了2天写的博客:

常见排序算法激烈讲解_暴走的橙子~的博客-CSDN博客

注意

看下面的代码:

	cyq::priority_queue<Date*,vector<Date*>,cyq::greater<Date*>> q3;
	q3.push(new Date(2022, 4, 31));
	q3.push(new Date(2022, 5, 1));
	q3.push(new Date(2021, 5, 1));
	q3.push(new Date(2021, 4, 31));
	q3.push(new Date(2021, 6, 31));
	q3.push(new Date(2022, 5, 31));
	while (!q3.empty())
	{
		cout << *(q3.top()) << endl;
		q3.pop();
	}

我们把new出来的对象的地址放进优先级队列。再看看我们实现的push版本:

void push(const T& x)
{
	_con.push_back(x);
	adjust_up(_con.size() - 1);//插入的数据向上调整
}

T在这里自动推导出来的类型是Date*,在这里会走特化的版本(需要自己实现)。

cyq::priority_queue<Date*,vector<Date*>,cyq::greater<Date*>>:这里一定要指明cyq::greater<Date*>里面的cyq::类域(greater在另一个工程里面,头文件#include<functiolal>包括了greater仿函数),否则greater就走了库里面的了,然后就按照地址进行比较,但是地址是随机的,这样比较显然是不对的!在这里博主调试好久才发现错误的,呜呜~~

greater的特化版本:

	//特化 -- 针对比如日期类这样的自定义类型 ->new出来的Date对象走这个
	template<>
	struct greater<Date*>
	{
		bool operator()(const Date*& x, const Date*& y) const
		{
			return *x > *y;
		}
	};

运行结果:

看到这里给博主支持下吧~

  • 12
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
在优先队列中,底层存储数据的容器默认是vector,因此优先队列的数组实现就是使用vector作为底层容器来存储数据。\[1\] 优先队列通过堆算法将vector中的元素构造成堆的结构,实现了优先级的排序。默认情况下,优先队列是大堆,即堆顶元素是最大的数据。\[1\] 优先队列的模板声明带有三个参数,分别是数据类型、保存数据的容器和元素比较方式。容器必须是用数组实现的容器,比如vector、deque,而STL默认使用的是vector作为容器。比较方式默认使用operator<来进行比较,所以如果不指定后面两个参数,优先队列就是大顶堆,队头元素最大。\[2\] #### 引用[.reference_title] - *1* *2* [C++优先级队列priority_queue详解及其模拟实现](https://blog.csdn.net/qq_61635026/article/details/125807805)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v4^insert_chatgpt"}} ] [.reference_item] - *3* [priority_queue(优先级队列)的用法,模拟实现priority_queue(堆排序、仿函数)](https://blog.csdn.net/qq_46131499/article/details/120731928)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v4^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值