一、概述
顾名思义,PriorityQueue 是一个队列,队列的特点是先进先出,后进后出,和现实生活中的排队场景非常的类似。而优先级队列是一个比较特殊的队列,它的入队普通的队列没有区别,而出队操作不是先来后到了,而是有优先级的,优先级高的先出队。
下面的代码描述了 Java 中的 PriorityQueue 的基本使用方法:
Queue<Integer> queue = new PriorityQueue<>();
queue.add(1); //入队
queue.add(3);
queue.add(5);
queue.add(2);
queue.poll(); //结果是 1
queue.poll(); //结果是 2
可以看到,每次出队的时候,都是队列中的最小元素,当然你也可以反过来,每次都出最大的元素,只需要在声明 PriorityQueue 的时候加上相应的 Comparator 即可,就像这样:
Queue<Integer> queue = new PriorityQueue<>(Collections.reverseOrder());
优先级队列中最重要的两个操作是入队和出队,下面就从源码的角度来简要分析一下。
二、入队
PriorityQueue 的 add 和 offer 方法都是入队,主要的代码是这样的:
/**
* Inserts the specified element into this priority queue.
*/
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1);
size = i + 1;
if (i == 0)
queue[0] = e;
else
siftUp(i, e);
return true;
}
入队之前有一些条件判断,如果为空则抛出异常,如果容量不够,则调用 grow 方法进行扩容,如果是第一个元素,则直接加入到数组的第一个位置,否则调用 siftUp 方法进行堆化。
哦对了,它的底层使用的是一个数组来存储数据,看代码中的声明就知道了:
/**
* Priority queue represented as a balanced binary heap: the two
* children of queue[n] are queue[2*n+1] and queue[2*(n+1)]. The
* priority queue is ordered by comparator, or by the elements'
* natural ordering, if comparator is null: For each node n in the
* heap and each descendant d of n, n <= d. The element with the
* lowest value is in queue[0], assuming the queue is nonempty.
*/
transient Object[] queue; // non-private to simplify nested class access
siftUp 方法里面有一个 if else,主要是对比较器 comparator 进行判断然后走不同的方法,这两个方法的底层的实现,其实没有太大的区别,随便选择一个看就行。入队的主要逻辑就在这里面了。
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
siftUpComparable 方法也很简短,如下:
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}
参数 k 表示新插入元素的位置,即数组的下标,x 是新元素,这里强转之后成为了 key。然后进入 while 循环,找到 k 的父级下标 parent,公式是 (k - 1) >>> 1,这里向右移一位,相当于除以 2。
要理解这个公式,首先需要知道队列中的元素是怎么存储的,参考下图:
对于数组中的任意一个下标为 i 的元素,其左子节点是 2 * i + 1,右子节点是 2 * i + 2,因此找到父节点就是上面那个公式了。
新来的元素先放到数组最后,由于堆顶元素(数组中的第一个元素)始终是队列中的最大(或者最小)值,因此需要将新的元素逐级与其上级的元素进行比较,直到新元素大于(或者小于)其父级元素,然后跳出循环,将新元素放到正确的位置上。
三、出队
理解了入队,再来分析下出队操作,出队 poll 方法如下:
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;
}
可以看到取了数组中的第一个元素做为返回值,然后再调用 siftDown 方法进行堆化,其中参数 x 是数组中的最后一个元素,堆化的主要逻辑如下:
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>)x;
int half = size >>> 1; // loop while a non-leaf
while (k < half) {
int child = (k << 1) + 1; // assume left child is least
Object c = queue[child];
int right = child + 1;
if (right < size &&
((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
c = queue[child = right];
if (key.compareTo((E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = key;
}
从 k 开始,比较其左右子节点的值,并将较小的元素放到 k 处,然后 k 更新为较小子节点的下标,继续向下堆化。
可以参看下面的动图来理解: