深入解析 STL 优先级队列:从原理到实战

深入解析 STL 优先级队列:从原理到实战

🔥小叶-duck个人主页

❄️个人专栏《Data-Structure-Learning》

《C++入门到进阶&自我学习过程记录》

未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

一、priority_queue 核心逻辑

  1、本质与优先级规则

  2、底层容器的适配要求

  3、常用接口与场景示例

二、priority_queue 底层实现解析

  1、默认 “大堆” 规则排序模拟实现

  2、仿函数

    2.1 仿函数的结构

    2.2 仿函数测试代码

    2.3 利用仿函数进行 priority_queue 底层实现

    2.4 测试代码演示

三、OJ实战练习:数组中第k个最大的元素

结束语


在 STL 中,priority_queue(优先队列)是处理优先级任务的实用工具 —— 以堆结构为核心,封装复杂排序逻辑,能高效获取优先级最高的元素(默认是大的值优先级高),广泛用于 Top K、任务调度等场景。本文将拆解其特性、接口与原理,帮你从 “会用” 到 “懂底层原理”。

一、priority_queue 核心逻辑

  1、本质与优先级规则

  • 容器适配器属性:不直接存储数据,而是封装底层容器(如 vector、deque),并通过堆算法(make_heap、push_heap、pop_heap)来维护堆结构。
  • 优先级规则:默认是大堆(堆顶为最大元素),可通过仿函数指定比较规则改为小堆;优先级由比较函数决定,支持内置类型和自定义类型。
参考文档:priority_queue - C++ Reference

  2、底层容器的适配要求

      底层容器需支持 “随机访问迭代器” 和以下操作:

  • empty():检测是否为空
  • size():返回元素个数
  • front():访问堆顶元素(即容器首元素)
  • push_back():在容器尾部插入元素(用于后续堆调整算法)
  • pop_back():删除容器尾部元素(配合堆调整算法移除堆顶数据)

      STL 中默认使用 vector(空间连续,堆算法效率更高),也可指定 deque

  3、常用接口与场景示例

接口声明功能说明
priority_queue()构造一个空的优先队列,初始化后无任何元素,需通过 push(x) 插入数据。
priority_queue(first, last)利用迭代器区间 [ first, last ] 中的所有元素初始化优先队列,初始化后会自动调整为堆结构
empty()检测优先队列是否为空,若队列中无元素则返回 true,存在元素则返回 false,常用于判断是否可执行 top() 或 pop() 操作。
size()返回优先队列中有效元素的个数,可用于了解队列数据量,或配合循环控制 pop() 操作次数(如 Top K 问题中弹出前 K - 1 个元素)。
top()返回堆顶元素的引用,大堆场景下返回队列中的最大值,小堆场景下返回最小值;需注意,调用前需用 empty() 确认队列非空,否则会触发未定义行为。
push(x)将元素 x 插入优先队列的尾部,插入后会自动调用堆算法 (push_heap) 调整堆结构,确保队列仍满足大堆或小堆的排序规则
pop()删除优先队列的堆顶元素,删除前会先通过堆算法(pop_heap)将堆顶元素交换到队列尾部,再执行删除;操作后需确保队列非空,且删除后仍保持堆结构

接口测试代码:

//Test.cpp #include<iostream> #include<queue> using namespace std; int main() { //priority_queue接口使用 priority_queue<int> pq1;//默认底层基于vector容器,且为大堆 pq1.push(4); pq1.push(7); pq1.push(2); pq1.push(5); pq1.push(10); pq1.push(8); while (!pq1.empty()) { cout << pq1.top() << " "; pq1.pop(); } cout << endl; priority_queue<int, vector<int>, greater<int>> pq2;//调整成默认是小的优先级高(小堆) pq2.push(4); pq2.push(7); pq2.push(2); pq2.push(5); pq2.push(10); pq2.push(8); while (!pq2.empty()) { cout << pq2.top() << " "; pq2.pop(); } cout << endl; return 0; }

