PriorityQueue 是 Java 集合框架中的一个重要类,它底层基于**堆(Heap)**这种数据结构实现,能够保证每次取出的元素都是队列中优先级最高的(默认是最小元素,即小顶堆)。
堆的模拟实现
PriorityQueue 的底层核心是堆,我们先通过代码手动实现一个大顶堆(每个父结点的值都大于等于其子结点的值),理解堆的初始化、插入、删除、调整等核心操作。
完整模拟实现代码
import java.util.Arrays;
/**
* 模拟实现大顶堆(对应 PriorityQueue 底层结构)
* 底层用数组存储数据,实现堆的初始化、插入、删除、排序等核心操作
*/
public class MyHeap {
// 存储堆元素的数组
public int[] elem;
// 记录堆中有效元素的个数(区别于数组容量)
public int usedSize;
// 构造方法:初始化数组容量为10
public MyHeap() {
this.elem = new int[10];
}
/**
* 初始化堆的元素数组
* @param arr 待初始化的原始数组
*/
public void initArray(int[] arr) {
for (int i = 0; i < arr.length; i++) {
this.elem[i] = arr[i];
this.usedSized++;
}
}
/**
* 构建大顶堆(核心:从最后一个非叶子结点开始向下调整)
*/
public void createHeap() {
// 最后一个非叶子结点的索引:(usedSized-1-1) = (最后一个元素索引 - 1) / 2
for (int parent = (usedSized - 2) / 2; parent >= 0; parent--) {
// 对每个非叶子结点进行向下调整,构建大顶堆
siftDown(parent, usedSized);
}
}
/**
* 堆的向下调整(核心方法:用于构建堆、删除堆顶元素后调整堆)
* @param parent 待调整的父结点索引
* @param usedSized 堆的有效元素个数(调整范围边界)
*/
private void siftDown(int parent, int usedSized) {
// 先获取左孩子结点索引(堆的数组存储特性:左孩子 = 2*parent + 1)
int child = parent * 2 + 1;
// 循环条件:孩子结点索引在有效范围内(说明存在孩子结点)
while (child < usedSized) {
// 1. 找到左右孩子中值较大的那个孩子(右孩子存在且右孩子值 > 左孩子值)
if (child + 1 < usedSized && elem[child] < elem[child + 1]) {
child++; // 此时 child 指向右孩子(较大值的孩子)
}
// 2. 比较较大孩子与父结点的值,若孩子值 > 父结点值,交换两者
if (elem[child] > elem[parent]) {
// 交换父结点和孩子结点的值
int tmp = elem[child];
elem[child] = elem[parent];
elem[parent] = tmp;
// 3. 向下继续调整:将父结点更新为当前孩子结点,孩子结点更新为新父结点的左孩子
parent = child;
child = 2 * parent + 1;
} else {
// 若孩子值 <= 父结点值,说明当前堆结构已满足大顶堆要求,直接退出
break;
}
}
}
/**
* 向堆中插入元素(对应 PriorityQueue 的 offer 方法)
* @param val 待插入的元素值
*/
public void offer(int val) {
// 1. 判断堆是否已满,若已满则扩容(扩容为原容量的2倍)
if (isFull()) {
this.elem = Arrays.copyOf(elem, 2 * elem.length);
}
// 2. 将新元素插入到堆的末尾(有效元素个数的位置)
elem[usedSized] = val;
// 3. 向上调整堆,恢复大顶堆的特性
siftUp(usedSized);
// 4. 有效元素个数加1
usedSized++;
}
/**
* 堆的向上调整(核心方法:用于插入元素后调整堆)
* @param child 待调整的孩子结点索引(刚插入的元素索引)
*/
private void siftUp(int child) {
// 1. 获取当前孩子结点的父结点索引(堆的数组存储特性:父结点 = (child - 1) / 2)
int parent = (child - 1) / 2;
// 2. 循环条件:父结点索引 >= 0(说明存在父结点)
while (parent >= 0) {
// 3. 比较孩子结点与父结点的值,若孩子值 > 父结点值,交换两者
if (elem[child] > elem[parent]) {
// 交换父结点和孩子结点的值
int tmp = elem[child];
elem[child] = elem[parent];
elem[parent] = tmp;
// 4. 向上继续调整:将孩子结点更新为当前父结点,父结点更新为新孩子结点的父结点
child = parent;
parent = (child - 1) / 2;
} else {
// 若孩子值 <= 父结点值,说明当前堆结构已满足大顶堆要求,直接退出
break;
}
}
}
/**
* 判断堆是否已满
* @return 已满返回 true,否则返回 false
*/
public boolean isFull() {
return usedSized == this.elem.length;
}
/**
* 删除堆顶元素(对应 PriorityQueue 的 poll 方法,大顶堆删除的是最大值)
* @return 被删除的堆顶元素值,堆为空时返回 -1
*/
public int poll() {
// 1. 判断堆是否为空,若为空返回 -1
if (isEmpty()) {
return -1;
}
// 2. 保存堆顶元素值(用于返回)
int val = elem[0];
// 3. 交换堆顶元素和最后一个有效元素
swap(0, usedSized - 1);
// 4. 有效元素个数减1(逻辑上删除最后一个元素,即原堆顶元素)
usedSized--;
// 5. 向下调整堆,恢复大顶堆的特性
siftDown(0, usedSized);
// 6. 返回被删除的堆顶元素
return val;
}
/**
* 交换数组中两个索引位置的元素
* @param a 第一个元素索引
* @param b 第二个元素索引
*/
private void swap(int a, int b) {
int tmp = elem[a];
elem[a] = elem[b];
elem[b] = tmp;
}
/**
* 判断堆是否为空
* @return 为空返回 true,否则返回 false
*/
public boolean isEmpty() {
return usedSized == 0;
}
/**
* 获取堆顶元素(对应 PriorityQueue 的 peek 方法)
* @return 堆顶元素值,堆为空时返回 -1
*/
public int peek() {
if (isEmpty()) {
return -1;
}
return elem[0];
}
/**
* 堆排序(基于大顶堆实现升序排序)
*/
public void heapSort() {
// 1. 记录最后一个有效元素的索引
int end = usedSized - 1;
// 2. 循环交换堆顶元素和最后一个有效元素,并调整堆
while (end > 0) {
// 交换堆顶(最大值)和当前最后一个有效元素
swap(0, end);
// 向下调整堆(调整范围为 [0, end),不包含 end)
siftDown(0, end);
// 最后一个有效元素索引向前移动一位
end--;
}
}
}
核心方法说明
createHeap()构建堆:从最后一个非叶子结点开始,依次对每个结点执行向下调整,最终将普通数组转换为大顶堆,时间复杂度为 O(n)。siftDown()向下调整:堆构建、堆顶元素删除后的核心调整方法,通过比较父结点与孩子结点,将较小的父结点“下沉”到合适位置。offer()+siftUp()插入元素:先将元素插入到数组末尾,再通过向上调整,将较大的新元素“上浮”到合适位置,保证堆的特性不被破坏。poll()删除堆顶:通过交换堆顶与最后一个元素,再执行向下调整,高效删除堆顶元素,时间复杂度为 O(logn)。
堆的实际使用
理解了堆的底层实现后,我们来学习 Java 中 PriorityQueue 的实际使用,它默认实现了小顶堆,也可以通过自定义比较器实现大顶堆。
堆的构造方法
PriorityQueue 提供了多种构造方法,常用的有以下三种,满足不同场景的初始化需求:
import java.util.ArrayList;
import java.util.List;
import java.util.PriorityQueue;
public class PriorityQueueTest {
public static void main(String[] args) {
// 1. 无参构造:创建一个空的 PriorityQueue,默认容量为11,默认是小顶堆
PriorityQueue<Integer> queue1 = new PriorityQueue<>();
// 2. 指定容量构造:创建一个指定初始容量的 PriorityQueue,默认是小顶堆
PriorityQueue<Integer> queue2 = new PriorityQueue<>(100);
// 3. 集合构造:通过已有的 Collection 集合创建 PriorityQueue,默认是小顶堆
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
PriorityQueue<Integer> queue3 = new PriorityQueue<>(list);
}
}
大顶堆的构造(自定义比较器)
PriorityQueue 默认是小顶堆,若需要大顶堆,需要通过自定义比较器实现,常用两种方式:实现 Comparator 接口、使用 Lambda 表达式(简洁高效)。
方式1:实现 Comparator 接口
import java.util.Comparator;
import java.util.PriorityQueue;
/**
* 实现 Comparator 接口,自定义比较规则构建大顶堆
*/
public class MaxHeapTest implements Comparator<Integer> {
public static void main(String[] args) {
// 传入自定义比较器实例,构建大顶堆
PriorityQueue<Integer> queue = new PriorityQueue<>(new MaxHeapTest());
// 向队列中插入元素
queue.offer(1);
queue.offer(2);
queue.offer(3);
queue.offer(4);
queue.offer(5);
// 输出堆顶元素(预期为 5,大顶堆堆顶是最大值)
System.out.println("堆顶元素:" + queue.peek());
}
/**
* 重写 compare 方法,定义比较规则
* @param o1 第一个待比较元素
* @param o2 第二个待比较元素
* @return 返回值 > 0:o1 > o2;返回值 = 0:o1 = o2;返回值 < 0:o1 < o2
* 此处返回 o2 - o1,实现大顶堆(优先保留较大值)
*/
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
}
方式2:Lambda 表达式(推荐,更简洁)
import java.util.PriorityQueue;
public class MaxHeapLambdaTest {
public static void main(String[] args) {
// 使用 Lambda 表达式直接传入比较规则,构建大顶堆
PriorityQueue<Integer> queue = new PriorityQueue<>((o1, o2) -> o2 - o1);
// 向队列中插入元素
queue.offer(1);
queue.offer(2);
queue.offer(3);
queue.offer(4);
queue.offer(5);
// 输出堆顶元素(预期为 5)
System.out.println("堆顶元素:" + queue.peek());
}
}
堆的常用方法
PriorityQueue 提供了一套简洁的方法用于操作队列,核心方法与我们模拟实现的堆方法一一对应,常用方法如下:
import java.util.PriorityQueue;
public class PriorityQueueCommonMethods {
public static void main(String[] args) {
// 创建一个默认的小顶堆
PriorityQueue<Integer> queue = new PriorityQueue<>();
// 1. offer(E e):向队列中插入元素,插入失败抛出异常(实际开发中常用,比 add 更安全)
queue.offer(1);
queue.offer(2);
queue.offer(3);
System.out.println("插入元素后队列:" + queue);
// 2. peek():获取堆顶元素(不删除),队列为空时返回 null
Integer top = queue.peek();
System.out.println("堆顶元素:" + top);
// 3. poll():删除并返回堆顶元素,队列为空时返回 null
Integer removedTop = queue.poll();
System.out.println("被删除的堆顶元素:" + removedTop);
System.out.println("删除堆顶后队列:" + queue);
// 4. size():返回队列中有效元素的个数
int size = queue.size();
System.out.println("队列元素个数:" + size);
// 5. clear():清空队列中的所有元素
queue.clear();
System.out.println("清空后队列:" + queue);
// 6. isEmpty():判断队列是否为空,为空返回 true,否则返回 false
boolean isEmpty = queue.isEmpty();
System.out.println("队列是否为空:" + isEmpty);
}
}
运行结果参考
插入元素后队列:[1, 2, 3]
堆顶元素:1
被删除的堆顶元素:1
删除堆顶后队列:[2, 3]
队列元素个数:2
清空后队列:[]
队列是否为空:true