一,堆
1,概念
1.1 堆的概念
堆是一种特殊的完全二叉树,它保证每个父节点的值都不大于(或不小于)其子节点的值,因此堆顶永远是最大(或最小)的元素。其具有以下性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值
- 堆总是一棵完全二叉树
堆又分为大根堆和小根堆,父节点比子节点大的是大根堆,反之则是小根堆。
1.2 堆的存储
因为堆是一棵完全二叉树,所以能以按层序(从上到下、从左到右)的顺序存储在数组中(根节点为下标0)。存储结构如下图所示:

注:
在二叉树章节我们曾提及:二叉树分为链式存储和线性存储。但若使用数组存储非完全二叉树,其会存在大量空节点,造成空间上的浪费;而完全二叉树则可以按照层序遍历顺序进行存储,不会浪费空间,因此一般的:
完全二叉树 ==> 使用线性存储
非完全二叉树 ==> 使用链式存储
并且在调整堆时,我们可以使用到以下性质获取节点下标:
简单来说就是
- 由父得子:左孩子下标 = (父 * 2) + 1;右孩子下标 = (父 * 2) + 2
- 由子得父:父节点下标 = (子 - 1)/ 2
2,实现及重要方法讲解
接下来将以小根堆为例进行讲解,代码中所有抛出的异常都是自定义异常类。
2.1 类定义
由概念部分我们可知,堆是一颗完全二叉树,可以使用数组进行存储,因此定义elem数组作为基本存储结构;定义usedSize变量用于记录堆中有效元素个数;定义DEFAULT_INITIAL_CAPACITY 作为默认堆容量。
java
public class MyHeap {
private int[] elem;
private int usedSize = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 11;
}
2.2 siftUp:向上调整
在向上调整时,siftUp函数一般接收待调整的子节点下标child作为形参列表,其执行逻辑如下:
1,若是child > 0,则说明往上还有节点,故进入循环继续调整
2,利用 父节点下标 = (子 - 1)/ 2 这一性质获取到父节点下标,使用parent记录
3,比较子节点和父节点值大小:
3.1 ***子节点值 < 父节点值:***父节点大于子节点,违反小根堆性质,因此交换父子节点元素值将其调整为小根堆,再让child = parent继续向上调整(因为交换后修改了以parent为子节点的树,故向上调整即是调整受影响的树)
3.2 ***子节点值 >= 父节点值:***父节点小于等于子节点,是小根堆,无需调整,直接返回即可(默认整棵树是小根堆,因此当目前子树是小根堆时即无需再调整)
4,循环结束则说明整棵树调整完毕
调整流程如下图所示:

代码如下所示:
java
/**
* 向上调整
* @param child 新插入节点位置
*/
private void siftUp(int child) {
// 当child<=0时则说明已调整完整棵树
while(child > 0) {
// 获取父节点下标
int parent = (child - 1) / 2;
if(this.elem[child] < this.elem[parent]) {
// 父节点>子节点 ==> 交换元素并使child = parent向上继续调整
swap(child, parent);
child = parent;
} else {
// 父节点<=子节点 ==> 符合小根堆,无需调整,直接返回
break;
}
}
}
2.3 siftDown:向下调整
在向下调整时,siftDown函数一般需要接收待调整子树的父节点下标parent与堆有效元素个数len(用于明确向下调整的结束位置)这两个参数,其执行逻辑如下:
1,利用左孩子下标 =(父 * 2)+ 1 这一性质获取到左孩子下标,使用child记录
2,若是child < len,则说明左孩子存在,进入循环
3,判断右孩子是否存在,若是右孩子存在 且 右孩子值<左孩子值,则child保存右孩子下标(child保存左右孩子中值最小的那个)
4,比较child子节点与parent父节点值大小:
4.1 ***子节点值 < 父节点值:***父节点大于子节点,违反小根堆性质,因此交换父子节点元素值将其调整为小根堆,再执行parent = child,child = (child * 2)+ 1继续向下调整(因为交换后修改了以child为父节点的树,故向下调整即是调整受影响的树)
4.2 ***子节点值 >= 父节点值:***父节点小于等于子节点,是小根堆,无需调整,直接返回即可(默认整棵树是小根堆,因此当目前子树是小根堆时即无需再调整)
5,循环结束则说明整棵树调整完毕
注:
使用len确定向下调整的结束位置,其本质是判断当前节点是否还有左右子树。若是child>=len,则说明左右子树不存在,没有受影响的子树,整颗树调整完毕。
调整流程如下图所示:

代码如下所示:
java
/**
* 向下调整
* @param parent 根节点下标
* @param len 调整界限
*/
private void siftDown(int parent, int len) {
// 获取左孩子下标
int child = parent * 2 + 1;
// 左孩子存在则进入循环调整
while(child < len) {
// child保存左右孩子中值最小的那个
if(child + 1 < len && this.elem[child + 1] < this.elem[child]) {
child ++;
}
if(this.elem[child] < this.elem[parent]) {
// 调整本树,并继续向下调整
swap(child, parent);
parent = child;
child = child * 2 + 1;
} else {
break;
}
}
}
2.4 add / poll / peek
<1> add 在堆中插入元素
新增逻辑如下:
1,判满,若满则扩容
2,不满则将尾插新元素
3,向上调整受影响的位置,使其在插入后仍能保证是小根堆
4,维护有效元素个数
流程如下图所示:

代码如下图所示:
java
/**
* 在堆中插入元素
* 插入到尾部,然后向上调整
* @param val 待插入元素
*/
public void add(int val) {
// 判满并扩容(1.5倍)
if(isFull()) {
grow();
}
// 尾插新元素
this.elem[this.usedSize] = val;
// 向上调整
siftUp(this.usedSize);
this.usedSize ++;
}
<2> poll 删除堆顶元素
删除逻辑如下:
1,判空,若空则抛出异常(具体操作视实际场景需求而定)
2,非空,交换首尾元素,维护有效元素个数(交换后就执行usedSize--,也就是逻辑删除尾部元素,并且也利于向下调整时传递结束位置)(此处代码我先维护了有效元素个数,是因为尾节点下标为usedSize-1,故提前处理)
3,向下调整根节点,使受交换影响的树重新成为小根堆
流程如下图所示:

代码如下所示:
java
/**
* 弹出堆顶元素
* 首尾交换,然后调整下标为0的树
* @return int
*/
public int poll() {
// 判空并抛出异常
if(isEmpty()) {
throw new HeapEmptyException("堆为空,无法执行删除操作!");
}
// 记录堆顶元素
int oldVal = this.elem[0];
// 交换首尾元素并维护有效元素个数
this.usedSize --;
swap(0, this.usedSize);
// 向下调整根节点
siftDown(0, this.usedSize);
return oldVal;
}
<3> 获取堆顶元素
获取逻辑较为简单,如下:
1,判空,若空则抛出异常(具体操作视实际场景需求而定)
2,非空,返回堆顶元素(下标为0的值)
代码如下所示:
java
/**
* 获取堆顶元素
* @return int
*/
public int peek() {
// 判空并抛出异常
if(isEmpty()) {
throw new HeapEmptyException("堆为空,无法执行获取堆顶元素操作!");
}
return this.elem[0];
}
2.5 heapify:建堆
我们通常使用向下调整的方式建堆,流程如下:
1,获取最后一棵子树的父节点,从该节点开始,逐树向下调整
注:
最后一棵子树父节点下标 = (最后一个有效元素下标 - 1)/ 2
2,调整完毕后建堆完成
流程如下图所示:

代码如下所示:
java
/**
* 建堆
*/
private void heapify() {
// 从最后一棵子树开始,逐树向下调整
for(int parent = (this.usedSize - 1 - 1) / 2; parent >= 0; parent --) {
siftDown(parent, this.usedSize);
}
}
2.6 扩容与构造器
<1> 构造器
在本次实现中,我定义了三个构造方法,分别是:
1,无参构造方法
为底层数组开辟默认大小空间
java
/**
* 无参构造方法
* */
public MyHeap() {
this.elem = new int[DEFAULT_INITIAL_CAPACITY];
}
2,带有一个初始容量的构造方法
若是给定的初始容量合法,则开辟指定大小空间;否则使用默认容量
java
/**
* 给定带有一个初始容量的构造方法
*/
public MyHeap(int capacity) {
// 校验capacity
if(capacity < 0 || capacity > (Integer.MAX_VALUE - 8)) {
this.elem = new int[DEFAULT_INITIAL_CAPACITY];
} else {
this.elem = new int[capacity];
}
}
3,带有一个初始化序列的构造方法
按照给定的初始化序列建堆,此处涉及开辟内存,需要校验合法性。
java
/**
* 给定带有一个初始化序列的构造方法
* @param initialArray 初始化序列
*/
public MyHeap(int[] initialArray) {
// 判空
if(initialArray == null) {
this.elem = new int[DEFAULT_INITIAL_CAPACITY];
} else {
// 校验数组
if(initialArray.length > Integer.MAX_VALUE - 8) {
throw new IllegalArgumentException("初始数组过大!");
}
// 开辟内存
long cap = Math.max(1, (long)(initialArray.length * 1.5));
if(cap > Integer.MAX_VALUE - 8) {
cap = Integer.MAX_VALUE - 8;
}
this.elem = new int[(int)cap];
// 写入数组
for(int i = 0; i < initialArray.length; i ++) {
this.elem[i] = initialArray[i];
this.usedSize ++;
}
// 建堆
heapify();
}
}
<2> 扩容
使用1.5倍扩容
java
/**
* 1.5倍扩容
*/
private void grow() {
// 扩容时至少保证容量大于当前的usedSize
int oldCap = this.elem.length;
int minCap = this.usedSize + 1;
long newCapacity = Math.max(minCap, (long)(oldCap * 1.5));
// 判断新容量合法性
if(newCapacity > (Integer.MAX_VALUE - 8)) {
if(oldCap >= Integer.MAX_VALUE - 8) {
throw new HeapFullException("堆容量已达上限!");
}
newCapacity = Integer.MAX_VALUE - 8;
}
// 扩容
this.elem = Arrays.copyOf(this.elem, (int)newCapacity);
}
二,优先级队列
1,基于堆实现优先级队列
优先级队列是一种抽象的数据类型,它与普通队列先进先出的规则不同:每个元素都有一个优先级,出队时总是优先级最高(最低)的元素被取出。因为它是一种抽象的数据类型,因此存在多种实现方式,其中堆是实现它最经典、最常用的方式之一。
堆实现优先级队列与实现堆本身的代码高度相似。为避免重复实现并保持封装(不把堆的全部 public 方法暴露给使用者),此处采用组合 + 委托的方式,基于上文的小根堆实现一个最小优先的优先级队列,代码如下所示:
java
/**
* 基于堆实现优先级队列
* @author 14993
* @date 2026/10/21
*/
public class MyPriorityQueue {
private MyHeap heap;
/**
* 无参构造方法
*/
public MyPriorityQueue() {
heap = new MyHeap();
}
/**
* 给定带有一个初始容量的构造方法
* @param capacity 初始容量
*/
public MyPriorityQueue(int capacity) {
heap = new MyHeap(capacity);
}
/**
* 给定带有一个初始化序列的构造方法
* @param initialArray 初始化序列
*/
public MyPriorityQueue(int[] initialArray) {
heap = new MyHeap(initialArray);
}
/**
* 入队
* @param val 新元素
*/
public void offer(int val) {
heap.add(val);
}
/**
* 出队
* @return int
*/
public int poll() {
return heap.poll();
}
/**
* 获取队头元素
* @return int
*/
public int peek() {
return heap.peek();
}
/**
* 判空
* @return boolean
*/
public boolean isEmpty() {
return heap.isEmpty();
}
/**
* 获取有效元素个数
* @return int
*/
public int size() {
return heap.size();
}
}
通过上面代码可以看出:MyPriorityQueue持有一个 MyHeap成员,并将 offer/poll/peek等操作委托给该成员完成。也就是说,它是以组合方式复用堆这一数据结构,来实现优先级队列这一抽象数据类型;堆是底层实现,优先级队列是对外呈现的接口。
2,Java集合类中的实现
优先级队列在Java中提供了两种实现,分别是PriorityQueue和PriorityBlockingQueue,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的。
其中PriorityQueue具有以下特性:
- 默认是小根堆
- 默认容量为11,可以传递Comparator比较器,并且我们可以通过修改传递比较器的比较逻辑去定义优先级。例如PriorityQueue默认小根堆,传递Comparator.reverseOrder()可变为大根堆。
- 若是没有传递Comparator比较器,则必须实现Comparable接口,否则无法比较并且会抛出异常
- 扩容时,旧容量小于64时约按2倍扩容(new = old*2+2),否则按1.5倍扩容