二、priority_queue 底层实现解析

      由于优先级队列的底层结构是堆,所以就会用到在之前所学数据结构堆中的相关功能的实现(如向上调整算法、向下调整算法等),如果不清楚的或者想要回顾之前的知识可以看下面我的往期文章:

数据结构之二叉树-堆

  1、默认 “大堆” 规则排序模拟实现

//Priority_Queue.h #include<iostream> #include<vector> using namespace std; namespace MyPriorityQueue { template<class T, class Container = vector<T>> class priority_queue { public: //向上调整算法(已知孩子位置推算父亲位置) void AdjustUp(size_t child)//访问下标 { size_t father = (child - 1) / 2; while (child > 0) //当child等于0说明已经调整到头节点 { //默认调整为大堆 if (_con[child] > _con[father]) { swap(_con[child], _con[father]); child = father; father = (child - 1) / 2; } else { break;//如果_con[child] <= _con[father]则说明调整完成 } } } //向下调整算法(已知父亲位置推算孩子位置) void AdjustDown(size_t father)//访问下标 { //假设父亲的左孩子为较大数 size_t child = father * 2 + 1; //默认调整为大堆 while (child < _con.size()) { //如果父亲存在右孩子且右孩子比左孩子大则进行修改 if ((child + 1) < _con.size() && _con[child] < _con[child + 1]) { child += 1; } if (_con[father] < _con[child]) { swap(_con[father], _con[child]); father = child; child = father * 2 + 1; } else { break;//如果父亲比较大孩子的值还大则说明已经调整完成了 } } } //插入数据(入优先级队列) void push(const T& x) { //首先尾插数据 _con.push_back(x); //再将该数据通过向上调整算法使得数组保持堆的结构 AdjustUp(_con.size() - 1); } //获取数据(队头数据) const T& top() { return _con[0]; } //删除数据(出优先级队列) void pop() { //首先首尾数据进行交换,将堆顶数据进行尾删 swap(_con[0], _con[_con.size() - 1]); _con.pop_back(); //再将首位置数据通过向下调整算法使得数组保持堆的结构 AdjustDown(0); } //获取队列数据个数 size_t size() { return _con.size(); } //判空 bool empty() { return _con.size() == 0; } private: Container _con; }; }

测试代码:

//Test.cpp #include"Priority_Queue.h" //测试priority_queue模拟实现 void test_priority_queue() { MyPriorityQueue::priority_queue<int> pq; pq.push(4); pq.push(7); pq.push(2); pq.push(5); pq.push(10); pq.push(8); while (!pq.empty()) { cout << pq.top() << " "; pq.pop(); } cout << endl; } int main() { test_priority_queue(); return 0; }

  2、仿函数

      

      我们会发现标准库里面实现的优先级队列有第三个模板参数,而且上面我们讲了当优先级队列不传第三个参数默认是建立“大堆”,那我们如果想要优先级队列建立成“小堆”怎么实现呢?
      就需要用到接下来讲解的——仿函数

      仿函数顾名思义就是像一个函数,所以其实仿函数并不是一个函数而是一个类,其是通过重载 operator() 让一个类实例化以后所生成的对象能够像函数一样使用

    2.1 仿函数的结构

//仿函数 //通过重载 operator() 让一个类实例化以后所生成的对象能够像函数一样使用 //比较器:用于大堆(父节点优先级高于子节点时,返回true触发交换) template<class T> struct Less { bool operator()(const T& x, const T& y) { return x < y; } };

    2.2 仿函数测试代码

//Test.cpp //仿函数的测试 void test_Functor() { //函数对象 Less<int> LessFunc; cout << LessFunc(1, 2) << endl; //我们会发现 LessFunc(1, 2) 在结构上是非常类似函数的使用 //但实际上就是一个对象调用了重载 operator() 的结果 //也可以写出下面更加清晰的结构: cout << LessFunc.operator()(1, 2) << endl; } int main() { test_Functor(); return 0; }

    2.3 利用仿函数进行 priority_queue 底层实现

      上面我们只是简单介绍一下仿函数,仿函数的使用远没有这么简单,只是这里我们讲解仿函数是为了实现模板参数比较器 Compare,让 priority_queue 不仅能实现大堆也能实现小堆:

