PriorityQueue
从名字上来看,PriorityQueue叫做优先队列,它的另一个名字叫堆(heap),这个名字更熟悉。它和普通的队列不太一样,它里面存储的元素具有一定的顺序性,而不是简单的“先进先出”。它的实现原理通常是二叉堆,二叉堆是一个完全填满的二叉树。这里不做过多的介绍,都是数据结构的知识。来介绍一下优先队列的通常实现。
优先队列的整体结构虽然是一个二叉堆,也是树结构,但是存储元素的却是用数组实现的,不是通常用链表作为树的节点。因为这里隐含着一个规律,用数组的索引可以很轻松的获取它的父节点和儿子节点的索引。举个例子。
这就是一个优先队列,大优先。整体上是一个满二叉树,下面的图表示在数组中的排列。
假设优先队列的第一个元素的索引就是数组中索引为0的元素。其中优先队列的一个节点对应数组的索引是parent,左儿子节点索引是left,右儿子节点是索引是right,则存在下面的关系:
- left=2parent+1,right=2parent+2
- parent=left/2,parent=(right-1)/2
特性
完整二叉树: 每一层都是二叉树 都被填满 ,除了最低/最下面的一层,并且所有的最底层顶点都尽力向左靠拢。
最大二叉堆特性: 每个顶点的父元素 - 除了根元素 - 都比当前元素的值要大。这种定义比下面这种更容易验证:一个顶点的值 - 除了叶顶点 - 必须必它的一个或者两个子元素要大。
举例
@Test
public void test() {
PriorityQueue<Integer> queue = new PriorityQueue<>();
queue.add(9);
queue.add(1);
queue.add(3);
queue.add(6);
queue.add(0);
System.out.println(Arrays.toString(queue.toArray()));
}
测试结果:[0, 1, 3, 9, 6]
树结构:
简单介绍后,我们直接看看jdk的实现代码。
源码
private static final int DEFAULT_INITIAL_CAPACITY = 11;
transient Object[] queue;
private final Comparator<? super E> comparator;
内部定义了一个数组,数组得话就涉及到扩容,默认得容量是11。因为优先队列的元素是需要排序得,不然怎么知道哪个元素优先呢?排序就涉及到两个接口Comparable和Comparator,这在需要排序的集合类中是通用的套路,如果元素的类型实现了Comparable接口,则使用Comparable排序,否则通过定制Comparator实例排序。
还是先来看add方法。
public boolean add(E e) {
return offer(e);
}
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;
}
add内部调用的offer方法,因为PriorityQueueu也是无界的队列,索引add和offer几乎没有区别。内部实际上是数组存储元素,随着元素的增多肯定涉及到扩容。
当元素个数大于等于队列长度时,进行扩容。扩容的方法是grow。
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// Double size if small; else grow by 50%
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);
}
方法传递过来的满足需求的最小容量,是在原来的长度上加一。这个方法判断了一下,如果当前的队列长度小于64,将长度变为两倍的原有长度再加2,如果长度大于等于64,1.5被扩容。
继续看offer的下面代码。
如果是第一次插入元素,不管元素时什么直接写到数组的索引0位置。否则,执行siftUp方法。siftUp方法有个专业名词,叫“上滤”,这里判断了一下采用哪种接口的排序,实际的逻辑是一致的,我们来看 siftUpComparable(k, x);
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
private void siftUpComparable(int k, E x) {bu
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) //比较大小,如果插入的元素比父节点大,就不需要再不比较了,新元素直接作为父节点的子节点就行了。
//否则,说明新元素需要往上移动,用新元素替换父节点,循环比较,直到找到比父节点大的节点终止或者已经到了
//根节点,就是while的循环条件不满足
break;
queue[k] = e;//将父节点替换为新元素
k = parent; //继续向上比较
}
queue[k] = key; //比较结束后需要替换节点
}
我们以上面的例子来说明,类型是PriorityQueue,PriorityQueue没有配置Comparator比较接口,默认是小优先,就说父元素要大于子元素,根元素最小。
像上面的例子一样,我们假设已经插入了几个节点0, 1, 3, 9, 6,树结构如下图,现在插入节点2,我们跟踪一下插入过程。
再来看remove和poll方法。
PriorityQueue中并没有实现remove方法,而是继承了抽象类AbstractQueue的remove方法,AbstractQueue的remove方法如下。
public E remove() {
E x = poll();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
内部调用的是poll方法,我们关注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); //移除数组中第0个元素,也是树的根
return result;
}
移除的时候并不会将数组缩容,只是将队列的size减1。如果只有一个元素的话,直接返回,否则执行“下滤”操作。
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, 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; //根节点左儿子索引值
Object c = queue[child]; //根节点左儿子值
int right = child + 1; //根节点右儿子索引值
if (right < size &&
((Comparable<? super E>) c).compareTo((E) queue[right]) > 0) //如果存在右儿子的话,比较左儿子和右儿子,如果右儿子小,获取右儿子 //的值,这里意思是,获取儿子中较小的节点值,保存在变量c
c = queue[child = right];
if (key.compareTo((E) c) <= 0) //如果最后一个节点的值比c小,直接退出循环,执行queue[k] = key;用最后一个元素替换原来的根节点,结束
break;
queue[k] = c; //否则用c的值替换根节点的值,但是此时需要c节点为跟节点,循环执行上面的逻辑直到直到退出循环
k = child;
}
queue[k] = key;
}
这是一个通用的方法,可以移除任意树的节点,在优先队列中,移除任意节点不常用,因为值确定根节点最大或者最小,并不知道第二大,第三大的节点在数组中的哪个位置。我们分析移除的根节点。方法传入的是0,最后一个元素的值。看一下这个过程,假设当前的树结构如下。
我们移除最小节点0
解析来看element和peak方法。
public E element() {
E x = peek();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
public E peek() {
return (size == 0) ? null : (E) queue[0];
}
这两个方法没什么可说的,直接获取数组中第0个元素。
最后需要看一下的是有参构造器。
public PriorityQueue(Collection<? extends E> c) {
if (c instanceof SortedSet<?>) {
SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
this.comparator = (Comparator<? super E>) ss.comparator();
initElementsFromCollection(ss);
}
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);
}
}
这个构造器传入的是个集合,当然集合肯定分为有序的无序各种,这里按类别分别处理,但最终都要调用initFromCollection方法处理
private void initFromCollection(Collection<? extends E> c) {
initElementsFromCollection(c); //从Cellection初始化元素
heapify(); //堆化
}
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
(size >>> 1) - 1表示的是最后一个节点的父节点,然后该遍历执行“下滤”操作,也就是如果父节和儿子节点比较,如果比儿子小,将儿子节点置为父节点,这样遍历下来,构建出了完整的堆。
总结
PriorityQueue通过以二叉堆的结构实现了优先队列,底层数据结构是数组,可以保证按照一定顺序出队,线程不安全。