PriorityQueue 源码阅读
PriorityQueue 源码阅读
PriorityQueue 简介
PriorityQueue 是 Java 中的一种队列数据结构,被称为 优先级队列。它和普通队列不同,普通队列都是遵循先进先出(FIFO)的原则,即先添加的元素先出队,后添加的元素后出队。而 PriorityQueue 则是按照元素的优先级来决定出队的顺序,默认情况下,优先级越小的元素先出队。
而优先队列的逻辑存储结构和普通队列有所不同,以 PriorityQueue 为例,其底层实际上是使用 小堆顶 形式的二叉堆,即值最小的元素优先出队。
JDK 自带 PriorityQueue 使用示例
我们不妨介绍一个 PriorityQueue 的使用示例了解以下 JDK 的 PriorityQueue。
基本类型优先队列
从测试结果来看 JDK 的 PriorityQueue 中的元素也是按照升序进行优先级排序的
import java.util.PriorityQueue;
public class Main {
public static void main(String[] args) {
//往队列中随机添加1000_0000条数据
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
int n = 1000_0000;
for (int i = 0; i < n; i++) {
priorityQueue.offer(RandomUtil.randomInt(1, n));
}
//将优先队列中的数据按照优先级取出并存到数组中
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = priorityQueue.poll();
}
//如果前一个元素大于后一个元素,则说明优先队列优先级排列有问题
for (int i = 1; i < arr.length; i++) {
if (arr[i - 1] > arr[i]) {
throw new RuntimeException("error PriorityQueue");
}
}
}
}
特殊类型优先队列
假如我们希望将自定义的对象放到优先队列中,并且我们希望优先队列按照年龄进行升序排序,那么我们就可以使用 JDK 的优先队列。
通过指明队列的泛型以及比较器,我们即可非常方便的实现一个存放自定义对象的优先队列。
public class Example {
static class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
public static void main(String[] args) {
PriorityQueue<Person> pq = new PriorityQueue<>(Comparator.comparing(Person::getAge));
pq.add(new Person("Alice", 25));
pq.add(new Person("Bob", 30));
pq.add(new Person("Charlie", 20));
while (!pq.isEmpty()) {
Person p = pq.poll();
System.out.println(p.getName() + " (" + p.getAge() + ")");
}
}
}
JDK 自带 PriorityQueue 源码分析
构造函数
先来看看这个传入数组容量 initialCapacity 和比较器的构造方法,可以看到 PriorityQueue 的构造方法要求用户传入一个 initialCapacity 用于初始化一个数组 queue,不难猜出这个数组就是优先队列底层所用到的二叉小顶堆。
同时该构造方法还要求用户传入一个 comparator 即一个2比较器,说明 queue 的元素在进行入队操作时是需要比较的,而这个 comparator 就是比较的依据。
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
//如果初始化容量小于1则抛出异常
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
再来看看另一个核心构造方法,我们发现 PriorityQueue 支持将不同的 Collection 类转化为 PriorityQueue,对此我们不妨对每一段逻辑进行深入分析。
public PriorityQueue(Collection<? extends E> c) {
//SortedSet类型的集合转为PriorityQueue,从SortedSet中获取一个比较器进行初始化,然后按照比较器的规则转移元素到PriorityQueue中
if (c instanceof SortedSet<?>) {
SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
this.comparator = (Comparator<? super E>) ss.comparator();
initElementsFromCollection(ss);
}
//如果集合类型是PriorityQueue则获取PriorityQueue的比较器,并将PriorityQueue的元素转移到我们的PriorityQueue中
else if (c instanceof PriorityQueue<?>) {
PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
this.comparator = (Comparator<? super E>) pq.comparator();
initFromPriorityQueue(pq);
}
else {
this.comparator = null;
initFromCollection(c);
}
}
当集合类型为 SortedSet 时,因为 SortedSet 是天生有序的,所以 PriorityQueue 直接获取其比较器之后,调用了一个 initElementFromCollection 具体做了些什么。
//SortedSet类型的集合转为PriorityQueue,从SortedSet中获取一个比较器进行初始化,然后按照比较器的规则转移元素到PriorityQueue中
if (c instanceof SortedSet<?>) {
SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
this.comparator = (Comparator<? super E>) ss.comparator();
initElementsFromCollection(ss);
}
可以看到 initElementFromCollection 只不过是将集合元素转为数组,然后赋值给优先队列底层的数组或成员变量 queue。当 a 数组未能返回一个 Object[] 类型时,则调用 Arrays.copyOf 方法将其转为一个正确的 Objectp[] 数组。
然后遍历元素进行判空,一切正常则将其赋值给 queue,并记录此时 queue 的长度。
private void initElementsFromCollection(Collection<? extends E> c) {
Object[] a = c.toArray();
// If c.toArray incorrectly doesn't return Object[], copy it.
if (a.getClass() != Object[].class)
a = Arrays.copyOf(a, a.length, Object[].class);
int len = a.length;
if (len == 1 || this.comparator != null)
for (int i = 0; i < len; i++)
if (a[i] == null)
throw new NullPointerException();
this.queue = a;
this.size = a.length;
}
我们再来看看 PriorityQueue 集合传入时的处理逻辑,同样的将 PriorityQueue 的比较器赋值给当前 PriorityQueue 之后,调用了一个 initFromPriorityQueue 方法,我们步入看看。
//如果集合类型是PriorityQueue则获取PriorityQueue的比较器,并将PriorityQueue的元素转移到我们的PriorityQueue中
else if (c instanceof PriorityQueue<?>) {
PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
this.comparator = (Comparator<? super E>) pq.comparator();
initFromPriorityQueue(pq);
}
可以看到 initFromPriorityQueue 操作就是让传入的 PriorityQueue 通过 toArray 返回底层的小顶堆数组,然后赋值给我们的 PriorityQueue,在记录一下当前 PriorityQueue 的长度。
private void initFromPriorityQueue(PriorityQueue<? extends E> c) {
if (c.getClass() == PriorityQueue.class) {
this.queue = c.toArray();
this.size = c.size();
} else {
//略
}
}
对于一般集合,我们的逻辑会走到这里,默认设置比较器为空,然后也调用了 initFromCollection,我们步入查看逻辑。
else {
this.comparator = null;
initFromCollection(c);
}
可以看到它的实现就是调用上文所介绍的 initElementsFromCollection 将数组存到 queue 中,然后调用一个 heapify 将这个数组转为小顶堆。
private void initFromCollection(Collection<? extends E> c) {
initElementsFromCollection(c);
heapify();
}
对于 JDK 实现的 heapify,可以发现获取父节点的操作使用了高效的右移运算,同样的遍历所有非叶子节点进行 siftDown 生成一个完整的小顶堆。
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
入队操作
对于 PriorityQueue 来说,核心的入队就是 offer,它的核心步骤为:
- 校验元素是否为空。
- 设置新插入的位置为 size。
- 判断数组容量是否足够,如果不够则扩容。
- 如果是第一个元素,则直接将其放到索引 0 位置。
- 如果不是第一个元素,则调用
siftUp将元素放入队列。
public boolean offer(E e) {
//校验元素是否为空
if (e == null)
throw new NullPointerException();
modCount++;
//设置新插入的位置为size
int i = size;
//判断数组容量是否足够,如果不够则扩容
if (i >= queue.length)
grow(i + 1);
size = i + 1;
//如果是第一个元素,则直接将其放到索引0位置
if (i == 0)
queue[0] = e;
else
//如果不是第一个元素,则调用siftUp将元素放入队列
siftUp(i, e);
return true;
}
在 siftUp 操作时会判断比较器是否为空,如果不为空则使用传入的比较器生成小顶堆,反之就将元素 x 转为 Comparable 对象进行比较。
因为整体比较逻辑都一样,所以我们就以 siftUpUsingComparator 查看一下进行 siftUp 操作时对于入队元素的处理逻辑。
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
siftUpUsingComparator 是不断向上比较父节点,找到比自己大的则交换位置,直到到达根节点或者比较父节点比自己小为止,整体来说 PriorityQueue 的 siftUp 分为以下几个步骤:
- 获取入队元素当前索引位置的父索引 parent。
- 根据父索引找到元素 e。
- 如果新节点 x 比 e 大,则说明当前入队操作符合小顶堆要求,直接结束循环。
- 如果 x 比 e 小,则将父节点 e 的值改为我们入队元素 x 的值。
- k 指向父索引,继续循环向上比较父索引,直到找到比 x 还小的父节点 e,终止循环。
- 将 x 存到符合要求的索引位置 k。
private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
//获取入队元素当前索引位置的父索引parent
int parent = (k - 1) >>> 1;
//根据父索引找到元素e
Object e = queue[parent];
// 如果新节点x比e大,则说明当前入队操作符合小顶堆要求,直接结束循环
if (comparator.compare(x, (E) e) >= 0)
break;
//如果x比e小,则将父节点e的值改为我们入队元素x的值
queue[k] = e;
//k指向父索引,继续循环向上比较父索引,直到找到比x还小的父节点e,终止循环
k = parent;
}
//将x存到符合要求的索引位置k
queue[k] = x;
}
出队操作
出队操作的逻辑步骤为:
- 判断队列是否为空。如果是,直接返回 null
- 如果不为空,首先将队列的 size 减 1,并将旧的末尾元素索引 s 保存下来。
- 将数组索引 0 位置的元素(即堆顶,也就是优先级最高的元素)暂存到
result变量中,作为最终的返回值。 - 将原队列的最后一个元素
queue[s](也就是 x)取出来。这个元素将作为新的“临时”堆顶,用于后续的堆调整。 - 将原队列末尾的位置设置为 null,有助于垃圾回收(GC),避免内存泄漏。
- 如果 s 不为 0,说明队列中不止一个元素,需要维持小顶堆的特性,需要从堆顶开始进行
siftDown操作。 - 返回优先队列优先级最高的元素 result。
public E poll() {
if (size == 0)
return null;
int s = --size;
modCount++;
E result = (E) queue[0];
E x = (E) queue[s];
queue[s] = null;
if (s != 0)
siftDown(0, x);
return result;
}
查看优先级最高的元素
peek 方法可以不改变队列结构查看优先级最高的元素,如果队列为空则返回 null,反之返回 0 索引位置的元素。
public E peek() {
return (size == 0) ? null : (E) queue[0];
}
更多推荐

所有评论(0)