//Priority_Queue.h //仿函数 //通过重载 operator() 让一个类实例化以后所生成的对象能够像函数一样使用 //比较器:用于大堆(父节点优先级高于子节点时,返回true触发交换) template<class T> struct Less { bool operator()(const T& x, const T& y) { return x < y; } }; //比较器:用于小堆(子节点优先级高于父节点时,返回true触发交换) template<class T> struct Greater { bool operator()(const T& x, const T& y) { return x > y; } }; namespace MyPriorityQueue { template<class T, class Container = vector<T>, class Compare = Less<T>> // T:存储的元素类型;Container:底层容器(默认vector,需支持随机访问和尾部操作) // Compare:比较器(默认Less,即大堆) class priority_queue { public: //向上调整算法(已知孩子位置推算父亲位置) void AdjustUp(size_t child)//访问下标 { Compare com;// 实例化比较器,用于判断优先级 size_t father = (child - 1) / 2; while (child > 0) //当child等于0说明已经调整到头节点 { //if (_con[child] > _con[father]) if(com(_con[father], _con[child])) //需要注意Less是建立大堆,但是比较大小是 < ,父亲和孩子的顺序不要错 { swap(_con[child], _con[father]); child = father; father = (child - 1) / 2; } else { break; } } } //向下调整算法(已知父亲位置推算孩子位置) void AdjustDown(size_t father)//访问下标 { Compare com;// 实例化比较器,用于判断优先级 size_t child = father * 2 + 1; while (child < _con.size()) { //if ((child + 1) < _con.size() && _con[child] < _con[child + 1]) if ((child + 1) < _con.size() && com(_con[child], _con[child + 1])) { child += 1; } //if (_con[father] < _con[child]) if (com(_con[father], _con[child])) { swap(_con[father], _con[child]); father = child; child = father * 2 + 1; } else { break; } } } //插入数据(入优先级队列) void push(const T& x) { //首先尾插数据 _con.push_back(x); //再将该数据通过向上调整算法使得数组保持堆的结构 AdjustUp(_con.size() - 1); } //获取数据(队头数据) const T& top() { return _con[0]; } //删除数据(出优先级队列) void pop() { //首先首尾数据进行交换,将堆顶数据进行尾删 swap(_con[0], _con[_con.size() - 1]); _con.pop_back(); //再将首位置数据通过向下调整算法使得数组保持堆的结构 AdjustDown(0); } //获取队列数据个数 size_t size() { return _con.size(); } //判空 bool empty() { return _con.size() == 0; } private: Container _con; }; }

    2.4 测试代码演示

//Test.cpp //测试priority_queue模拟实现 void test_priority_queue() { MyPriorityQueue::priority_queue<int> pq1; pq1.push(4); pq1.push(7); pq1.push(2); pq1.push(5); pq1.push(10); pq1.push(8); while (!pq1.empty()) { cout << pq1.top() << " "; pq1.pop(); } cout << endl; MyPriorityQueue::priority_queue<int, vector<int>, Greater<int>> pq2; pq2.push(4); pq2.push(7); pq2.push(2); pq2.push(5); pq2.push(10); pq2.push(8); while (!pq2.empty()) { cout << pq2.top() << " "; pq2.pop(); } cout << endl; } int main() { test_priority_queue(); return 0; }

三、OJ实战练习:数组中第k个最大的元素

题目链接:

215. 数组中的第K个最大元素 - 力扣(LeetCode)

题目描述:

C++算法代码:

class Solution { public: int findKthLargest(vector<int>& nums, int k) { //默认构造 + push // priority_queue<int> pq; // for(auto e : nums) // { // pq.push(e); // } //迭代器区间构造(更简洁) priority_queue<int> pq(nums.begin(), nums.end()); while(--k) { pq.pop(); } return pq.top(); } };
结束语

