C++第二十七弹---优先级队列的高级应用:结合仿函数优化性能

个人主页: 熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】

目录

1 priority_queue的介绍和使用

1.1 priority_queue的介绍

1.2 priority_queue的使用

2 仿函数的介绍和使用

2.1 仿函数的介绍 

2.2 仿函数的使用

3 自定义类型的使用

4 指针类型的使用

5 priority_queue的模拟实现

总结


1 priority_queue的介绍和使用


1.1 priority_queue的介绍

1. 优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。

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

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

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

  • empty():检测容器是否为空
  • size():返回容器中有效元素个数
  • front():返回容器中第一个元素的引用
  • push_back():在容器尾部插入元素
  • pop_back():删除容器尾部元素


5. 标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector。

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

1.2 priority_queue的使用


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


默认情况下priority_queue是大堆。
 

函数声明接口说明
priority_queue()构造一个空的优先级队列
empty( )检测优先级队列是否为空,是返回true,否则返回
false
top( )返回优先级队列中最大(最小元素),即堆顶元素
push(x)在优先级队列中插入元素x
pop()删除优先级队列中最大(最小)元素,即堆顶元素

【注意】
1. 默认情况下,priority_queue是大堆。

代码演示:

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

int main()
{
	//默认less 大堆
	priority_queue<int> pq;//实例化堆
	pq.push(1);//插入值
	pq.push(4);
	pq.push(2);
	pq.push(3);

	//堆不为空时打印堆顶元素,大堆打印出来为降序
	while (!pq.empty())
	{
		cout << pq.top() << " ";//打印堆顶元素
		pq.pop();//删除堆顶元素
	}
	cout << endl;
	return 0;
}

代码测试: 

 通过上面的代码,我们能够简单的使用优先级队列了,但是在STL库中,优先级队列有三个类模板参数,使用如下:

声明:

template <class T, class Container = vector<T>,  
class Compare = less<typename Container::value_type> > 
class priority_queue;

使用: 

priority_queue<int, vector<int>, greater<int>> pq1;
pq1.push(1);
pq1.push(4);
pq1.push(3);
pq1.push(2);
while (!pq1.empty())
{
	cout << pq1.top() << " ";
	pq1.pop();
}
cout << endl;

模板参数解释:

class Container = vector<T>:

这是用来内部存储优先级队列中元素的容器类型。默认是 std::vector,但也可以是其他符合要求的容器类型,比如 std::deque。但是需要注意的是,必须支持随机访问迭代器(Random Access Iterator),以及 front(),push_back(),pop_back() 的操作。

class Compare = less<typename Container::value_type>:

这是用来比较元素优先级的比较函数对象。默认是 std::less,该函数使得最大的元素被认为是最高优先级(形成最大堆)。如果想要最小的元素为最高优先级(形成最小堆),可以通过提供 std::greater 函数对象作为这个模板参数来改变这个行为。

2 仿函数的介绍和使用

优先级队列默认使用less这个仿函数,如果我们需要建立小堆,需要自己传参:

priority_queue<int,vector<int>,greater<int>> pq;

2.1 仿函数的介绍 

什么是仿函数?

在C++中,仿函数是一种使用对象来模拟函数的技术。它们通常是通过类实现的,该类重载了函数调用操作符(operator())。

2.2 仿函数的使用

// 定义一个仿函数模板类
template<class T>
struct Less
{
    // 重载函数调用操作符
	bool operator()(const T& x, const T& y)
	{
		return x < y;
	}
};

int main()
{
	Less<int> lessfunc;//创建有名对象
	cout << lessfunc(1, 2) << endl;//有名对象调用类成员函数
	cout << lessfunc.operator()(1, 2) << endl;//显示调用
	cout << Less<int>()(1, 2) << endl;//匿名对象,前面一个括号构造对象,后面调用函数
	cout << Less<int>().operator()(1, 2) << endl;
	return 0;
}

在上面例子中,我们定义了一个名为 Less 的仿函数类,它重载了 operator() 来实现前面一个数是否小于后面一个数的功能。

在 main 函数中创建了该类的一个实例 lessfunc 并且像调用函数一样使用 lessfunc来比较两数是否属于小于关系。

仿函数广泛用于C++标准库中,特别是在算法(std::sort)中作为比较函数或者操作函数,以及在容器(如 std::set 或者 std::map)中作为排序准则。

举例如下:

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

int main()
{
	vector<int> v = { 1,5,9,7,3,4 };
	//算法库中的排序,默认升序
	sort(v.begin(), v.end());
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;

	//降序,greater >  ,函数传的对象,()为匿名对象
    //函数参数传递的是对象,为了方便,此处使用匿名对象
	sort(v.begin(), v.end(), greater<int>());
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

在上面的例子中,greater 仿函数用来定义一个降序规则,随后在 std::sort 中将其实例化并传递给算法进行降序排序。

仿函数的一个主要优点是它们可以保持状态,这意味着它们可以在多次调用之间保存和修改信息。这使得它们非常灵活和强大。此外,由于它们是类的实例,它们也可以拥有额外的方法和属性。

3 自定义类型的使用

如果在priority_queue中放自定义类型的数据,用户需要在自定义类型中提供> 或者< 的重载。

日期类:

class Date
{
public:
	Date(int year = 1900, 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);
	}
	friend ostream& operator<<(ostream& _cout, const Date& d)
	{
		_cout << d._year << "-" << d._month << "-" << d._day;
		return _cout;
	}
private:
	int _year;
	int _month;
	int _day;
};

代码举例: 

void priority_queue_test3()
{
    // 如果要创建小堆,需要用户提供>的重载
	lin::priority_queue<Date, vector<Date>, lin::Greater<Date>> pq;
	Date d1(2024, 1, 1);//有名对象
	pq.push(d1);
	pq.push(Date(2024,1,3));//匿名对象
	pq.push({2024,1,2});//多参数隐式类型转换
	while (!pq.empty())
	{
		cout << pq.top() << " ";
		pq.pop();
	}
	cout << endl;
}

 测试结果:

4 指针类型的使用

 代码举例:

void priority_queue_test3()
{
	//指针类型,内置类型,不能运算符重载,指针的大小比较是随机的
	lin::priority_queue<Date*, vector<Date*>, lin::Greater<Date*>> pqptr;

	pqptr.push(new Date(2024, 1, 1));
	pqptr.push(new Date(2024, 1, 3));
	pqptr.push(new Date(2024, 1, 2));
	while (!pqptr.empty())
	{
		cout << *(pqptr.top()) << " ";
		pqptr.pop();
	}
	cout << endl;
}

 测试结果:

由于优先级队列中存放的地址,每次动态开辟的地址是随机的,因此运行的结果也是随机的。

我们实际的目的是想通过日期比较大小,即对指针解引用比较大小,解决方案如下:

  • 1、对指针进行运算符重载,但是指针是内置类型,不能运算符重载。
  • 2、使用仿函数,即我们可以使用一个专门对日期类指针比较大小的仿函数或者使用模板类比较大小的仿函数。

日期类仿函数

class GreaterPDate
{
public:
	bool operator()(const Date* p1, const Date* p2)
	{
		return *p1 < *p2;
	}
};

模板类仿函数

template<class T>
class GreaterPT
{
public:
	bool operator()(const T* p1, const T* p2)
	{
		return *p1 < *p2;
	}
};

优化后代码

void priority_queue_test3()
{
	lin::priority_queue<Date*, vector<Date*>, GreaterPDate> pqptr;
    //lin::priority_queue<Date*, vector<Date*>, GreaterPT> pqptr;
	pqptr.push(new Date(2024, 1, 1));
	pqptr.push(new Date(2024, 1, 3));
	pqptr.push(new Date(2024, 1, 2));
	while (!pqptr.empty())
	{
		cout << *(pqptr.top()) << " ";
		pqptr.pop();
	}
	cout << endl;
}

5 priority_queue的模拟实现


priority_queue的底层结构就是堆,因此此处只需对堆进行通用的封装即可。

博主在数据结构专栏详细讲解了堆的实现,此处则不做详细讲解,感兴趣的uu可以看博主数据结构第十一弹---堆,链接如下:

数据结构第十一弹---堆icon-default.png?t=N7T8https://blog.csdn.net/2201_75584283/article/details/135325855

namespace lin
{
	//仿函数  对象像函数一样使用
	//大堆使用小于
	template<class T>
	class Less
	{
	public:
		bool operator()(const T& x, const T& y)
		{
			return x < y;
		}
	};
	//小堆使用大于
	template<class T>
	class Greater
	{
	public:
		bool operator()(const T& x, const T& y)
		{
			return x > y;
		}
	};

	//仿函数默认大堆,使用Less
	template<class T,class Container = vector<T>,class Com = Less<T>>
	class priority_queue
	{
	public:
		//默认大堆
		void adjust_up(size_t child)
		{
			Com com;//创建仿函数对象
			size_t parent = (child - 1) / 2;
			while (child > 0)
			{
				//if (_con[child] > _con[parent])//孩子大于父亲则交换 
				//if (_con[parent] < _con[child])//默认大堆,改为小于
				if(com(_con[parent],_con[child]))//使用仿函数
				{
					swap(_con[child], _con[parent]);
					child = parent;//更新下标
					parent = (parent - 1) / 2;
				}
				else
				{
					break;
				}
			}
		}
		void push(const T& x)
		{
			_con.push_back(x);//尾插数据
			adjust_up(_con.size() - 1);//向下调整
		}
		void adjust_down(size_t parent)
		{
			size_t child = parent * 2 + 1;
			Com com;
			while (child < _con.size())
			{
                //假设法,假设左孩子更大,如果右孩子大则++child
				//if (child + 1 < _con.size() && _con[child] < _con[child + 1])
				if (child + 1 < _con.size() && com(_con[child] , _con[child + 1]))
				{
					++child;
				}
				//if (_con[child] > _con[parent])//孩子大于父亲则交换 
				//if (_con[parent] < _con[child])
				if(com(_con[parent],_con[child]))
				{
					swap(_con[child], _con[parent]);
					parent = child;
					child = parent * 2 + 1;
				}
				else
				{
					break;
				}
			}
		}
		void pop()
		{
			swap(_con[0], _con[_con.size() - 1]);//交换堆顶与最后一个元素
			_con.pop_back();//删除最后一个元素
			adjust_down(0);//向下调整
		}
		const T& top()
		{
			return _con[0];//堆顶元素为第一个元素
		}
		bool empty()
		{
			return _con.empty();
		}
		size_t size()
		{
			return _con.size();
		}
	private:
		Container _con;
	};
}

总结

本篇博客就结束啦,谢谢大家的观看,如果公主少年们有好的建议可以留言喔,谢谢大家啦!

  • 62
    点赞
  • 56
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 55
    评论
评论 55
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小林熬夜学编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值