当前位置: 首页 > news >正文

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(标准模板库)大量使用仿函数来实现其算法的通用性。主要分为以下几类:

  1. 算术运算仿函数std::plus<T>std::minus<T>std::multiplies<T>std::divides<T>std::modulus<T>std::negate<T>
  2. 比较运算仿函数std::equal_to<T>std::not_equal_to<T>std::greater<T>std::greater_equal<T>std::less<T>std::less_equal<T>
  3. 逻辑运算仿函数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() 方法访问的元素)是优先级最高的元素,对于基本数据类型(如 intdouble),这意味着数值最大的元素。

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)对容器「末尾」的操作依赖极强:

  1. push 操作:先将新元素插入容器末尾(push_back),再通过「上浮」调整堆结构。
  2. pop 操作:先交换堆顶(容器首元素)和容器末尾元素,再删除末尾元素(pop_back),最后通过「下沉」调整堆结构。

这意味着:容器的 push_back 和 pop_back 操作效率直接决定了堆操作的性能上限。

对比 std::vector 和 std::deque 的末尾操作性能:

  • std::vectorpush_back/pop_back 在「容量充足」时是 O (1) 操作(仅修改尾指针);即使需要扩容,扩容频率也较低(通常是翻倍扩容,均摊下来仍是 O (1))。
  • std::dequepush_back/pop_back 也是 O (1),但它的底层是缓冲区链表,每次操作可能涉及缓冲区的创建 / 销毁(虽然概率低),且缓冲区管理的开销比 std::vector 简单的连续内存管理略大。

更重要的是:堆操作完全用不到 std::deque 的核心优势—— 高效的 push_front/pop_front(O(1))。std::priority_queue 的 pop 操作看似删除「首元素」,实则通过「交换首尾 + 删除末尾」实现,根本不需要 pop_frontstd::deque 的这一优势被完全浪费。


4.4.3 内存连续性与缓存友好性

std::vector 的连续内存布局带来了一个关键优势:缓存友好性

  • 连续内存中的元素会被操作系统加载到同一个 CPU 缓存行中,堆操作中频繁访问的父子节点大概率在同一缓存行内,能减少「缓存未命中」的概率,从而提升运行速度。
  • std::deque 的分段内存布局导致元素可能分散在不同缓冲区,父子节点可能不在同一缓存行,缓存未命中的概率更高,实际运行效率会略低。

综上:

std::deque 的核心优势是「两端都能高效插入 / 删除」,但 std::priority_queue 的堆操作:

  1. 不需要 push_front/pop_front
  2. 对「随机访问」和「末尾操作」的效率要求极高;
  3. 连续内存的缓存友好性更重要。

std::vector 恰好完美匹配这些需求,而 std::deque 的优势无法发挥,劣势反而会影响性能。因此,C++ 标准库将 std::vector 作为 std::priority_queue 的默认底层容器。


结语

如有不足或改进之处,欢迎大家在评论区积极讨论,后续我也会持续更新C++相关的知识。文章制作不易,如果文章对你有帮助,就点赞收藏关注支持一下作者吧,让我们一起努力,共同进步!

http://www.dtcms.com/a/614500.html

相关文章:

  • 做网站哪些dw使用模板做网站教程
  • 深圳市光明建设发展集团网站网站建设面谈话术
  • Java EE进阶5:Spring IoCDI
  • 中专生学历提升与职业发展指南
  • 易语言怎么反编译 | 如何通过反编译理解易语言的工作原理与破解技巧
  • 阿里国际站韩语网站怎么做百度帐号个人中心
  • EnsembleRetriever中的倒数融合排序算法
  • 网站客户端制作多少钱wordpress导出html
  • 银河麒麟高级服务器系统(V11)的安装部署实操保姆级教程
  • 202552读书笔记|《漫步在晴朗的日子里》——拥有一颗坚定的心去面对朝花夕拾,潮涨潮落
  • 物流查询网站开发青岛网站建设好不好
  • C#20、什么是LINQ
  • Springboot加盟平台推荐可视化系统ktdx2ldg(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
  • 公网动态ip如何做网站网站项目建设周期
  • 路由器选择需关注无线传输速率、端口配置与信号覆盖
  • php网站建设论文答辩温州手机建站模板
  • 达梦的dbms_lock在DSC中能用吗
  • 前端微前端部署方案,Nginx与Webpack
  • 网站建站系统ps软件下载电脑版多少钱
  • c++ easylogging 使用示例
  • Holdout机制:推荐系统中评估部门级业务贡献的黄金标准
  • 地域性旅游网站建设系统结构品牌公司网站设计
  • 4k中国视频素材网站wordpress用哪个版本
  • 计算机网络应用层
  • 写资料的网站有哪些宽屏公司网站源码php
  • 网站开发 验收移交写网站建设的软文
  • C语言编译器App介绍与使用指南
  • Clang与GCC链接机制解析:从标准库选择到跨平台编译
  • 【ZeroRange WebRTC】WebRTC拥塞控制技术深度分析
  • 网站动态背景怎么做国际新闻今天