C++—priority_queue/仿函数:优先级队列的使用及模拟实现
目录
一. 仿函数
1.1 仿函数的定义
1.2 仿函数的核心特点
1.3 为什么使用仿函数?(与普通函数 / 函数指针的对比)
1) 状态保持能力(最核心优势)
2)可作为模板参数
3)性能优势
1.4 仿函数在 STL 中的应用
1.5 仿函数传递时的注意点
1.5.1 作类模板参数,传递的是实例化类型
1.5.2 作函数参数,传递的是对象
1.6 仿函数的使用
1.6.1 库中的仿函数的使用
1)库中的仿函数对象作函数参数
2)库中的仿函数作模板参数
1.6.2 库中的仿函数都不能满足我们的需求,自己写仿函数
二. priority_queue的介绍
2.1 什么是优先队列(Priority Queue)?
2.2 基本特性
三. priority_queue的使用
四. prioroty_queue的底层实现
4.1 priority_queue常用接口的实现
4.2 用于控制大小比较的仿函数的实现
4.3 向上调整算法与向下调整算法
4.4 为什么优先级队列底层容器默认使用vector,而不是deque
4.4.1 堆操作对「随机访问」的强依赖
4.4.2 堆操作对「末尾操作」的偏好
4.4.3 内存连续性与缓存友好性
引言
在学习优先级队列前,我要先学习仿函数的相关知识,它不仅在容器中有所使用,在算法库中等地方也有许多的用处。理解仿函数的概念对于深入掌握 C++ 模板编程、STL 等的工作原理至关重要。
一. 仿函数
1.1 仿函数的定义
仿函数(Functor) 是一个重载了函数调用运算符 operator() 的类或结构体。
当你创建这个类的对象后,你可以像使用普通函数一样,通过在对象名后面加上括号 () 并传递参数来调用这个重载的 operator() 方法。
1.2 仿函数的核心特点
行为类似函数:可以像普通函数一样被调用,语法相同。
本质是类 / 结构体:这意味着它可以拥有成员变量和成员函数,可以保存状态。
可作为模板参数:这是仿函数在 C++ 中最重要的用途之一,尤其是在 STL 中。
1.3 为什么使用仿函数?(与普通函数 / 函数指针的对比)
1) 状态保持能力(最核心优势)
普通函数无法轻易地在多次调用之间保持状态,除非使用全局变量或静态变量,这会带来副作用且线程不安全。而仿函数可以通过其成员变量轻松、安全地保持状态
2)可作为模板参数
C++ 的模板机制可以接受类型作为参数。仿函数是一个类(一种类型),而普通函数名或函数指针是一个值。将仿函数类型作为模板参数,可以让算法(如 STL 算法)变得异常灵活和高效。
3)性能优势
与函数指针相比,仿函数的调用通常可以被编译器内联(inline),从而消除函数调用的开销。而函数指针的调用是否能被内联,取决于编译器的优化能力,通常不如仿函数可靠。
1.4 仿函数在 STL 中的应用
STL(标准模板库)大量使用仿函数来实现其算法的通用性。主要分为以下几类:
- 算术运算仿函数:
std::plus<T>,std::minus<T>,std::multiplies<T>,std::divides<T>,std::modulus<T>,std::negate<T> - 比较运算仿函数:
std::equal_to<T>,std::not_equal_to<T>,std::greater<T>,std::greater_equal<T>,std::less<T>,std::less_equal<T> - 逻辑运算仿函数:
std::logical_and<T>,std::logical_or<T>,std::logical_not<T>
这些仿函数定义在 <functional> 头文件中。
1.5 仿函数做参数传递时的注意点
1.5.1 作类模板参数,传递的是实例化类型

int main()
{Priority::priority_queue<int, vector<int>, greater<int>> pq;pq.push(3);pq.push(2);pq.push(7);pq.push(6);pq.push(8);while (!pq.empty()){cout << pq.top() << endl;pq.pop();}
}
1.5.2 作函数参数,传递的是对象

