11.2 剖析PriorityQueue
11.2 剖析PriorityQueue
本节探讨堆在Java中的具体实现类:PriorityQueue。顾名思义,PriorityQueue是优先级队列,它首先实现了队列接口(Queue),与LinkedList类似,它的队列长度也没有限制,与一般队列的区别是,它有优先级的概念,每个元素都有优先级,队头的元素永远都是优先级最高的。
PriorityQueue内部是用堆实现的,内部元素不是完全有序的,不过,逐个出队会得到有序的输出。虽然名字叫优先级队列,但也可以将PriorityQueue看作一种比较通用的实现了堆的性质的数据结构,可以用PriorityQueue来解决适合用堆解决的问题,下一小节我们会来看一些具体的例子。下面,我们先介绍其用法,接着分析实现代码,最后总结分析其特点。
11.2.1 基本用法
PriorityQueue实现了Queue接口,我们在LinkedList一节介绍过Queue,为便于阅读,这里重复下其定义:
1 | public interface Queue<E> extends Collection<E> { |
PriorityQueue有多个构造方法,部分构造方法如下所示:
1 | public PriorityQueue() |
PriorityQueue是用堆实现的,堆物理上就是数组,与ArrayList类似,PriorityQueue同样使用动态数组,根据元素个数动态扩展,initialCapacity表示初始的数组大小,可以通过参数传入。对于默认构造方法,initialCapacity使用默认值11。对于最后的构造方法,数组大小等于参数容器中的元素个数。与TreeMap/TreeSet类似,为了保持一定顺序, PriorityQueue要求要么元素实现Comparable接口,要么传递一个比较器Comparator。
我们来看个基本的例子:
1 | Queue<Integer> pq = new PriorityQueue<>(); |
代码很简单,添加元素,然后逐个从头部删除,与普通队列不同,输出是从小到大有序的:
1 | 2 4 6 7 8 10 11 12 12 13 15 19 22 34 |
如果希望是从大到小呢?传递一个逆序的Comparator,将第一行代码替换为:
1 | Queue<Integer> pq = new PriorityQueue<>(11, Collections.reverseOrder()); |
输出就会变为:
1 | 34 22 19 15 13 12 12 11 10 8 7 6 4 2 |
我们再来看个例子。模拟一个任务队列,定义一个内部类Task表示任务,如下所示:
1 | static class Task { |
Task有两个实例变量:priority表示优先级,值越大优先级越高;name表示任务名称。Task没有实现Comparable,我们定义一个单独的静态成员taskComparator表示比较器,如下所示:
1 | private static Comparator<Task> taskComparator = new Comparator<Task>() { |
下面来看任务队列的示例代码:
1 | Queue<Task> tasks = new PriorityQueue<Task>(11, taskComparator); |
代码很简单,就不解释了,输出任务按优先级排列:
1 | 处理任务: 写代码,优先级:100 |
11.2.2 实现原理
理解了PriorityQueue的用法和特点,我们来看其具体实现代码(基于Java
7),从内部组成开始。内部有如下成员:
1 | private transient Object[] queue; |
queue就是实际存储元素的数组。size表示当前元素个数。comparator为比较器,可以为null。modCount记录修改次数,在介绍第一个容器类ArrayList时已介绍过。
如何实现各种操作,且保持堆的性质呢?我们来看代码,从基本构造方法开始。
几个基本构造方法的代码是:
1 | public PriorityQueue() { |
代码很简单,就是初始化了queue和comparator。下面介绍一些操作的代码,大部分的算法和图示我们在11.1节已经介绍过了。
添加元素(入队)的代码如下所示,我们添加了一些注释:
1 | public boolean offer(E e) { |
有两步复杂一些,一步是grow,另一步是siftUp,我们来细看下。grow()方法的代码为:
1 | private void grow(int minCapacity) { |
如果原长度比较小,大概就是扩展为两倍,否则就是增加50%,使用Arrays.copyOf方法复制数组。siftUp的基本思路我们在11.1节介绍过了,其实际代码为:
1 | private void siftUp(int k, E x) { |
根据是否有comparator分为了两种情况,代码类似,我们只看一种:
1 | private void siftUpUsingComparator(int k, E x) { |
参数k表示插入位置,x表示新元素。k初始等于数组大小,即在最后一个位置插入。代码的主要部分是:往上寻找x真正应该插入的位置,这个位置用k表示。
怎么找呢?新元素(x)不断与父节点(e)比较,如果新元素(x)大于等于父节点(e),则已满足堆的性质,退出循环,k就是新元素最终的位置,否则,将父节点往下移(queue [k]=e),继续向上寻找。这与11.1节介绍的算法和图示是对应的。
查看头部元素的代码为:
1 | public E peek() { |
就是返回第一个元素。
删除头部元素(出队)的代码为:
1 | public E poll() { |
返回结果result为第一个元素,x指向最后一个元素,将最后位置设置为null(queue[s] =null),最后调用siftDown将原来的最后元素x插入头部并调整堆,siftDown的代码为:
1 | private void siftDown(int k, E x) { |
同样分为两种情况,代码类似,我们只看一种:
1 | private void siftDownComparable(int k, E x) { |
k表示最终的插入位置,初始为0, x表示原来的最后元素。代码的主要部分是:向下寻找x真正应该插入的位置,这个位置用k表示。
怎么找呢?新元素key不断与较小的孩子节点比较,如果小于等于较小的孩子节点,则已满足堆的性质,退出循环,k就是最终位置,否则将较小的孩子节点往上移,继续向下寻找。这与11.1节介绍的算法和图示也是对应的。
解释下其中的一些代码:
1)k<half
表示编号为k的节点有孩子节点,没有孩子节点,就不需要继续找了;
2)child表示较小的孩子节点编号,初始为左孩子,如果有右孩子(编号right)且小于左孩子则child会变为right;
3)c表示较小的孩子节点。
根据值删除元素的代码为:
1 | public boolean remove(Object o) { |
先查找元素的位置i,然后调用removeAt进行删除,removeAt的代码为:
1 | private E removeAt(int i) { |
如果是删除最后一个位置,直接删即可,否则移动最后一个元素到位置i并进行堆调整,调整有两种情况,如果大于孩子节点,则向下调整,否则如果小于父节点则向上调整。代码先向下调整(siftDown(i, moved)),如果没有调整过(queue[i] ==moved),可能需向上调整,调用siftUp(i, moved)。如果向上调整过,返回值为moved,其他情况返回null,这个主要用于正确实现PriorityQueue迭代器的删除方法,迭代器的细节我们就不介绍了。
如果从一个既不是PriorityQueue也不是SortedSet的容器构造堆,代码为:
1 | private void initFromCollection(Collection<? extends E> c) { |
initElementsFromCollection的主要代码为:
1 | private void initElementsFromCollection(Collection<? extends E> c) { |
主要是初始化queue和size。heapify的代码为:
1 | private void heapify() { |
与之前算法一样,heapify也在11.1节介绍过了,就是从最后一个非叶子节点开始,自底向上合并构建堆。如果构造方法中的参数是PriorityQueue或SortedSet,则它们的toArray方法返回的数组就是有序的,就满足堆的性质,就不需要执行heapify了。
11.2.3 小结
本节介绍了Java中堆的实现类PriorityQueue,它实现了队列接口Queue,但按优先级出队,内部是用堆实现的,有如下特点:
1)实现了优先级队列,最先出队的总是优先级最高的,即排序中的第一个。
2)优先级可以有相同的,内部元素不是完全有序的,如果遍历输出,除了第一个,其他没有特定顺序。
3)查看头部元素的效率很高,为O(1),入队、出队效率比较高,为O(log2(N)),构建堆heapify的效率为O(N)。
4)根据值查找和删除元素的效率比较低,为O(N)。
除了用作基本的优先级队列,PriorityQueue还可以作为一种比较通用的数据结构,用于解决一些其他问题,让我们在下一节继续探讨。