探索实现C++ STL容器适配器:优先队列priority_queue

探索实现C++ STL容器适配器:优先队列priority_queue
前引: 在算法竞赛中,选手们常常能在0.01秒内分出胜负;在实时交易系统中,毫秒级的延迟可能意味着数百万的盈亏;在高并发服务器中,每秒需要处理数万条不同优先级的请求——这些系统背后,都隐藏着同一种强大的数据结构:​优先队列(priority_queue)​作为C++标准库中最优雅的数据结构适配器,priority_queue完美封装了堆算法的高效性,却只需几行代码即可实现复杂优先级管理。本文将深入剖析:

(1)​堆原理与优先队列的机械美学​

(2)定制化优先级策略的高级技巧​

(3)实战性能对比与编译器级优化​

(4)在万亿级数据处理中的独特优势

目录

优先队列介绍

优先队列的渊源

实例化

插入元素

访问元素

获取元素个数

判断优先队列是否为空

删除堆顶元素

·

优先队列的模拟实现

类模板

插入元素

访问元素

获取元素个数

判断优先队列是否为空

删除堆顶元素

效果展示


优先队列介绍

优先队列priority_queue 是 C++ 标准模板库(STL)中的一个容器适配器,提供了堆数据结构的实现(默认为最大堆)。它在需要高效访问最大/最小元素的场景下非常有用!

如果需要使用小顶堆,可以这样传参 priority_queue< int , vector<int> , greater<int> > 

它是默认基于(大顶)堆实现的,例如一颗用数组存储的完全二叉树:

特点总结:

(1)采用数组形式存储

(2)默认基于最大堆实现