      到此,优先级队列也就讲解完了。作为 STL 中基于堆结构实现的容器适配器,priority_queue 用简洁的接口封装了复杂的堆调整逻辑,无论是默认的大堆排序,还是自定义类型的优先级适配,都能轻松应对 “按优先级取元素” 的需求。优先级队列功能的模拟实现本身是没有难度的,因为底层结构是基于之前所学的数据结构堆,本篇文章主要是对仿函数这个以前没有接触过在使用上像函数的类进行讲解。希望这篇文章对大家学习C++能有所帮助!

C++参考文档:
https://legacy.cplusplus.com/reference/
https://zh.cppreference.com/w/cpp
https://en.cppreference.com/w/

Read more

【金仓数据库】ksql 指南(七) —— 启动和管理事务(KingbaseES 数据一致性保障)

【金仓数据库】ksql 指南(七) —— 启动和管理事务(KingbaseES 数据一致性保障)

引言 在实际业务当中,不少操作要“多步同步完成或者同步失败”,就像转账时,从A账户扣款并给B账户入账,如果仅仅扣了款却没有入账,就会引发数据错误,所以要用“事务”来保证数据的一致性,本文就“ksql命令行运作事务”展开论述,按照“事务概念——基本操作——隔离级别——问题查找”的顺序层层分解,利用通俗易懂的语言和实际业务案例(比如转账)加以阐述,使得初学者也能够领会事务的关键意义及其操作手段。 文章目录 * 引言 * 一、前置准备:确认操作环境(衔接前文,确保示例可落地) * 1.1 1. 用有权限的用户连接数据库 * 1.2 2. 确认测试表与数据(用转账场景的表为例) * 二、核心概念:事务与 ACID 特性(新手必懂) * 三、事务控制命令:3 个核心命令玩转事务

By Ne0inhk
【AI】——结合Ollama、Open WebUI和Docker本地部署可视化AI大语言模型

【AI】——结合Ollama、Open WebUI和Docker本地部署可视化AI大语言模型

🎼个人主页:【Y小夜】 😎作者简介:一位双非学校的大三学生,编程爱好者, 专注于基础和实战分享,欢迎私信咨询! 🎆入门专栏:🎇【MySQL,Javaweb,Rust,python】 🎈热门专栏:🎊【Springboot,Redis,Springsecurity,Docker,AI】  感谢您的点赞、关注、评论、收藏、是对我最大的认可和支持!❤️ 目录 🎈本地部署模型 🎉安装Ollama 🎉安装 Open WebUI 🎊安装Docker 🥞启动 Hyper-v 🥞 安装 WSL(适用于Linux的Windows的子系统): 🥞安装Docker  🎊Docker 部署 Open WebUI 🎈本地部署模型 🎉安装Ollama 官网: Ollama 然后进行一下下载 安装完成之后是没有提示的,然后我们需要去测试一下。(这里我是以QWen为例子,大家可以尝试其他的模型) 打开一个终端,

By Ne0inhk

仓库管理系统-17-前端之物品类型管理

文章目录 * 1 表设计(goodstype) * 2 后端代码 * 2.1 Goodstype.java * 2.2 GoodstypeMapper.java * 2.3 GoodstypeService.java * 2.4 GoodstypeServiceImpl.java * 2.5 GoodstypeController.java * 3 前端代码 * 3.1 goodstype/GoodstypeManage.vue * 3.2 添加菜单 * 3.3 页面显示 1、goodstype表设计。2、根据表生成后端代码。3、编写后端增删改查代码。4、ApiPost测试查询代码。5、编写前端相关代码。 1

By Ne0inhk

前端常用字符串/数组操作(含相关手撕)

字符串转数组的方法 1. split() - 最常用的方法 功能描述:使用指定的分隔符将字符串分割成字符串数组 语法: str.split([separator[, limit]]) 参数: * separator:指定表示每个拆分点的字符串,如果省略,则返回包含整个字符串的数组 * limit:可选,限制返回的数组片段数量 示例: const str = "apple,banana,orange"; const arr = str.split(","); // 结果: ["apple", "banana", "orange"] const str2 = "hello"; const

By Ne0inhk