☰
priority_queue的介绍和使用
2026/10/2 17:20:35 网站建设 项目流程

优先队列的基本概念与实现原理

priority_queue文档介绍

优先队列是一种容器适配器,其核心特性是始终保证队列中的第一个元素(即队首元素)为当前所有元素中优先级最高的一个。在默认情况下,该“最高优先级”表现为数值上的最大值,因此优先队列在默认配置下表现为大根堆结构。这种设计使得优先队列特别适用于需要频繁获取最大(或最小)元素的场景,如任务调度、图算法中的最短路径计算等。

优先队列的底层实现基于堆(heap)数据结构,而堆是一种完全二叉树结构,满足父节点的值大于等于(或小于等于)其子节点值的性质。通过维护这一性质,可以确保每次访问顶部元素时都能快速获得全局最大(或最小)值,时间复杂度为常数级别O ( 1 ) O(1)O(1)。

底层容器的选择与要求

优先队列作为容器适配器,将特定的标准容器类封装为其底层存储结构。这意味着它并不直接管理数据,而是依赖于所选容器来完成实际的数据存储和操作。常见的底层容器包括std::vector和std::deque,二者均满足优先队列对容器的基本要求:

  • 支持随机访问迭代器;
  • 提供empty()接口以判断容器是否为空;
  • 提供size()接口返回有效元素数量;
  • 提供front()接口访问首个元素;
  • 支持push_back()向尾部插入元素;
  • 支持pop_back()删除尾部元素。

这些接口共同保障了堆结构的动态构建与维护。由于堆操作依赖于随机访问能力(例如在调整堆结构时需快速定位父节点或子节点),因此必须使用支持随机访问迭代器的容器。

默认容器与堆算法的自动维护机制

当未显式指定底层容器时,优先队列默认使用std::vector作为其存储容器。std::vector具有良好的内存连续性、高效的插入与访问性能,且支持随机访问,非常适合用于堆的实现。

为了维持堆的有序性,优先队列内部通过调用标准库中的三个关键算法函数实现自动维护:

  • make_heap(first, last):将一段范围内的元素构造成一个合法的堆;
  • push_heap(first, last):在已构成堆的基础上,向末尾添加新元素并重新调整堆结构;
  • pop_heap(first, last):将堆顶元素移至末尾,并重新调整剩余元素形成新的堆。

这些操作由优先队列的成员函数(如push()、pop())在内部自动触发,用户无需手动干预。这使得优先队列既具备高效性,又具有良好的封装性。

优先队列的核心接口详解

函数声明功能说明
priority_queue()/priority_queue(first,last)构造一个空的优先队列;也可从指定范围内的元素初始化队列,此时会自动调用make_heap构建初始堆结构。
empty()判断优先队列是否为空。若无元素则返回true,否则返回false。时间复杂度为O ( 1 ) O(1)O(1)。
top()返回当前优先队列中优先级最高的元素(即堆顶元素)。该操作不改变队列内容,仅读取,时间复杂度为O ( 1 ) O(1)O(1)。注意:若队列为空,调用此函数可能导致未定义行为。
push(const T& x)将元素x插入优先队列。插入后自动调用push_heap保持堆结构,时间复杂度为O ( log ⁡ n ) O(\log n)O(logn)。
pop()移除堆顶元素。内部先执行pop_heap将最大元素移到末尾,再调用pop_back实际删除,时间复杂度为O ( log ⁡ n ) O(\log n)O(logn)。

优先队列的本质:堆的封装

尽管优先队列提供了类似队列的操作语义(先进后出的逻辑顺序),但其实质是堆的高级封装。它利用堆的特性实现了“按优先级出队”的行为,而非传统的“先进先出”。因此,在任何需要动态维护最大值或最小值的场合,都可以考虑使用优先队列替代手动维护堆结构。

例如:

  • 在 Dijkstra 算法中,需要不断取出距离最小的节点;
  • 在合并多个有序链表时,需选择当前最小头节点;
  • 在实时系统中,优先处理高优先级任务。

上述场景均可通过优先队列高效解决。

自定义比较规则:小根堆的实现

默认情况下,优先队列是大根堆(即堆顶为最大值)。若需实现小根堆(堆顶为最小值),可通过自定义比较函数进行配置。例如:

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

其中greater<int>是一个模板仿函数,表示“较小者优先”,从而使得队列顶部始终为最小元素。

更复杂的自定义比较逻辑也可通过定义结构体或函数对象实现,灵活适应不同应用场景。

总结重点

  • 优先队列是基于堆结构的容器适配器,提供高效的最大/最小元素访问;
  • 默认使用std::vector作为底层容器,支持随机访问与动态扩展;
  • 所有堆操作均由内部算法自动完成,用户无需关心具体实现细节;
  • 核心操作如push()、pop()、top()均具有良好的时间复杂度保障;
  • 可通过自定义比较器切换为小根堆,增强适用性;
  • 优先队列本质上就是堆的封装,适用于所有需要动态维护极值的算法场景。