int main()
{vector<int> v1 = { 1,5,6,3,7,3,8,0 };// < 升序// > 降序/*greater<int> gt;sort(v1.begin(), v1.end(), gt);*/sort(v1.begin(), v1.end(), greater<int>()); // 直接传匿名对象for (auto e : v1){cout << e << " ";}cout << endl;return 0;
}
1.6 仿函数的使用
我们既可以使用C++提供的仿函数,也可以自己写仿函数来使用;既可以作函数参数,也可以作类模板的实例化类型
1.6.1 库中的仿函数的使用
1)库中的仿函数对象作函数参数
int main()
{vector<int> v1 = { 1,5,6,3,7,3,8,0 };// < 升序// > 降序/*greater<int> gt;sort(v1.begin(), v1.end(), gt);*/sort(v1.begin(), v1.end(), greater<int>()); // 直接传匿名对象for (auto e : v1){cout << e << " "; // 降序}cout << endl;
}
2)库中的仿函数作模板参数
int main()
{Priority::priority_queue<int, vector<int>, greater<int>> pq;pq.push(3);pq.push(2);pq.push(7);pq.push(6);pq.push(8);while (!pq.empty()){cout << pq.top() << endl; // 降序pq.pop();}
}
1.6.2 库中的仿函数都不能满足我们的需求,自己写仿函数
我们先来看一下以下场景:
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;
};
如果实例化为指针类型,那么我们想要对指针指向的数据进行排序,如果用它默认提供的仿函数就会有问题
int main()
{P::piority_queue<Date*> pq; // 如果用默认的仿函数,就是用指针进行比较,没意有义pq.push(new Date(2025, 1, 2));pq.push(new Date(2025, 1, 1));pq.push(new Date(2025, 1, 3));// 因为new是在对上开空间,然后返回一个指针,所以得到的指针都是随机的,不存在大小关系// 所以解引用后的顺序也是随机的// 综上:仿函数中用指针比较时没有意义的while (!pq.empty()){cout << *pq.top() << " "; // 注意这里要解引用pq.pop();}return 0;
}
我们应该自己写一个仿函数来控制
struct PDateLess
{bool operator()(const Date* p1, const Date* p2){// 日期类中重载了对应的比较逻辑return *p1 < *p2;// 如果没有重载,那我们就要访问它的成员,自己实现比较逻辑}
};
因为优先级队列底层仿函数模板传的是缺省参数,所以我们可以直接将自己写的仿函数传过去
int main()
{P::piority_queue<Date*, vector<Date*>, PDateLess> pq; // 用指针指向的对象比较才有意义pq.push(new Date(2025, 1, 2));pq.push(new Date(2025, 1, 1));pq.push(new Date(2025, 1, 3));while (!pq.empty()){cout << *pq.top() << " ";pq.pop();}return 0;
}
二. priority_queue的介绍
2.1 什么是优先队列(Priority Queue)?
优先队列是一种抽象数据类型(ADT),它类似于普通的队列(Queue),但每个元素都有一个与之关联的 “优先级”。在优先队列中,优先级高的元素会比优先级低的元素先被访问和处理,而不是像普通队列那样遵循严格的先进先出顺序。在 C++ 标准库中,std::priority_queue 是优先队列的实现,它被定义在 <queue> 头文件中。
你可以把它想象成一个总是先处理 “最重要” 任务的任务调度系统。它具有以下特点:
- 优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素 中最大的。
- 基于堆(Heap)数据结构实现的,在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶 部的元素)。
- 优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类。
- 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。
- 标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue 类实例化指定容器类,则使用vector。
- 需要支持随机访问迭代器,以便始终在内部保持堆结构。
2.2 基本特性
1. 底层实现:std::priority_queue 通常是基于堆(Heap)数据结构实现的,在 C++ 中默认是最大堆(Max-Heap)。
2. 元素顺序:默认情况下,队列顶部(top() 方法访问的元素)是优先级最高的元素,对于基本数据类型(如 int, double),这意味着数值最大的元素。
3. 核心操作:
push(): 向队列中插入一个元素,并根据优先级重新排序。pop(): 移除队列顶部的元素(优先级最高的元素)。top(): 返回队列顶部元素的引用,但不移除它。empty(): 判断队列是否为空。size(): 返回队列中元素的数量。
三. priority_queue的使用
优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用 priority_queue。注意:默认情况下priority_queue是大堆。
| 函数声明 | 接口说明 |
| priority_queue() / priority_queue(first, last) | 构造一个空的优先级队列 |
| empty() | 检测优先级队列是否为空,是返回true,否则返回false |
| size() | 优先级队列中元素个数 |
| top() | 返回优先级队列中最大(最小元素),即堆顶元 素 |
| push(x) | 在优先级队列中插入元素x |
| pop() | 删除优先级队列中最大(最小)元素,即堆顶元素 |
int main()
{// priority_queue<int> pq; // 默认是大的优先级高(大堆)priority_queue<int, vector<int>, greater<int>> pq; // 调整小的优先级高(小堆)// 容器默认是vector<T> ,用deque也可以// 但由于优先级队列的是底层是堆,会大量的进行下标随机访问,比较父子大小,所以用vector效率比较高pq.push(3);pq.push(1);pq.push(5);pq.push(7);pq.push(2);while (!pq.empty()){cout << pq.top() << " ";pq.pop();}cout << endl;return 0;
}
四. prioroty_queue的底层实现
priority_queue 的底层核心是堆(Heap)数据结构,而堆又是基于完全二叉树(Complete Binary Tree)实现的。C++ 标准库默认使用最大堆(Max-Heap),这意味着堆顶元素总是优先级最高的(对于基本类型,就是值最大的)。priority_queue的底层实际上是对容器的封装,并加上仿函数加以控制。
4.1 priority_queue常用接口的实现
namespace Priority
{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 = std::vector<T>, class Compare = Less<T>> // 通过仿函数控制内部的比较逻辑(像开关一样)class priority_queue{public:// 支持迭代器区间初始化(栈和队列不支持)template <class InputIterator>priority_queue(InputIterator first, InputIterator last):_con(first, last) // 调用容器的迭代器区间初始化的构造,此时还不是堆{// 法1.可以遍历一遍,然后push_back不断向上调整,但是向上调整建堆的效率不太好// 法2.向下调整算法建堆:找到最后一个不是叶子的节点(即最后一个节点的父亲),向下调整,然后--,倒着走for (int i = (_con.size() - 1 - 1) / 2; i >= 0; --i){adjust_down(i);}}// 默认构造priority_queue() = default; // 强制编译器生成默认构造// 会调用_con的默认构造void push(const T& x){_con.push_back(x);adjust_up(_con.size() - 1);}// 优先级队列删除数据为什么不能直接往前挪动数据?// 1、效率很低,尤其是vector// 2、挪动后数据就全乱了,需要重新建堆// 优先队列要先出优先级高的,默认是最大的,所以要出堆顶void pop(){// swap(_con[0], _con[_con.size() - 1]);swap(_con.front(), _con.back());_con.pop_back();adjust_down(0);}const T& top() const{return _con[0];}bool empty() const {return _con.empty();}size_t size() const{return _con.size();}private:Container _con;};
}
4.2 用于控制大小比较的仿函数的实现
namespace Priority
{template<class T>struct Less{bool operator()(const T& x, const T& y){return x < y;}};template<class T>struct Greater{bool operator()(const T& x, const T& y){return x > y;}};
}
4.3 向上调整算法与向下调整算法
namespace Priority
{class priority_queue{void adjust_up(int child){Compare com;int 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 = (child - 1) / 2;}elsebreak;}}void adjust_down(int parent){Compare com;int child = parent * 2 + 1;while (child < _con.size()){// if (child + 1 < _con.size() && _con[child + 1] > _con[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[parent] < _con[child])if (com(_con[parent], _con[child])){swap(_con[child], _con[parent]);parent = child;child = parent * 2 + 1;}elsebreak;}}}
}
4.4 为什么优先级队列底层容器默认使用vector,而不是deque
4.4.1 堆操作对「随机访问」的强依赖
std::priority_queue 的底层是堆(完全二叉树),而堆的核心操作(上浮、下沉)需要频繁通过「索引」访问父节点和子节点
对比 std::vector 和 std::deque 的随机访问性能:
std::vector:元素存储在连续内存中,随机访问是「直接地址计算」,效率极高(O (1) 无额外开销)。std::deque:元素存储在「分段连续内存」中(由多个固定大小的缓冲区组成),随机访问需要先计算元素所在的缓冲区,再定位缓冲区内部的位置。虽然理论上也是 O (1),但实际开销比std::vector大(多了一层缓冲区索引的计算)。
堆操作(上浮 / 下沉)中,每个元素可能需要多次访问父子节点,std::vector 更高效的随机访问能显著提升堆操作的整体性能。
4.4.2 堆操作对「末尾操作」的偏好
堆的两个核心操作(push 和 pop)对容器「末尾」的操作依赖极强:
push操作:先将新元素插入容器末尾(push_back),再通过「上浮」调整堆结构。pop操作:先交换堆顶(容器首元素)和容器末尾元素,再删除末尾元素(pop_back),最后通过「下沉」调整堆结构。
这意味着:容器的 push_back 和 pop_back 操作效率直接决定了堆操作的性能上限。
对比 std::vector 和 std::deque 的末尾操作性能:
std::vector:push_back/pop_back在「容量充足」时是 O (1) 操作(仅修改尾指针);即使需要扩容,扩容频率也较低(通常是翻倍扩容,均摊下来仍是 O (1))。std::deque:push_back/pop_back也是 O (1),但它的底层是缓冲区链表,每次操作可能涉及缓冲区的创建 / 销毁(虽然概率低),且缓冲区管理的开销比std::vector简单的连续内存管理略大。
更重要的是:堆操作完全用不到 std::deque 的核心优势—— 高效的 push_front/pop_front(O(1))。std::priority_queue 的 pop 操作看似删除「首元素」,实则通过「交换首尾 + 删除末尾」实现,根本不需要 pop_front,std::deque 的这一优势被完全浪费。
4.4.3 内存连续性与缓存友好性
std::vector 的连续内存布局带来了一个关键优势:缓存友好性。
- 连续内存中的元素会被操作系统加载到同一个 CPU 缓存行中,堆操作中频繁访问的父子节点大概率在同一缓存行内,能减少「缓存未命中」的概率,从而提升运行速度。
std::deque的分段内存布局导致元素可能分散在不同缓冲区,父子节点可能不在同一缓存行,缓存未命中的概率更高,实际运行效率会略低。
综上:
std::deque 的核心优势是「两端都能高效插入 / 删除」,但 std::priority_queue 的堆操作:
- 不需要
push_front/pop_front; - 对「随机访问」和「末尾操作」的效率要求极高;
- 连续内存的缓存友好性更重要。
std::vector 恰好完美匹配这些需求,而 std::deque 的优势无法发挥,劣势反而会影响性能。因此,C++ 标准库将 std::vector 作为 std::priority_queue 的默认底层容器。
结语
如有不足或改进之处,欢迎大家在评论区积极讨论,后续我也会持续更新C++相关的知识。文章制作不易,如果文章对你有帮助,就点赞收藏关注支持一下作者吧,让我们一起努力,共同进步!
