制作不易,欢迎各位点赞收藏,如有不足,欢迎在评论区补充
引入
STL作为c++重要特性之一,为各位的编程过程中带来不少便利,其中首当其冲要掌握的便是其中的优先队列(Priority Queue)。他与普通的队列相比,最重要的特点就是不是按照进入顺序进行所谓的“先进先出原则”,而是根据元素的优先级进行排列,而后根据元素的优先级进行弹出和插入操作,这里的优先级可以有很多定义,既可以是大小,也可以是根据类的定义不同,自行规定优先级的排列规则,如“奶牛的食量”,“Acmer的编程水平”等等,进行由高到低或者由低到高的排序。
Priority Queue的底层实现
在c++的STL中,优先队列(priority_queue)的底层默认是用堆来实现的,并通常是用vector容器来储存堆中的元素。而提到堆,就不得不提到堆的两大性质:完全二叉树性与堆序性。前者(完全二叉数性)只允许二叉树中的最后一行不为空,且最后一行节点尽量靠左(必须从左往右排序,最后一行元素间不可以有间隔)。后者(堆序性)是对于最大堆,堆序性要求每个父节点的值都大于或等于其子节点的值;对于最小堆,每个父节点的值都小于或等于其子节点的值。
堆序性和完全二叉树性是实现优先队列底层的关键
原因如下:
堆序性
- 快速确定优先级:堆序性规定了父节点和子节点的大小关系。这使得优先队列能在O(1)时间内通过堆顶元素确定最高或最低优先级元素,能高效地实现 top 操作,快速返回优先队列中优先级最高或最低的元素。
- 高效调整:插入和删除元素时,依据堆序性通过上浮或下沉操作,能以O(\log n)的时间复杂度重新平衡堆,保持堆的特性,确保优先队列操作的高效性。比如插入新元素时,将其与父节点比较并根据堆序性调整位置,直至满足堆序。
完全二叉树性
- 空间效率高:完全二叉树可以用数组紧凑存储,节点在数组中的位置有规律可循,能有效利用存储空间。对于具有n个节点的完全二叉树,数组下标从1开始存储时,节点i的左子节点是2i,右子节点是2i + 1,无需额外空间存储节点间的指针关系。
- 操作高效:完全二叉树的结构特点使堆的调整操作更高效。由于其结构规整,在进行上浮或下沉操作时,能快速定位节点及其子节点和父节点,减少比较和移动次数。而且这种结构保证了堆在插入和删除元素后,能通过简单的计算和有限次调整,迅速恢复为满足堆序性的完全二叉树,维持优先队列的性质。
C++中优先队列的具体用法
模版介绍与常用操作
在C++中,优先队列被C++标准模版库(STL)封装成了priority_queue类(注意要带“_”)。它的模板参数有三个,分别是 T 、 Container 和 Compare 。
priority_queue<T, Container, Comparator>
以下是具体介绍:
T :表示优先队列中存储元素的数据类型,可以是内置数据类型(如 int 、 double 等),也可以是自定义的数据类型(如结构体、类)。
Container :用于指定优先队列的底层容器,默认是 vector ,也可以使用 deque 等其他满足要求的序列容器。这个容器用于存储优先队列中的元素,并提供随机访问和在末尾插入/删除元素的功能。
Compare :是一个比较函数对象类型,用于定义元素之间的比较规则,以确定元素在优先队列中的优先级。默认情况下,对于数值类型,使用 less<T> ,即大的元素优先级高,形成最大堆;若要使用最小堆,可指定为 greater<T> 。对于自定义类型,需要用户自定义比较函数或函数对象。
同时,其在调用之前必须要在main函数前带上#include <queue>的头文件,同时与他搭配的,还有以下操作
操作 | 标识符 | 功能 | 时间复杂度 |
插入元素 | push | 将一个新元素及其优先级插入到优先队列中。 | O(log n) |
删除元素 | pop | 删除并返回优先队列中优先级最高(对于最大堆)或最低(对于最小堆)的元素。(注意不是返回被删除元素) | O(log n) |
访问队首元素 | top或front | 返回优先队列中优先级最高或最低的元素,但不删除该元素。 | O(1) |
判断优先队列是否为空 | empty | 检查优先队列中是否包含任何元素。 | O(1) |
获取优先队列中元素的个数 | size | 返回优先队列中元素的数量 | O(1) |
需要注意的一点就是,优先队列本身是不支持迭代器访问的,所以 无法通过迭代器直接遍历和删除指定元素,那我们该如何删除元素呢?通常方式有两个:
1. 删除队首元素:因为优先队列默认情况下只能访问和操作队首(优先级最高或最低)的元素,所以可以使用 pop 操作来删除队首元素。
样例代码如下:
#include <iostream>
#include <queue>
using namespace std;
signed main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
priority_queue<int> pq;
pq.push(5);
pq.push(7);
pq.push(3);
pq.push(1);
// 删除队首元素(优先级最高的元素)
//默认大根堆
pq.pop();//删除大根堆的堆顶 7
while(!pq.empty())
{
cout<<pq.top()<<endl;
pq.pop();
}
return 0;
}
输出如下:
5
3
1
2.重新构建优先队列:如果要删除特定的元素,一种间接的方法是将优先队列中的元素逐个取出,跳过要删除的元素,然后再将其余元素重新插入到一个新的优先队列中。这种方法的时间复杂度较高,因为需要遍历原优先队列中的所有元素并进行插入操作。
样例代码如下:
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
signed main() {
priority_queue<int> pq;
pq.push(5);
pq.push(3);
pq.push(8);
int elementToDelete = 3;
vector<int> temp;
while (!pq.empty()) {
//如果队头不是被删元素,将其
if (pq.top() != elementToDelete) {
temp.push_back(pq.top());
}
pq.pop();
}
// 重新构建优先队列
for (int num : temp) {
pq.push(num);
}
while (!pq.empty()) {
cout << pq.top() << endl;
pq.pop();
}
return 0;
}
输出如下:
8
5
自定义类型优先级比较写法与演示
上文提到,在C++中, priority_queue 默认使用 less 比较函数(对于数值类型,大的元素优先级高,形成最大堆)。然而当需要自定义类型(如结构体或类)的优先级比较规则时,可以通过两种常见的方式来实现,以下是详细的写法与演示:
1.使用函数对象(仿函数)
我们可以自行构造仿函数,在定义priority_queue类时直接写到模版当中,从而从底层改变优先级的排列方式。代码与注释如下:
#include <iostream>
#include <queue>
#include <string>
using namespace std;
// 定义自定义类型结构体
struct Student {
string name;
int score;
Student(const string& n, int s) : name(n), score(s) {}
};
// 自定义比较函数对象(仿函数),分数高的学生优先级高
struct CompareStudent {
bool operator()(const Student& s1, const Student& s2) {
return s1.score < s2.score;
}
};
int main() {
// 创建优先队列,元素类型为Student,底层容器为vector,比较函数为CompareStudent
priority_queue<Student, vector<Student>, CompareStudent> studentQueue;
// 插入学生信息
studentQueue.push(Student("Alice", 85));
studentQueue.push(Student("Bob", 90));
studentQueue.push(Student("Charlie", 80));
// 依次取出并输出学生信息,分数高的先输出
while (!studentQueue.empty()) {
Student currentStudent = studentQueue.top();
studentQueue.pop();
cout << "学生姓名: " << currentStudent.name << ", 分数: " << currentStudent.score << endl;
}
return 0;
}
输出如下:
学生姓名: Bob, 分数: 90
学生姓名: Alice, 分数: 85
学生姓名: Charlie, 分数: 80
2.重载 operator<
利用结构体或类的重载,定义比较方式,使得编译器会判断自定义类型的优先级大小。
#include <iostream>
#include <queue>
#include <string>
using namespace std;
// 定义自定义类型结构体
struct Employee {
string name;
int age;
Employee(const string& n, int a) : name(n), age(a) {}
// 重载operator<,年龄大的员工优先级高
bool operator<(const Employee&e1)const
{
return age<e1.age;
}
};
int main() {
// 创建优先队列,元素类型为Employee,使用默认的vector作为底层容器和默认的比较方式(因为Employee重载了operator<)
priority_queue<Employee> employeeQueue;
// 插入员工信息
employeeQueue.push(Employee("Tom", 30));
employeeQueue.push(Employee("Jerry", 35));
employeeQueue.push(Employee("Lucy", 28));
// 依次取出并输出员工信息,年龄大的先输出
while (!employeeQueue.empty()) {
Employee currentEmployee = employeeQueue.top();
employeeQueue.pop();
cout << "员工姓名: " << currentEmployee.name << ", 年龄: " << currentEmployee.age << endl;
}
return 0;
}
输出:
员工姓名: Jerry, 年龄: 35
员工姓名: Tom, 年龄: 30
员工姓名: Lucy, 年龄: 28
以下内容当做了解
如果我们想从小到大排序非自定义类型,在 C++ 中, priority_queue<int, vector<int>, greater<int> > q; 最后的 int> 与 > 之间必须空一个格。这是因为 >> 在 C++ 中是右移运算符,如果不空格,编译器会将其解析为右移运算符,导致编译错误。所以为了让编译器正确识别为模板参数的结束,需要在 int> 和 > 之间添加一个空格。
然而,在C++11标准以后,引入了尖括号匹配规则的改进,编译器能够更好地识别模板参数列表的结束,所以即使 >> 之间没有空格,也能正确解析为两个连续的右尖括号,而不是右移运算符。这使得代码在书写上更加灵活,也提高了代码的可读性和可维护性。
结语
相信如果你看到这了,一定对优先队列有了更深度的了解,然而他的美妙之处更在于与算法的结合(堆排序,Dijkstra算法,Huffman编码),这些就要慢慢探索了。