优先级队列
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>()是仿函数类实例化出的匿名对象。