(3)适配器容器底层为 vector (使用需要包含#include<queue>) 

(4)每次只能访问队列顶部的元素,即优先级最高的元素

(5)复杂度:访问O(1)、插入O(log n)、删除顶部元素O(log n)

优先队列的渊源

我们通过 优先队列 的容器结构应该猜到,它的底层容器是 vector ,为什么不取名叫优先维克托呢

问:为什么底层容器是vector?

连续内存结构适合堆的随机访问需求,缓存友好,且动态数组支持高效尾部操作

问:为什么头文件是queue?

作为容器适配器,优先队列在概念上属于队列的一种,与queue共用同一头文件,体现了接口的一致性,比如队列的各种接口刚好吻合它的访问、插入、删除行为:

priority_queue --> "<queue>头文件" : 声明接口

问:为什么叫优先队列而不是优先维克托?

名称语义分解​:

优先(Priority):元素按内在重要性排序,而非插入顺序

队列(Queue):仅允许特定端点访问的操作模型(队尾插入,队首访问)

​行为本质​:

插入操作:push(),时间复杂度 O(log n)

访问操作:top(),总是获取优先级最高的元素

删除操作:pop(),移除当前最高优先级元素

实例化

采用:priority_queue<数据类型> 变量名;

我们可以选择默认初始化:

priority_queue<int> V1;

也可以选择范围初始化:

priority_queue<int> V2(arr,arr+n); //或者用另一个容器去初始化 priority_queue<int> V3(V1.begin(),V1.end());

效果展示: 

插入元素

V.push(val);

访问元素

遵循堆的性质,只能访问堆顶元素

V.top();

获取元素个数

V.size();

判断优先队列是否为空

V.empty();

为空返回 true ;不为空返回 false

删除堆顶元素

V.pop();

优先队列的模拟实现

类模板

template<class T, class contain = vector<T>> class priority_queue { public: //构造可以不写,因为可以直接使用vector //函数实现 private: contain x; }

既然底层是 vector,我们用缺省参数直接实例化出一个 vector 类型的变量就可以作为底层实现了

插入元素

插入元素调用vector的接口就行了,这里由于需要满足优先队列的性质(大顶堆),我们还需要在插入之后使用向上调整,保证堆顶(首元素)是最大的

插入元素:

//插入数据 void push(const T& date) { x.push_back(date); //向上调整 Upgrade(size() - 1); }

向上调整:

//向上调整 void Upgrade(int child) { //父节点 int parent = (child - 1) / 2; //如果孩子节点大于父节点,就向上调整交换(根节点可能也需要调整) while (parent >= 0) { //只要进入循环,那么节点下标一定是在合法范围 if (x[child] > x[parent]) { //交换 swap(x[child], x[parent]); //更新孩子、父节点 child = parent; parent = (child - 1) / 2; } else break; } }

这样经过向上调整就可以达到下面的效果:

访问元素

访问第一个元素即可(堆顶元素)

//访问元素 T top() { return x[0]; }

获取元素个数

//计算元素个数 T size() { return x.size(); }

判断优先队列是否为空

//判断是否为空 bool empty() { return x.empty(); }

删除堆顶元素

这里的删除调用 vector的尾删即可。

删除的方法:先交换堆顶 和 尾部元素,再删除,再使用向下调整保证大顶堆的性质

//删除堆顶元素 void pop() { Eliminate(size() - 1); }

向下调整: 

//向下调整 void Eliminate(int child) { //交换堆顶和末尾元素 swap(x[child], x[0]); //去尾 x.pop_back(); //父子节点 int parent = 0; child = 2 * parent + 1; //开始调整(子节点不能超过范围) while (child < x.size()) { //如果右节点大于左节点 if (x[child] < x[child + 1]) { child++; } //如果父节点小于子节点 if (x[parent] < x[child]) { swap(x[parent], x[child]); //更新 parent = child; child = 2 * parent + 1; } else break; } }

效果展示

void text1_t() { priority_queue<int> V1; //插入元素 V1.push(10); V1.push(15); V1.push(5); V1.push(20); V1.push(0); V1.push(25); //元素个数 cout << V1.size() << endl; //访问堆顶元素 cout << V1.top() << endl; //出堆顶元素 V1.pop(); //访问堆顶元素 cout << V1.top() << endl; //判断是否为空 cout << V1.empty() << endl; }

                                                   【雾非雾】期待与你的下次相遇! 

Read more

【c++与Linux进阶】线程篇 -互斥锁

【c++与Linux进阶】线程篇 -互斥锁

1. 前言: 在我们之前学习的代码种,就是在建造多线程的路上,我们可以看到出现了乱码或者抢占输出,这是为什么呢? 本章将带着这个问题来带你思考: 1. 一个例子先来领略问题的所在。 2. 什么是线程互斥. 3. 见识互斥锁。 4. 使用互斥锁 2. 一个买票的例子: 假设我们有100张电影票,我们同时抢票会出现什么,我们来尝试写代码来看看: #include<iostream>#include<thread>#include<vector>#include<string>#include<cstdio>#include<unistd.h>int ticket =100;voidroutine(std:

By Ne0inhk
【C++】深入拆解二叉搜索树:从递归与非递归双视角,彻底掌握STL容器的基石

【C++】深入拆解二叉搜索树:从递归与非递归双视角,彻底掌握STL容器的基石

【C++】深入拆解二叉搜索树:从递归与非递归双视角,彻底掌握STL容器的基石 * 摘要 * 目录 * 一、概念 * 二、 性能分析 * 三、key结构非递归模拟实现 * 1. 二叉搜索树的插入 * 2. 二叉搜索树的查找 * 3. 二叉搜索树的删除 * 4. 二叉搜索树的中序遍历 * 四、key结构递归的模拟实现 * 1. 递归与非递归二叉搜索树核心操作的对比 * 2. 递归插入 * 3. 递归查找 * 4. 递归删除 * 总结 摘要 二叉搜索树(BST)是一种重要的数据结构,它通过"左子树所有节点值小于根节点,右子树所有节点值大于根节点"的特性实现高效的元素组织。本文详细解析了BST的核心概念、性能特点,并分别通过非递归和递归两种方式完整实现了插入、查找、删除等关键操作,深入探讨了指针引用在递归实现中的巧妙应用,以及两种实现方式在时间复杂度、空间复杂度和适用场景上的差异。 目录

By Ne0inhk
【C++】第二十六节—C++11(中) | 右值引用和移动语义(续集)+lambda

【C++】第二十六节—C++11(中) | 右值引用和移动语义(续集)+lambda

Hi,我是云边有个稻草人,C++领域博主与你分享专业知识(*^▽^*) 《C++》本篇文章所属专栏—持续更新中—欢迎订阅~ 目录 上节总览,详情见—>【C++】第二十五节—C++11 (上) | 详解列表初始化+右值引用和移动语义 本节总览 (4)右值引用和移动语义在传参中的提效 6. 类型分类 7. 引用折叠 8. 完美转发 四、lambda 1. lambda表达式语法 2. lambda的应用 3. 捕捉列表 4. lambda的原理 接着上节,正文开始—— (4)右值引用和移动语义在传参中的提效 * 查看STL文档我们发现C++11以后容器的push和insert系列的接口否增加的右值引用版本 * 当实参是一个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象 * 当实参是一个右值,容器内部则调用移动构造,右值对象的资源到容器空间的对象上

By Ne0inhk
【C++深学日志】C++“类”的完全指南--从基础到实践(一)

【C++深学日志】C++“类”的完全指南--从基础到实践(一)

假想一下,你是一个顶级汽车设计师,你的任务不是亲自拧紧每一个螺丝,而是要设计出一幅“汽车蓝图”,你在图纸上设计了一辆汽车所需的一切:车轮、车灯、V8发动机、方向盘等,你手上这份设计好的蓝图就相当于我们今天要讲的C++中的“类”,它规定了汽车的属性(例如:离合器)和方法(功能:换挡),它本身并不是一辆真正的汽车,只是你的一份设计规划,后续你交付给工厂,工厂按照你的设计蓝图,生产出了一辆汽车,这就是实例化,后续工厂有根据你的蓝图设计了一条流水线,每一辆从流水线上生产下来的车辆,都是里这个蓝图(类)的一个对象,他们都有蓝图定义的属性和功能。在C++中类就充当着蓝图的作用,它定义了对象拥有哪些属性,那么就和我一起来揭开这份“蓝图”的面纱吧。 1.类 1.1.类的定义 类的基本思想是数据抽象和封装,数据抽象是一种依赖于接口和实现的分离式编程技术,类的接口包括用户所能执行的操作,类的实现则是包括类的数据成员、负责接口实现的函数以及定义类所需的各种私有函数。封装实现了类的接口和实现的分离,封装后的类隐藏了他的视线细节,也就是说,

By Ne0inhk