该设计兼顾了效率、灵活性与易用性,是现代 C++ 编程中不可或缺的重要工具之一。


priority_queue的模拟实现

priority_queue模拟实现

仿函数(Functor)的设计与实现原理

一个类若重载了operator(),其对象便具备了像函数一样被调用的能力。这种对象称为仿函数(Function Object),也叫函数对象或函数式对象。

template<classT>classLess{public:booloperator()(constT&x,constT&y)const{returnx<y;}};
  • operator()的重载使得Less<int> lessFunc;创建的对象可以像函数一样使用:
    lessFunc(a, b)等价于lessFunc.operator()(a, b)。
  • 返回类型为bool比int更加语义清晰,符合比较操作的逻辑预期。
  • 由于是模板类,Less<int>、Less<double>等可实例化为不同类型的比较器,只要元素支持<操作符即可。
为什么使用仿函数而非普通函数指针?
特性仿函数函数指针
可内联优化✅ 编译期确定具体类型,可直接展开❌ 通常无法内联,运行时跳转开销
支持状态✅ 可包含成员变量(如计数器、缓存等)❌ 无状态,仅函数地址
类型即策略✅ 比较方式作为模板参数传递,编译期配置❌ 运行时传入函数地址,灵活性差
性能⭐ 零开销(空类可被完全优化)⚠️ 调用开销不可忽略

示例:

priority_queue<int,vector<int>,Greater<int>>pq;// Compare = Greater<int> 是一个类型,它在编译时决定比较行为

这正是 STL 的核心设计思想:将策略抽象为类型,通过模板实现“类型即配置”。


priority_queue模板类结构解析

