优先队列
优先队列
优先队列的基本概念
在普通的队列中,元素会按照入队的顺序排列在一起,先入队的元素排在队头,后入队的元素排在队尾。这就像是现实中我们排队上火车,先去排队的人排在队伍的前面,优先排到队头,从而登上火车、离开队伍。
优先队列就像是一个每个人都有一定优先级的队列,比如说同样是排队上火车,但是这次将要来排队的人中,既有一般的普通乘客,又有买商务座的高级乘客,又有军人和消防队员,还有老、弱、病、残、孕等等各类乘客。根据规定,军人、消防队员依法优先,购买商务座的高级乘客依规优先,老、弱、病、残、孕按理来说应该优先,这个时候,这个队伍还能按照入队顺序排列吗?
显然,此时的队伍的排列方式发生了根本性的改变,由原本的根据入队顺序排列,变为了根据内部各个元素的优先级排序,此时,先出队的不一定是先入队的,而一定是目前优先队列里面优先级最高的。比如说,有一队老弱病残正在排队,此时来了一个商务舱高级乘客,他不会默默地排在队尾,而是会因为他的优先级更高,直接移动到队头。
一般情况下,我们默认具有最大优先级的元素会排在队头,而具有最小优先级的元素会排在队尾,这种优先队列一般被称为大根堆。如果反过来,具有最小优先级的元素会排在队头,具有最大优先级的元素会排在队尾,这种优先队列则被称为小根堆。
优先队列一般采用堆方法实现,堆的根节点对应了优先队列的队头元素。因为这种队列堆的根节点(即队头位置)具有最大的优先级,所以叫做大根堆。小根堆的命名原因同理。
优先队列的基本操作
优先队列支持以下基本操作:
- 元素入队:将某个特定元素添加到优先队列中,并按照优先级放在对应的位置
- 队头出队:移除优先队列中目前的队头元素(大根堆中的最大优先级元素,小根堆中的最小优先级元素)
- 查看队头:查看目前优先队列中目前的队头元素(大根堆中的最大优先级元素,小根堆中的最小优先级元素)
- 查看队列长度:获取目前优先队列中元素的数量
默认数据类型的优先队列
C++
在 C++ 中,我们通常使用 STL 标准库中的 priority_queue
作为优先队列,此外,由于 multiset
内部有序,也可以被用于实现优先队列。需要注意的是,这里不能使用普通的 set
来实现优先队列,因为 set
会自动删除掉重复的元素,所以当两个元素优先级相同时,会自动删除掉重复的元素(想象一堆老弱病残来排队,结果队里只留下了一个人,这也太恐怖了!)。
一般情况下,priority_queue
会默认将元素的“大小”作为优先级,或者说根据对应变量类型判定相对大小的方法来判断哪个元素的优先级更高。例如,114 的优先级低于 514,“aba” 的优先级低于 “bab”(字符串的大小是判断字典序),以此类推。
如果用 T
代表你想放进优先队列里面的元素的变量类型,用 pq
代表我们要创建的优先队列的变量名,则优先队列(大根堆)的声明语法为:
priority_queue<T> pq;
优先队列(小根堆)的声明语法为:
priority_queue<T, vector<T>, greater<T> > pq;
需要注意的是,greater<T>
和后面的 >
之间需要有一个空格,否则在部分编译器环境下这两个大于号(尖括号)会被视为一个符号 >>
,进而导致编译错误。
对于优先队列的四个操作,他们分别的语法如下表所示:
元素入队 | 优先队列的变量名.push(需要入队的元素) | pq.push(3) | |
队头出队 | 优先队列的变量名.pop() | pq.pop() | |
查看队头 | 优先队列的变量名.top() | pq.top() | |
查看队列长度 | 优先队列的变量名.size() | pq.size() |
下面举一个例子方便你更好地理解优先队列的用法:
#include <bits/stdc++.h>
using namespace std;
int main() {
// 创建一个储存int类型元素的优先队列(大根堆)
priority_queue<int> maxHeap;
// 添加元素
maxHeap.push(10);
maxHeap.push(30);
maxHeap.push(20);
maxHeap.push(5);
// 查看队列长度
cout << "队列长度: " << maxHeap.size() << endl; // 输出: 队列长度: 4
// 查看队头
cout << "查看队头: " << maxHeap.top() << endl; // 输出: 查看队头: 30
// 队头出队
maxHeap.pop();
cout << "队头出队后查看队头: " << maxHeap.top() << endl; // 输出: 队头出队后查看队头: 20
// 创建一个储存int类型元素的小根堆
priority_queue<int, vector<int>, greater<int>> minHeap;
// 添加元素
minHeap.push(10);
minHeap.push(30);
minHeap.push(20);
minHeap.push(5);
// 查看队列长度
cout << "最小堆的最高优先级元素: " << minHeap.top() << endl; // 输出: 最小堆的最高优先级元素: 5
// 遍历优先队列(注意:这会清空队列)
cout << "最小堆元素(按优先级顺序): ";
while (minHeap.size()) {
cout << minHeap.top() << " ";
minHeap.pop();
}
cout << endl; // 输出: 最小堆元素(按优先级顺序): 5 10 20 30
return 0;
}
Java
在 Java 中,我们通常使用 java.util
包中的 PriorityQueue
类来实现优先队列。默认情况下,PriorityQueue
实现的是一个小根堆,也就是说,优先级最低的元素(通常是数值最小或字典序最小的元素)会排在队头。
如果用 T
代表你想放进优先队列里面的元素的变量类型,用 pq
代表我们要创建的优先队列的变量名,则优先队列(小根堆)的声明语法为:
PriorityQueue<T> pq = new PriorityQueue<>();
如果你想实现一个大根堆(优先级最高的元素排在队头),可以通过传入一个自定义的 Comparator
来实现,或者利用 Collections.reverseOrder()
:
PriorityQueue<T> pq = new PriorityQueue<>(Collections.reverseOrder());
对于优先队列的四个基本操作,它们在 Java 中的对应方法如下表所示:
元素入队 | 优先队列的变量名.add(需要入队的元素) | pq.add(3) |
|
队头出队 | 优先队列的变量名.poll() | pq.poll() |
|
查看队头 | 优先队列的变量名.peek() | pq.peek() |
|
查看队列长度 | 优先队列的变量名.size() | pq.size() |
下面是一个例子,展示了 Java 中 PriorityQueue
的基本用法:
import java.util.Collections;
import java.util.PriorityQueue;
public class PriorityQueueExample {
public static void main(String[] args) {
// 创建一个储存Integer类型元素的优先队列(小根堆)
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
// 添加元素
minHeap.add(10);
minHeap.add(30);
minHeap.add(20);
minHeap.add(5);
// 查看队列长度
System.out.println("小根堆队列长度: " + minHeap.size()); // 输出: 小根堆队列长度: 4
// 查看队头 (最小值)
System.out.println("小根堆查看队头: " + minHeap.peek()); // 输出: 小根堆查看队头: 5
// 队头出队 (移除最小值)
minHeap.poll();
System.out.println("小根堆队头出队后查看队头: " + minHeap.peek()); // 输出: 小根堆队头出队后查看队头: 10
// 创建一个储存Integer类型元素的大根堆
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
// 添加元素
maxHeap.add(10);
maxHeap.add(30);
maxHeap.add(20);
maxHeap.add(5);
// 查看队头 (最大值)
System.out.println("大根堆的最高优先级元素: " + maxHeap.peek()); // 输出: 大根堆的最高优先级元素: 30
// 遍历优先队列(注意:这会清空队列)
System.out.print("大根堆元素(按优先级顺序): ");
while (!maxHeap.isEmpty()) {
System.out.print(maxHeap.poll() + " ");
}
System.out.println(); // 输出: 大根堆元素(按优先级顺序): 30 20 10 5
}
}
Python
在 Python 中,我们通常使用 heapq
模块来实现优先队列。heapq
模块提供了一系列函数,可以直接在一个普通的列表(list)上实现堆的操作。默认情况下,heapq
实现的是一个小根堆。
由于 heapq
是直接操作列表,所以我们首先需要创建一个空列表:
pq = []
heapq
模块没有直接提供大根堆的实现。一种常见的技巧是,在将元素存入堆时存入其相反数,这样原来的最大值就变成了最小值,从而可以利用小根堆来模拟大根堆。取出元素时再取其相反数即可恢复原值。
对于优先队列的四个基本操作,它们在 Python 中的对应函数如下表所示(以小根堆为例):
元素入队 | heapq.heappush(列表名, 需要入队的元素) |
heapq.heappush(pq, 3) |
|
队头出队 | heapq.heappop(列表名) |
heapq.heappop(pq) |
|
查看队头 | 列表名[0] (需要确保列表不为空) |
pq[0] |
|
查看队列长度 | len(列表名) |
len(pq) |
注意: 直接访问 列表名[0]
来查看队头元素的前提是列表(堆)不能为空,否则会引发 IndexError
。
下面是一个例子,展示了 Python 中 heapq
的基本用法:
import heapq
# 创建一个空列表作为小根堆
min_heap = []
# 添加元素
heapq.heappush(min_heap, 10)
heapq.heappush(min_heap, 30)
heapq.heappush(min_heap, 20)
heapq.heappush(min_heap, 5)
# 查看队列长度
print(f"小根堆队列长度: {len(min_heap)}") # 输出: 小根堆队列长度: 4
# 查看队头 (最小值)
if min_heap: # 检查是否为空
print(f"小根堆查看队头: {min_heap[0]}") # 输出: 小根堆查看队头: 5
# 队头出队 (移除最小值)
smallest = heapq.heappop(min_heap)
if min_heap:
print(f"小根堆队头出队后查看队头: {min_heap[0]}") # 输出: 小根堆队头出队后查看队头: 10
# 模拟大根堆:存入相反数
max_heap = []
heapq.heappush(max_heap, -10)
heapq.heappush(max_heap, -30)
heapq.heappush(max_heap, -20)
heapq.heappush(max_heap, -5)
# 查看队头 (最大值,需要取反)
if max_heap:
print(f"大根堆的最高优先级元素: {-max_heap[0]}") # 输出: 大根堆的最高优先级元素: 30
# 遍历优先队列(注意:这会清空队列)
print("大根堆元素(按优先级顺序): ", end="")
while max_heap:
print(-heapq.heappop(max_heap), end=" ")
print() # 输出: 大根堆元素(按优先级顺序): 30 20 10 5
例题 1:模板:整数优先队列
题意
给定一个数列,初始为空,请支持下面三种操作:
- 给定一个整数
,请将
加入到数列中。
- 输出数列中最小的数。
- 删除数列中最小的数(如果有多个数最小,只删除
个)。
思路分析
我们发现需要维护一个小根堆优先队列:
- 对于第一种操作,我们直接读入
并将其加入优先队列中即可
- 对于第二种操作,我们直接输出优先队列的队头元素即可
- 对于第三种操作,我们直接将优先队列的队头元素出队即可
代码实现
#include<bits/stdc++.h>
using namespace std;
priority_queue<int,vector<int>,greater<int> > pq; // 创建一个小根堆优先队列
int main(){
int n;
cin>>n; // 读入操作次数
for(int i=0;i<n;i++){
int op;
cin>>op; // 读入操作类型
if(op==1){
int x;
cin>>x; // 读入要插入的元素
pq.push(x); // 将元素x加入优先队列
}
if(op==2){
if(!pq.empty()){ // 如果优先队列不为空
cout<<pq.top()<<endl; // 输出当前队头元素(最小值)
}
}
if(op==3){
pq.pop(); // 删除队头元素(最小值)
}
}
return 0;
}
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 创建一个小根堆优先队列
PriorityQueue<Integer> pq = new PriorityQueue<>();
int n = scanner.nextInt(); // 读入操作次数
for (int i = 0; i < n; i++) {
int op = scanner.nextInt(); // 读入操作类型
if (op == 1) {
int x = scanner.nextInt(); // 读入要插入的元素
pq.add(x); // 将元素x加入优先队列
}
if (op == 2) {
if (!pq.isEmpty()) { // 如果优先队列不为空
System.out.println(pq.peek()); // 输出当前队头元素(最小值)
}
}
if (op == 3) {
pq.poll(); // 删除队头元素(最小值)
}
}
scanner.close();
}
}
import heapq
# 创建一个空列表作为小根堆
pq = []
n = int(input()) # 读入操作次数
for i in range(n):
op = list(map(int, input().split())) # 读入操作
if op[0] == 1:
x = op[1] # 读入要插入的元素
heapq.heappush(pq, x) # 将元素x加入优先队列
if op[0] == 2:
if pq: # 如果优先队列不为空
print(pq[0]) # 输出当前队头元素(最小值)
if op[0] == 3:
if pq: # 如果优先队列不为空
heapq.heappop(pq) # 删除队头元素(最小值)
例题 2:结构体优先队列
在前面的例子中,我们之所以能让优先队列自动根据元素的大小来判断优先级,是因为优先队列默认会根据元素的大小来判断优先级。但是,在实际的算法题中,我们往往需要自定义元素的优先级。例如,我们要实现一个储存学生各项信息的优先队列,我们希望学生的优先级是根据他们的分数来判断的,具体而言,有:
- 若两个学生总分不同,则总分高的优先级高;
- 若两个学生总分相同,但语文成绩不同,则语文成绩高的优先级高;
- 若两个学生总分相同,且语文成绩也相同,则数学成绩高的优先级高;
- 若两个学生总分相同,且语文成绩也相同,且数学成绩也相同,则英语成绩高的优先级高;
- 若两个学生总分相同,且语文成绩也相同,且数学成绩也相同,且英语成绩也相同,则名字字典序小的优先级高。
此时,我们应该如何让优先队列维护学生类型的优先级呢?
C++
在 C++ 中,我们可以使用结构体来封装一个学生的信息。为了让优先队列能根据我们自定义的方法来确定各个结构体变量的优先级高低,我们需要重载结构体的小于号运算符 <
为我们希望的比较两个结构体变量优先级的方法。
下面是一个例子,展示了如何使用结构体和自定义比较器来实现优先队列:
#include <bits/stdc++.h>
using namespace std;
// 定义一个学生结构体,包含姓名和三门课成绩
struct student{
string name; // 学生姓名
int ch,ma,en; // ch:语文成绩,ma:数学成绩,en:英语成绩
// 重载小于号运算符,用于优先队列内部比较优先级
bool operator<(const student &a) const{
// 1. 总分不同,总分高的优先级高
if(ch+ma+en != a.ch+a.ma+a.en) return ch+ma+en < a.ch+a.ma+a.en;
// 2. 总分相同,语文高的优先级高
if(ch!=a.ch) return ch<a.ch;
// 3. 总分和语文相同,数学高的优先级高
if(ma!=a.ma) return ma<a.ma;
// 4. 总分、语文、数学都相同,英语高的优先级高
if(en!=a.en) return en<a.en;
// 5. 所有成绩都相同,名字字典序小的优先级高
return name>a.name;
}
student() = default; // 默认构造函数
student(string A,int B,int C,int D){
name=A,ch=B,ma=C,en=D; // 构造函数初始化成员变量
}
};
// 声明一个存储student结构体的优先队列(大根堆,优先级高的在队头)
priority_queue<student> pq;
int main(){
// 通过构造函数向优先队列中加入学生信息
pq.push(student("silence76",11,45,14));
pq.push(student("wangzai76",14,45,11));
pq.push(student("qingchu76",150,150,150));
// 取出优先级最高的学生(队头)
student best_student = pq.top();
cout<<best_student.name<<endl; // 输出优先级最高学生的姓名
pq.pop(); // 移除队头元素
cout<<pq.size()<<endl; // 输出当前队列中元素数量
cout<<pq.top().name<<endl; // 输出当前队头学生的姓名
return 0;
}
Java
在 Java 中,我们可以使用类来封装学生信息,并通过实现 Comparable
接口或使用 Comparator
来自定义优先级规则。下面是一个例子:
import java.util.*;
// 定义一个学生类,包含姓名和三门课成绩
class Student implements Comparable<Student> {
String name; // 学生姓名
int ch, ma, en; // ch:语文成绩,ma:数学成绩,en:英语成绩
// 构造函数
public Student(String name, int ch, int ma, int en) {
this.name = name;
this.ch = ch;
this.ma = ma;
this.en = en;
}
// 获取总分
public int getTotal() {
return ch + ma + en;
}
// 实现Comparable接口的compareTo方法,用于优先队列内部比较优先级
@Override
public int compareTo(Student other) {
// 1. 总分不同,总分高的优先级高
if (this.getTotal() != other.getTotal()) {
return other.getTotal() - this.getTotal(); // 注意:Java的PriorityQueue默认是小根堆,所以这里用other减this来实现大根堆
}
// 2. 总分相同,语文高的优先级高
if (this.ch != other.ch) {
return other.ch - this.ch;
}
// 3. 总分和语文相同,数学高的优先级高
if (this.ma != other.ma) {
return other.ma - this.ma;
}
// 4. 总分、语文、数学都相同,英语高的优先级高
if (this.en != other.en) {
return other.en - this.en;
}
// 5. 所有成绩都相同,名字字典序小的优先级高
return this.name.compareTo(other.name);
}
}
public class Main {
public static void main(String[] args) {
// 声明一个存储Student类的优先队列(大根堆,优先级高的在队头)
PriorityQueue<Student> pq = new PriorityQueue<>();
// 向优先队列中加入学生信息
pq.add(new Student("silence76", 11, 45, 14));
pq.add(new Student("wangzai76", 14, 45, 11));
pq.add(new Student("qingchu76", 150, 150, 150));
// 取出优先级最高的学生(队头)
Student bestStudent = pq.peek();
System.out.println(bestStudent.name); // 输出优先级最高学生的姓名
pq.poll(); // 移除队头元素
System.out.println(pq.size()); // 输出当前队列中元素数量
System.out.println(pq.peek().name); // 输出当前队头学生的姓名
}
}
Python
在 Python 中,我们可以使用类来封装学生信息,并通过实现 __lt__
方法(小于比较)来自定义优先级规则。另外,我们也可以使用元组和 functools.cmp_to_key
来实现自定义排序。下面是一个使用类的例子:
import heapq
# 定义一个学生类,包含姓名和三门课成绩
class Student:
def __init__(self, name, ch, ma, en):
self.name = name # 学生姓名
self.ch = ch # 语文成绩
self.ma = ma # 数学成绩
self.en = en # 英语成绩
# 获取总分
def get_total(self):
return self.ch + self.ma + self.en
# 实现小于比较方法,用于优先队列内部比较优先级
def __lt__(self, other):
# 1. 总分不同,总分高的优先级高(由于Python的heapq是小根堆,所以这里用大于号来实现大根堆)
if self.get_total() != other.get_total():
return self.get_total() > other.get_total()
# 2. 总分相同,语文高的优先级高
if self.ch != other.ch:
return self.ch > other.ch
# 3. 总分和语文相同,数学高的优先级高
if self.ma != other.ma:
return self.ma > other.ma
# 4. 总分、语文、数学都相同,英语高的优先级高
if self.en != other.en:
return self.en > other.en
# 5. 所有成绩都相同,名字字典序小的优先级高
return self.name < other.name
# 创建一个空列表作为优先队列
pq = []
# 向优先队列中加入学生信息
heapq.heappush(pq, Student("silence76", 11, 45, 14))
heapq.heappush(pq, Student("wangzai76", 14, 45, 11))
heapq.heappush(pq, Student("qingchu76", 150, 150, 150))
# 取出优先级最高的学生(队头)
best_student = pq[0]
print(best_student.name) # 输出优先级最高学生的姓名
heapq.heappop(pq) # 移除队头元素
print(len(pq)) # 输出当前队列中元素数量
print(pq[0].name) # 输出当前队头学生的姓名
优先队列的遍历
C++
在 priority_queue
中,我们无法使用迭代器直接对优先队列进行遍历,这使得我们在处理一些信息时会遇到一些困难。不过,我们可以通过一些费劲的方法来遍历优先队列,比如说:先依次将所有元素出队并放入一个序列中,然后再遍历这个序列,同时将元素重新放回到优先队列中。显然,这种做法过于费时费力,有没有方便一点的方法呢?
其实,multiset
内部的各个元素是有序的,如果我们使用 multiset
来实现优先队列,那么我们就可以直接用迭代器遍历 multiset
来获取优先队列的元素,而不需要将元素一个个出队再放回去。
对应的使用方法也很简单:
如果用 T
代表你想放进优先队列里面的元素的变量类型,用 ms
代表我们要创建的优先队列的变量名,则优先队列的声明语法为:
multiset<T> ms;
multiset
内部会把所有元素按照从小到大的顺序排列,所以如果你关注 multiset
的起始位置,那么此时获取的就是优先级最小的元素,也就相当于是一个小根堆。如果你关注 multiset
的末尾位置,那么此时获取的就是优先级最大的元素,也就相当于是一个大根堆。这里需要注意的细节是,ms.end()
代表的是 multiset
的末尾位置的下一个位置,所以如果你要获取 multiset
的末尾位置,你需要使用 --ms.end()
来获取。
对于优先队列(小根堆)的四个操作,他们分别的语法如下表所示:
元素入队 | ms.insert(3) |
|
队头出队 | ms.erase(ms.begin()) |
|
查看队头 | *ms.begin() |
|
查看队列长度 | ms.size() |
对于优先队列(大根堆)的四个操作,他们分别的语法如下表所示:
元素入队 | ms.insert(3) |
|
队头出队 | ms.erase(--pq.end()) |
|
查看队头 | * --ms.end() |
|
查看队列长度 | ms.size() |
在此基础上,我们就可以通过迭代器来直接遍历优先队列了:
for(auto it=ms.begin();it!=ms.end();it++){
cout<<*it<<endl;
}
Java
在Java中,PriorityQueue
是一个可以实现优先队列的类,它内部使用堆结构来存储元素,默认为小根堆(最小值在堆顶)。但是 PriorityQueue
类不支持直接的迭代器遍历,因为它没有提供像C++中迭代器那样的访问方式。
不过,我们可以通过其他方式实现优先队列的遍历,例如将元素复制到一个数组中,然后遍历数组。或者使用 PriorityQueue
的 forEach
方法来遍历集合中的每个元素。以下是一个简单的示例:
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.add(3);
pq.add(1);
pq.add(4);
// 遍历优先队列中的元素
pq.forEach(System.out::println);
这里需要注意的是,PriorityQueue
的遍历顺序不一定是元素的自然顺序,因为 forEach
方法并不保证以特定顺序处理元素。
如果你想实现大根堆的功能,可以通过 PriorityQueue
的自定义比较器来实现:
PriorityQueue<Integer> pq = new PriorityQueue<>(Comparator.reverseOrder());
pq.add(3);
pq.add(1);
pq.add(4);
// 遍历优先队列中的元素
pq.forEach(System.out::println);
对于优先队列(小根堆)和大根堆的四个基本操作,它们的语法如下表所示:
元素入队 | pq.add(3) |
|
队头出队 | pq.poll() |
|
查看队头 | pq.peek() |
|
查看队列长度 | pq.size() |
Python
在Python中,heapq
模块提供了堆队列算法的实现。Python的堆默认是最小堆(小根堆),不过可以通过对元素进行符号反转来实现最大堆(大根堆)的效果。
以下是如何使用Python的 heapq
模块来实现优先队列:
import heapq
pq = []
heapq.heappush(pq, 3)
heapq.heappush(pq, 1)
heapq.heappush(pq, 4)
# 遍历优先队列中的元素
for num in pq:
print(num)
在Python中,heapq
模块的堆结构是一个数组,数组中的元素按堆的结构排列。但需要注意的是,遍历堆时得到的顺序并不是按堆的顺序排列的,堆顶的元素始终是最小的(因为是小根堆),但整个数组的顺序并不是按排序后的顺序排列的。
对于大根堆,可以通过将元素反转标志(取负数)来实现:
import heapq
pq = []
heapq.heappush(pq, -3)
heapq.heappush(pq, -1)
heapq.heappush(pq, -4)
# 遍历并还原元素的原始值
for num in [-x for x in pq]:
print(num)
对于优先队列(小根堆)和大根堆的四个基本操作,它们的语法如下表所示:
元素入队 | heapq.heappush(pq, 3) |
|
队头出队 | heapq.heappop(pq) |
|
查看队头 | pq[0] |
|
查看队列长度 | len(pq) |
课后习题
汗牛充栋,学海无涯。<br/> 内含算法知识点讲解,以及牛客题库精选例题。<br/> 学习算法,从牛栋开始。