优先级队列(堆)
1. 优先级队列
1.1 概念
前面我们了解了队列------一种先进先出(FIFO)的数据结构。但在某些场景中,数据操作需要考虑优先级,要求高优先级的元素先出队列。这时普通队列就无法满足需求了,比如:
- 手机游戏运行时接到来电,系统会优先处理电话
- 初中班主任安排座位时,成绩优异的学生可以优先选择
针对这类需求,数据结构需要提供两个核心操作:返回最高优先级对象和添加新对象。这种支持优先级处理的数据结构就称为优先级队列(Priority Queue)。
2. 优先级队列的模拟实现
JDK1.8的PriorityQueue底层采用堆数据结构实现,该结构本质上是对完全二叉树的一种优化调整。
2.1 堆的概念
给定一个关键码集合 K = {k₀, k₁, k₂, ..., kₙ₋₁},若将其所有元素按照完全二叉树的顺序存储在一维数组中,并满足以下条件之一:
- 对于所有 i = 0, 1, 2...,有 kᵢ ≤ k₂ᵢ₊₁ 且 kᵢ ≤ k₂ᵢ₊₂,则称为小堆(或最小堆)
- 对于所有 i = 0, 1, 2...,有 kᵢ ≥ k₂ᵢ₊₁ 且 kᵢ ≥ k₂ᵢ₊₂,则称为大堆(或最大堆)
其中,根节点值最大的堆称为最大堆(大根堆),根节点值最小的堆称为最小堆(小根堆)。
堆的性质:
- 堆中某个节点的值总是不⼤于或不⼩于其⽗节点的值;
- 堆总是⼀棵完全⼆叉树。

2.2 堆的存储⽅式
从堆的概念可知,堆是⼀棵完全⼆叉树,因此可以层序的规则采⽤顺序的⽅式来⾼效存储

注意:对于非完全二叉树,顺序存储方式并不适用。因为需要额外存储空节点以确保树结构的正确还原,这将显著降低空间利用率。
将元素存储到数组中后,可以根据⼆叉树章节的性质5对树进⾏还原。假设i为节点在数组中的下标,则有:
- 当i为0时,该节点为根节点;否则,其父节点下标为⌊(i-1)/2⌋
- 若2i+1小于节点总数,则节点i的左子节点下标为2i+1,否则无左子节点
- 若2i+2小于节点总数,则节点i的右子节点下标为2i+2,否则无右子节点
2.3 堆的创建
2.3.1 堆向下调整
对于集合{ 27,15,19,18,28,34,65,49,25,37 }中的数据,如果将其创建成堆呢?

仔细观察上图后发现:根节点的左右⼦树已经完全满⾜堆的性质,因此只需将根节点向下调整好即
可。
向下过程(以小顶堆为例):
- 初始化parent指向待调整节点,child指向parent的左孩子(注意:parent若有孩子必先有左孩子)
- 当child < size时(即左孩子存在),循环执行以下步骤:
- 若parent存在右孩子,则比较左右孩子,取较小者作为child
- 比较parent与child:
- 若parent ≤ child,调整完成
- 否则交换parent与child,并更新指针: parent = child child = 2*parent +1 然后继续步骤2的判断

参考代码:
java
public void shiftDown(int[] array, int parent){
//child 先标记 parent的左孩子,因为 parent可能有左没有右
int child = parent * 2 + 1;
int size = array.length;
while(child < size){
//如果右孩子存在,找到左右孩子中较小的孩子,用child标记
if(child+1 < size && array[child+1] < array[child]){
child += 1;
}
//如果双亲比最小的孩子还要小,说明已经满足堆的特性
if(array[parent] < array[child]){
break;
}else{
//将双亲与最小的孩子交换
int temp = array[parent];
array[parent] = array[child];
array[child] = temp;
//parent中大的元素往下移动,可能会造成子树不满足堆的性质,因此需要继续向下调整.
parent = child;
child = parent * 2 + 1;
}
}
}
注意:在以parent节点为根的⼆叉树进行调整时,必须确保其左右⼦树已经是堆结构才能执行向下调整操作。
时间复杂度分析: 在最坏情况下(如图所示),需要从根节点一直比较到叶⼦节点,比较次数等于完全⼆叉树的⾼度
2.3.2 堆的创建
那对于普通的序列{ 1,5,3,8,7,6 },即根节点的左右⼦树不满⾜堆的特性,⼜该如何调整呢?

参考代码:
java
public static void createHeap(int[] array) {
// 找倒数第⼀个⾮叶⼦节点,从该节点位置开始往前⼀直到根节点,遇到⼀个节点,应⽤向下调整
int root = ((array.length-2)>>1);
for (; root >= 0; root--) {
shiftDown(array, root);
}
}
2.3.3 建堆的时间复杂度
堆结构属于完全二叉树的一种特殊情况,而满二叉树作为完全二叉树的特例,在此处被用来简化时间复杂度分析。由于时间复杂度本身考察的是近似量级,增加少量节点不会影响最终结论。

因此:建堆的时间复杂度为O(N)。
2.4 的插⼊与删除
2.4.1 堆的插⼊
堆的插⼊总共需要两个步骤:
-
先将元素放⼊到底层空间中(注意:空间不够时需要扩容)
-
将最后新插⼊的节点向上调整,直到满⾜堆的性质

参考代码:
java
public void shiftUp(int[] array, int child){
//找到child的双亲
int parent = (child - 1) / 2;
while (child > 0){
//如果双亲比孩子小,parent满足堆的性质,调整结束
if(array[parent] <= array[child]){
break;
}else{
//将双亲与孩子节点进行交换
int t = array[parent];
array[parent] = array[child];
array[child] = t;
//大的元素向下移动,可能导致子树不满足堆的性质,因此要继续向上递增
child = parent;
parent = (child - 1) / 2;
}
}
}
2.4.2 堆的删除
注意:堆的删除⼀定删除的是堆顶元素。具体如下:
-
将堆顶元素对堆中最后⼀个元素交换
-
将堆中有效数据个数减少⼀个
-
对堆顶元素进⾏向下调整

2.5 ⽤堆模拟实现优先级队列
java
public class MyPriorityQueue {
// 演⽰作⽤,不再考虑扩容部分的代码
private int[] array = new int[100];
private int size = 0;
public void offer(int e) {
array[size++] = e;
shiftUp(size - 1);
}
public int poll() {
int oldValue = array[0];
array[0] = array[--size];
shiftDown(0);
return oldValue;
}
public int peek() {
return array[0];
}
}
常⻅习题:
1.下列关键字序列为堆的是:()
A: 100,60,70,50,32,65 B: 60,70,65,50,32,100 C: 65,100,70,32,50,60
D: 70,65,100,32,50,60 E: 32,50,100,70,65,60 F: 50,100,70,65,60,32
2.已知⼩根堆为8,15,10,21,34,16,12,删除关键字8之后需重建堆,在此过程中,关键字之间的⽐较
次数是()
A: 1 B: 2 C: 3 D: 4
3.最⼩堆[0,3,2,5,7,4,6,8],在删除堆顶元素0之后,其结果是()
A: [3,2,5,7,4,6,8] B: [2,3,5,7,4,6,8]
C: [2,3,4,5,7,8,6] D: [2,3,4,5,6,7,8]
参考答案
1.A 2.C 4.C
3. 常⽤接⼝介绍
3.1 PriorityQueue特性
Java集合框架提供了两种优先级队列实现:
- PriorityQueue:线程不安全
- PriorityBlockingQueue:线程安全
本文将重点介绍PriorityQueue的实现原理和使用方法。

关于PriorityQueue的使⽤要注意:
- 使⽤时必须导⼊PriorityQueue所在的包,即:
import java.util.PriorityQueue;
- PriorityQueue 中的元素必须实现大小比较功能,插入无法比较的对象会抛出 ClassCastException 异常
- 禁止插入 null 对象,否则会触发 NullPointerException
- 支持无限容量扩展,内部会自动进行扩容处理
- 插入和删除操作的时间复杂度为 O(log n)
- 底层采用堆数据结构实现
- 默认构建的是小顶堆,每次获取的都是最小元素
3.2 PriorityQueue 常用接口介绍
1. 优先级队列的构造
此处只是列出了PriorityQueue中常⻅的⼏种构造⽅式
| 构造器 | 功能介绍 |
|---|---|
| PriorityQueue() | 创建一个空的优先级队列,默认容量是 11 |
| PriorityQueue(int initialCapacity) | 创建一个初始容量为 initialCapacity 的优先级队列,注意:initialCapacity 不能小于 1,否则会抛 IllegalArgumentException 异常 |
| PriorityQueue(Collection<? extends E> c) | 用一个集合来创建优先级队列 |
java
static void TestPriorityQueue(){
// 创建⼀个空的优先级队列,底层默认容量是11
PriorityQueue<Integer> q1 = new PriorityQueue<>();
// 创建⼀个空的优先级队列,底层的容量为initialCapacity
PriorityQueue<Integer> q2 = new PriorityQueue<>(100);
ArrayList<Integer> list = new ArrayList<>();
list.add(4);
list.add(3);
list.add(2);
list.add(1);
// ⽤ArrayList对象来构造⼀个优先级队列的对象
// q3中已经包含了三个元素
PriorityQueue<Integer> q3 = new PriorityQueue<>(list);
System.out.println(q3.size());
System.out.println(q3.peek());
}
注意:默认情况下,PriorityQueue队列是⼩堆,如果需要⼤堆需要⽤⼾提供⽐较器
java
// ⽤⼾⾃⼰定义的⽐较器:直接实现Comparator接⼝,然后重写该接⼝中的compare⽅法即可
class IntCmp implements Comparator<Integer>{
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;
}
}
public class TestPriorityQueue {
public static void main(String[] args) {
PriorityQueue<Integer> p = new PriorityQueue<>(new IntCmp());
p.offer(4);
p.offer(3);
p.offer(2);
p.offer(1);
p.offer(5);
System.out.println(p.peek());
}
}
此时创建出来的就是⼀个⼤堆。
2. 插⼊/删除/获取优先级最⾼的元素
| 函数名 | 功能介绍 |
|---|---|
| boolean offer(E e) | 插入元素 e,插入成功返回 true,如果 e 对象为空,抛出 NullPointerException 异常,时间复杂度 O(log n),注意:空间不够时会进行扩容 |
| E peek() | 获取优先级最高的元素,如果优先级队列为空,返回 null |
| E poll() | 移除优先级最高的元素并返回,如果优先级队列为空,返回 null |
| int size() | 获取有效元素的个数 |
| void clear() | 清空队列 |
| boolean isEmpty() | 检测优先级队列是否为空,空返回 true |
java
static void TestPriorityQueue2(){
int[] arr = {4,1,9,2,8,0,7,3,6,5};
// ⼀般在创建优先级队列对象时,如果知道元素个数,建议就直接将底层容量给好
// 否则在插⼊时需要不多的扩容
// 扩容机制:开辟更⼤的空间,拷⻉元素,这样效率会⽐较低
PriorityQueue<Integer> q = new PriorityQueue<>(arr.length);
for (int e: arr) {
q.offer(e);
}
System.out.println(q.size()); // 打印优先级队列中有效元素个数
System.out.println(q.peek()); // 获取优先级最⾼的元素
// 从优先级队列中删除两个元素之和,再次获取优先级最⾼的元素
q.poll();
q.poll();
System.out.println(q.size()); // 打印优先级队列中有效元素个数
System.out.println(q.peek()); // 获取优先级最⾼的元素
q.offer(0);
System.out.println(q.peek()); // 获取优先级最⾼的元素
// 将优先级队列中的有效元素删除掉,检测其是否为空
q.clear();
if(q.isEmpty()){
System.out.println("优先级队列已经为空!!!");
}
else{
System.out.println("优先级队列不为空");
}
}
注意:以下是JDK 1.8中,PriorityQueue的扩容⽅式:JDK17也是类似的,只不过部分进⾏了封装
java
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// Double size if small; else grow by 50%
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));
// overflow-conscious code
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
queue = Arrays.copyOf(queue, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
优先级队列的扩容说明:
• 当容量小于64时,扩容方式是将原容量(oldCapacity)扩大2倍 • 当容量达到或超过64时,扩容方式是将原容量(oldCapacity)扩大1.5倍 • 当容量超过MAX_ARRAY_SIZE时,直接扩容至MAX_ARRAY_SIZE
3.3 编程练习
top-k问题:寻找最大或最小的前k个数据。例如:世界500强企业排名
java
class Solution {
public int[] smallestK(int[] arr, int k) {
// 参数检测
if(null == arr || k <= 0)
return new int[0];
PriorityQueue<Integer> q = new PriorityQueue<>(arr.length);
// 将数组中的元素依次放到堆中
for(int i = 0; i < arr.length; ++i){
q.offer(arr[i]);
}
// 将优先级队列的前k个元素放到数组中
int[] ret = new int[k];
for(int i = 0; i < k; ++i){
ret[i] = q.poll();
}
return ret;
}
}
该解法只是PriorityQueue的简单使⽤,并不是topK最好的做法,那topk该如何实现?
4. 堆的应⽤
4.1 PriorityQueue的实现
⽤堆作为底层结构封装优先级队列
4.2 堆排序
堆排序即利⽤堆的思想来进⾏排序,总共分为两个步骤:
1. 建堆
- 升序:建⼤堆
- 降序:建⼩堆
2. 利⽤堆删除思想来进⾏排序
建堆和堆删除中都⽤到了向下调整,因此掌握了向下调整,就可以完成堆排序。

常⻅习题:
1.⼀组记录排序码为(5 11 7 2 3 17),则利⽤堆排序⽅法建⽴的初始堆为()
A: (11 5 7 2 3 17) B: (11 5 7 2 17 3) C: (17 11 7 2 3 5)
D: (17 11 7 5 3 2) E: (17 7 11 3 5 2) F: (17 7 11 3 2 5)
答案:C
4.3 Top-k问题
TOP-K问题是指在数据集中找出前K个最大或最小的元素,这类问题通常涉及海量数据处理。常见的应用场景包括:专业排名前10、世界500强企业榜单、富豪排行榜、游戏活跃玩家前100等。
传统解决方案是通过排序,但当数据量极大时,排序方法存在明显缺陷(如无法一次性将所有数据加载到内存)。此时,采用堆结构是更高效的解决方案,其实现步骤如下:
-
构建初始堆:
- 使用数据集的前K个元素建立堆
- 若求前K个最大元素,建小顶堆
- 若求前K个最小元素,建大顶堆
-
筛选处理:
- 将剩余N-K个元素依次与堆顶元素比较
- 不满足条件则替换堆顶元素
- 处理完成后,堆中剩余的K个元素即为所求
(具体代码实现详见后续博客)
感谢您的观看!