template<classT,classContainer=vector<T>,classCompare=Greater<T>>classpriority_queue{private:Container _con;// 底层容器Compare _com;// 比较器对象(仿函数)// 私有辅助函数voidadjust_up(intchild);voidadjust_down(intparent);public:// 构造函数priority_queue()=default;template<classInputIterator>priority_queue(InputIterator first,InputIterator last):_con(first,last){// 建堆:从最后一个非叶子节点开始向下调整for(inti=(_con.size()-1-1)/2;i>=0;--i)adjust_down(i);}// 插入元素voidpush(constT&x);// 删除堆顶voidpop();// 获取堆顶元素constT&top()const;// 判空与大小boolempty()const{return_con.empty();}size_tsize()const{return_con.size();}};
模板参数详解
参数含义默认值
T元素类型——
Container底层容器类型(必须支持随机访问和push_back)vector<T>
Compare比较器类型(仿函数)Greater<T>

关键点:默认使用Greater<T>,意味着该优先队列是一个大顶堆(最大元素在顶部)。


建堆过程:heapify算法详解

构造函数中:

template<classInputIterator>priority_queue(InputIterator first,InputIterator last):_con(first,last){for(inti=(_con.size()-1-1)/2;i>=0;--i)adjust_down(i);}
1. 为何从(n-1-1)/2开始?
  • 完全二叉树中,下标从0开始。
  • 最后一个节点索引为n-1。
  • 其父节点索引为(n-1 - 1) / 2 = (n-2)/2。
  • 所以最后一个非叶子节点的下标是(n-2)/2,即(_con.size() - 1 - 1) / 2。
2. 时间复杂度分析
  • 若逐个插入并向上调整:O(n log n)
  • 使用adjust_down从底向上建堆:O(n)
  • 证明基于每层节点的下沉高度总和,数学上可证为线性。

举例:对数组{4, 1, 3, 2, 16, 9, 10, 14, 8, 7}建堆,只需从第4个节点(索引4)开始调整。


插入操作:push与adjust_up

voidpush(constT&x){_con.push_back(x);// 放到末尾(完全二叉树最后位置)adjust_up(_con.size()-1);// 向上调整至合适位置}
向上调整逻辑(adjust_up)
voidadjust_up(intchild){Compare com;// 每次构造比较器对象size_t parent=(child-1)/2;while(child>0){if(com(_con[parent],_con[child])){swap(_con[child],_con[parent]);child=parent;parent=(child-1)/2;}else{break;}}}
核心解读:
  • 父节点公式:parent = (child - 1) / 2
  • 循环条件:child > 0,直到到达根节点为止。
  • com(parent, child)返回true表示:父节点“不如”子节点→ 需要交换,让子节点上浮。
  • 交换后继续向上检查。
重要结论:
  • 当Compare = Greater<T>时,com(a,b)即a > b。
    • com(parent, child)为真 ⇒parent > child⇒ 父比子大?但还要交换?
    • 实际上,这里com(a,b)的语义是:“a 是否应该让位给 b?”
    • 因此,当parent > child为真时,说明父更大,应保留;但代码却执行了交换!

矛盾吗?不!

我们来澄清这个关键点:

if(com(_con[parent],_con[child]))
  • com(a, b)为真 ⇒a应该让位给b⇒b更优 ⇒b应上浮。
  • 所以:如果parent > child且Compare = Greater,则com(parent, child) == true⇒ 交换 ⇒child上浮。

这意味着:只有当子节点更“小”时才让它上浮,最终形成的是小堆(最小元素在顶部)。

但这与标准priority_queue的行为相反!

🚩 重大发现:本实现与标准库行为相反
比较器堆性质输出顺序
Greater<T>小堆(最小值在顶)升序出队
Less<T>大堆(最大值在顶)降序出队

而标准库std::priority_queue默认使用std::less<T>,即大堆。

所以:当前实现中,Greater<T>对应小堆,Less<T>对应大堆,与标准库互补。

这是由com(a,b)的语义决定的:

“如果 a 不如 b,就交换” → 即“让 b 上浮”,所以com(a,b)为真表示b更优。

因此,堆的形状取决于比较器如何定义“谁更优”。


删除操作:pop与adjust_down

voidpop(){swap(_con[0],_con[_con.size()-1]);// 1. 交换堆顶与末尾_con.pop_back();// 2. 移除末尾(原堆顶)adjust_down(0);// 3. 新堆顶向下调整}
为什么不能直接删除index=0?

index=0 意味着你正在访问一个数据序列中的第一个元素。它不是“第零个”元素,而是“第一个”,只是编号方式从 0 开始。这种设计广泛应用于编程语言、数据分析工具和算法实现中。

  • 会破坏完全二叉树结构。
  • 必须保证树的连续性和层级完整性。
  • 正确做法:将堆顶替换为最后一个元素,再从根开始向下调整。
向下调整逻辑(adjust_down)
voidadjust_down(intparent){Compare com;size_t child=parent*2+1;// 左孩子while(child<_con.size()){// 选择两个孩子中更“优”的那个(即更应该上浮的那个)if(child+1<_con.size()&&com(_con[child],_con[child+1]))child++;// 右孩子更优,选右// 如果父节点不如孩子,则交换并继续下沉if(com(_con[parent],_con[child])){swap(_con[child],_con[parent]);parent=child;child=parent*2+1;}else{break;}}}
详细解释:
  • child = parent * 2 + 1:左孩子下标。
  • child + 1 < _con.size():判断右孩子是否存在。
  • com(_con[child], _con[child + 1]):若为真,说明左孩子“不如”右孩子 → 选右孩子。
  • 然后判断com(_con[parent], _con[child]):若为真,说明父节点“不如”孩子 → 交换。
举个例子:

假设Compare = Greater<T>,即com(a,b)为真当且仅当a > b。

  • com(left, right)为真 ⇒left > right⇒right更小 ⇒ 更优 ⇒ 选右孩子。
  • com(parent, child)为真 ⇒parent > child⇒ 父更大 ⇒ 不应留在上面 ⇒ 交换。

结果:较小的元素不断上浮,形成小堆。


接口封装与性能保障

constT&top()const{return_con[0];}boolempty()const{return_con.empty();}size_tsize()const{return_con.size();}
  • top():常量引用返回,避免拷贝,时间复杂度O(1)。
  • empty()/size():转发到底层容器,无额外开销。

总结:核心机制与设计哲学

机制说明
仿函数驱动比较逻辑由Compare控制,支持自定义策略
类型即配置比较方式作为模板参数,编译期决定行为
堆性质由比较器定义com(a,b)为真 ⇒b应上浮 ⇒ 决定堆的形态
大堆/小堆切换Less<T>→ 大堆(降序输出)
Greater<T>→ 小堆(升序输出)
建堆效率O(n)而非O(n log n),利用 heapify 算法
内存布局底层容器为vector<T>,支持随机访问,便于堆操作

完整测试示例(验证行为)

#include<iostream>#include<vector>#include<algorithm>// 假设已有 Less / Greater / priority_queue 定义intmain(){// 测试:使用 Greater<T> → 小堆 → 升序出队priority_queue<int,vector<int>,Greater<int>>pq1;pq1.push(5);pq1.push(3);pq1.push(7);pq1.push(1);std::cout<<"Greater<int> 堆(小堆): ";while(!pq1.empty()){std::cout<<pq1.top()<<" ";pq1.pop();}std::cout<<"\n";// 输出: 1 3 5 7// 测试:使用 Less<T> → 大堆 → 降序出队priority_queue<int,vector<int>,Less<int>>pq2;pq2.push(5);pq2.push(3);pq2.push(7);pq2.push(1);std::cout<<"Less<int> 堆(大堆): ";while(!pq2.empty()){std::cout<<pq2.top()<<" ";pq2.pop();}std::cout<<"\n";// 输出: 7 5 3 1return0;}

结论

  • 本实现展示了priority_queue的底层机制。
  • 重点在于:比较器决定了堆的性质。
  • Greater<T>在此处构建的是小堆,与标准库std::priority_queue的默认行为相反。
  • 设计精妙之处在于:用仿函数实现策略解耦,类型即配置,零开销,高内联性。
  • 理解com(a,b)的语义是掌握堆逻辑的关键:“a 是否应让位给 b?”

✅ 正确理解:com(a,b)为真 ⇒b更优 ⇒b应上浮 ⇒ 堆中“更优”的元素靠近根。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询