前言
从学习队列(Queue)的第一天起,我们就知道队列是满足FIFO(先进先出)的。但是所有的队列都是这样吗?
很遗憾,不是,今天所讲解的优先队列PriorityQueue就不满足FIFO。
优先队列
优先队列是计算机科学中的一类抽象数据类型。优先队列中的每个元素都有各自的优先级,优先级最高的元素最先得到服务;优先级相同的元素按照其在优先队列中的顺序得到服务。优先队列往往用堆来实现。
前一篇 图解数据结构:堆 已经较为详细的讲解了二叉堆这种数据结构,此处不再赘述。
实例
因为优先队列比较特殊,不满足FIFO,所以先来看下优先队列的用法:
public class PriorityQueueDemo {
public static void main(String[] args) {
PriorityQueue<Integer> queue = new PriorityQueue<>();
queue.offer(3);
queue.offer(4);
queue.offer(5);
queue.offer(1);
queue.offer(2);
while (!queue.isEmpty()) {
System.out.print(queue.poll() + " ");
}
}
}
执行结果
1 2 3 4 5
如果满足FIFO的话,输出应该是3 4 5 1 2
,但是实际输出却不是,并且可以看出输出的结果是有序的。
定义
优先队列在JDK中有完整的实现PriorityQueue
,先来看下类的定义:
public class PriorityQueue<E> extends AbstractQueue<E>
implements java.io.Serializable {
private static final long serialVersionUID = -7720805057305804111L;
// 默认容量
private static final int DEFAULT_INITIAL_CAPACITY = 11;
/**
* 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.
*
* 优先队列底层是用数组实现的二叉堆,根据comparator或者元素的自然顺序来排序
*/
transient Object[] queue; // non-private to simplify nested class access
/**
* The number of elements in the priority queue.
* 优先队列中元素个数
*/
private int size = 0;
// 比较器
private final Comparator<? super E> comparator;
// 用于fail-fast机制
transient int modCount = 0; // non-private to simplify nested class access
// 默认容量11
public PriorityQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
// 构造方法可以传入外部比较器
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
// Note: This restriction of at least one is not actually needed,
// but continues for 1.5 compatibility
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
}
核心方法
优先队列也是队列,所以重点关注的方法有三个:入队、出队、查看队首元素,在优先队列PriorityQueue
中对应的方法就是:offer(E e)
、poll()
、peek()
offer(E e)
offer(E e)
方法用于往队列尾部添加一个元素(入队),然后将元素上浮到合适的位置,其实现代码如下:
public boolean offer(E e) {
if (e == null)
// 不能放入空元素
throw new NullPointerException();
modCount++;
// 队列中元素个数
int i = size;
// 队列已满
if (i >= queue.length)
// 扩容
grow(i + 1);
// 队列元素+1
size = i + 1;
if (i == 0)
// 队列中没有元素,直接把元素方法第一位
queue[0] = e;
else
// 插入元素
siftUp(i, e);
return true;
}
先来看下数组扩容相关的方法grow
,其实现如下:
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// Double size if small; else grow by 50%
// 如果oldCapacity(旧容量)小于64,则扩容后新容量变成 2 * (oldCapacity + 1)
// 否则新容量变成 oldCapacity + oldCapacity / 2
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);
}
因为PriorityQueue
底层也是用数组实现的,所以扩容过程和ArrayList
差不多。
接下来查看把元素添加到堆中的方法siftUp
,其实现如下:
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接口的类
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)
// 子节点元素大于等于父节点元素,跳出循环
// 可以看出PriorityQueue实现的是最小堆
break;
// 把父节点元素的值赋给当前子节点(相当于父元素下沉)
queue[k] = e;
// 当前节点
k = parent;
}
// 此时k为新增元素应该在的下标
queue[k] = key;
}
这个方法执行的图解过程在上篇讲解堆的时候,已经详细画出过。
poll()
poll()
用于出队操作,用于取出PriorityQueue
中最小的元素,因为PriorityQueue
默认实现的是最小堆。方法实现如下:
public E poll() {
if (size == 0)
// 队列为空,直接返回null
return null;
// 元素个数-1
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
,其实现方式如下:
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
同样,只需要看其中一个即可,siftDownComparable
实现如下:
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)
// 如果右孩子存在,并且左孩子的值大于右孩子,则记录右孩子为child节点
// 因为是最小堆,所以要找到最两个孩子中比较小的那个
c = queue[child = right];
if (key.compareTo((E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = key;
}
同样,该方法的图解执行过程在讲解堆的时候已经详细给出。
peek()
peek()
用于查看下一个出队的元素,也就是堆顶元素(数组中第一个元素),所以该方法实现比较简单
public E peek() {
return (size == 0) ? null : (E) queue[0];
}
总结
JDK中的优先队列PriorityQueue
实现的是最小堆,需要注意的是。比较元素有两种方式:外部排序器、元素的自然顺序,对于这两种方式,相信都不陌生。当我们没有传入外部排序器,且队列中元素没有自然顺序时,会抛出异常。
除此之外,PriorityQueue
并没有提供任何操作来保证线程安全,所以它是线程不安全的。