优先级队列:priority_queue

优先级队列

1、简单认识

优先级队列,其底层的逻辑结构,就是二叉树的堆

优先级队列的使用需要包含头文件:#include<queue>

优先级队列的核心接口,依旧是我们熟悉的:

  • push
  • pop
  • top

只不过,pop和top的作用对象,是优先级高的对象。

1.1、仿函数的简单认识

上面代码中,我们使用优先级队列,其实默认传入了一个less模板:

less类模板,本质上重载了操作符():

bash 复制代码
template<class T>
struct Less
{
	bool operator() (const T& x, const T& y) const { return x < y; }
};

我们利用模板类实例化出一个对象,并使用:

如果我们单看less(1, 2),我们很容易就会认为这是一个函数调用。

我们可以理解为这种行为是用来代替函数指针的,因为重载()的类实例化出来的对象可以像函数一样使用。

这种重载()的类,我们也叫做仿函数。

我们可以用仿函数less,规定优先级队列的优先级为:数值大的数。那么就可以成大堆,排降序:

也可以用仿函数greater,规定优先级队列的优先级为:数值小的数。那么就可以成小堆,排升序:

2、模拟实现

由于我们还没有系统学习仿函数,这里我们的模拟实现就先统一调整为大堆。

基本架构

cpp 复制代码
template<class T, class Container = vector<T>>
class priority_queue
{
public:
	priority_queue() = default;// 强制编译器生成默认的构造函数
	
private:
	Container _con;
};

push

push对于大根堆的思路:

  • push_back
  • 向上调整
cpp 复制代码
void AdjustUp(int child)
{
	while (child > 0)
	{
		int parent = (child - 1) / 2;
		if (_con[parent] < _con[child])
		{
			std::swap(_con[parent], _con[child]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

void push(const T& val)
{
	_con.push_back(val);
	AdjustUp(_con.size() - 1);
}

pop

pop对于大根堆的思路:

  • 首尾交换
  • pop_back
  • 向下调整
cpp 复制代码
void AdjustDown(int parent, int size)
{
	int child = parent * 2 + 1;
	while (child < size)// 如果是parent < size,会越界
	{
		if (child + 1 < size && _con[child] < _con[child + 1])
			++child;
		if (_con[parent] < _con[child])// 
		{
			std::swap(_con[parent], _con[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void pop()
{
	std::swap(_con[0], _con[_con.size() - 1]);
	_con.pop_back();// 
	AdjustDown(0, _con.size());
}

迭代器区间构造

这里我们可以先调用_con自己的迭代器区间构造。

由于传入的序列不一定排成了大堆,所以我们还是要调整。

cpp 复制代码
// 迭代器区间构造
template<class InputIterator>
priority_queue(InputIterator first, InputIterator last)
	:_con(first, last)
{
	int n = _con.size();
	for (int i = (n - 1 - 1)/2; i >= 0; --i)
	{
		AdjustDown(i, n);
	}
}

其它的重要接口

cpp 复制代码
T& top() { return _con[0]; }

bool empty() const { return _con.size() == 0; }

2.1、仿函数

我们之前简单认识了仿函数。仿函数其实是一个类(模板类),里面重载了操作(),使得我们可以实例化出一个对象,这个对象调用重载(),就可以达到我们想要的效果。

2.1.1、应用举例

我们上面的代码,其实是固定了必须调成大堆,那么我们是否可以修改代码,使得我们自行确定优先级是什么,以使代码更灵活?

我们就在模板类型参数,多加一个仿函数类:

cpp 复制代码
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:
	priority_queue() = default;
	
private:
	Container _con;
	Compare _com;// 仿函数对象
};

然后我们修改向上、向下调整算法,使得我们能够自由定义优先级是什么:(比大?比小?)

cpp 复制代码
void AdjustUp(int child)
{
	while (child > 0)
	{
		int parent = (child - 1) / 2;
		if (_com(_con[parent], _con[child]))// 仿函数
		{
			std::swap(_con[parent], _con[child]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}
cpp 复制代码
void AdjustDown(int parent, int size)
{
	int child = parent * 2 + 1;
	while (child < size)
	{
		if (child + 1 < size && _com(_con[child], _con[child + 1]))// 仿函数
			++child;
		if (_com(_con[parent], _con[child]))// 仿函数
		{
			std::swap(_con[parent], _con[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

当我们想建大堆,排降序:

当我们想建小堆,排升序:

2.1.2、应用场景举例

仿函数对于比较大小的应用使用场景有两个:

  • 对象不支持直接比较大小
  • 对象可以支持直接比较大小,但比较的结果不是我们期望的

比如,我们引入日期类,创建一个优先级队列,然后放入三个日期类(指针):

cpp 复制代码
void test2()
{
	priority_queue<Date*, vector<Date*>> pq;
	pq.push(new Date(2026, 5, 2));
	pq.push(new Date(2026, 5, 3));
	pq.push(new Date(2026, 5, 4));
}

然后,我们按照经验,排一个降序:



排了三次,每次都不一样?

要知道我们当前进行排序的是Date指针,Date指针的大小是随机的。

所以我们应该另外写一个仿函数,实现真正的比较大小的需求:

再比如,我们观察算法库方法remove_if()

remove()方法是传入特定的值,然后在一段迭代器区间中删除这个特定的值。

但是remove_if()可以实现的不只有删除特定值,还可以实现其它的删除方法。

比如删除所有的偶数:

cpp 复制代码
template<class T>
struct DelEven
{
	bool operator() (const T& x) const { return x % 2 == 0; }
};
void test4()
{
	list<int> lt1 = { 1,2,3,4,4,5,6,6,6,7 };
	for (auto& e : lt1)
	{
		cout << e << " ";
	}cout << endl;
	lt1.remove_if(DelEven<int>());
	for (auto& e : lt1)
	{
		cout << e << " ";
	}cout << endl;
}

我们也会发现一点,算法库方法中,如果出现_if,很可能需要传入仿函数对象。

算法库remove_if的使用问题。


我们上面举例使用的remove_if是list的成员函数。

而我们直接使用算法库的remove_if,结果是:

前面四个,是我们要的奇数,那后面的呢?


我们再好好想一想,当前remove_if的作用是什么:删除所有偶数。

删除偶数,意味着实际lt1被remove_if()操作完后,就只剩下1, 3, 5, 7;而我们直接打印,结果打印了与原来序列一样长的序列。

说明当前lt1的end()失效了。那么我们可以怎么补救呢?


我们看官方对于算法库remove_if返回值的解释:

当first指向的数不满足被删的要求,并且result与first不是指向同一个位置时,first指向值赋值给result指向值,然后result跳到下一步。

这很像我们之前做过的删除指定数 的算法题。而result标记的就是处理好的序列末位的下一个位置。

所以我们的补救措施是:重新定义end(),接收remove_if的返回值,再用于打印:

2.1.3、仿函数类型与对象的区分

cpp 复制代码
void test3()
{
	priority_queue<int, vector<int>, Less<int>> pq;
	vector<int> v;
	v.push_back(3);
	v.push_back(5);
	v.push_back(2);
	v.push_back(4);
	sort(v.begin(), v.end(), Less<int>());
}

上面代码中,Less<int>Less<int>()的区别是什么?

我们看看priority_queue和sort的描述:


我们就能明白,Less<int>是仿函数类型,Less<int>()是仿函数类实例化出的匿名对象。

相关推荐
jieyucx1 小时前
Go 零基础数据结构:顺序表(像「排抽屉」一样学增删改查)
java·数据结构·golang
曦夜日长1 小时前
C++ STL容器string(一):string的变量细节、默认函数的认识以及常用接口的使用
java·开发语言·c++
代码中介商1 小时前
C++ STL 标准模板库完全指南:从容器到迭代器
开发语言·c++·stl
winner88811 小时前
C++ 构造函数、析构函数、虚函数、虚析构
开发语言·c++
想唱rap1 小时前
应用层协议与序列化
linux·运维·服务器·网络·数据结构·c++·算法
许长安1 小时前
protobuf 使用详解
c++·经验分享·笔记·中间件
Soley1 小时前
用 Boost.Log 封装一个更顺手的 C++17 日志库:GoodLog
c++
HAPPY酷2 小时前
从Public到Private:UE5 C++类创建路径差异全解析
java·c++·ue5
无敌昊哥战神2 小时前
【LeetCode 37】解数独 (Sudoku Solver) —— 回溯法详解 (Python/C/C++)
c语言·c++·python·算法·leetcode