PriorityQueue剖析
优先队列本质上就是一个最小堆。所以先讲讲堆的性质:
堆
堆(也叫优先队列),是一棵完全二叉树,它的特点是父节点的值大于(小于)两个子节点的值(分别为大顶堆和小顶堆)。需要注意的是堆中任一子树也是堆。下图中给出了从二叉树角度来看的大顶堆。
如果从数组角度来看,那么该大顶堆如下:
按照编号来看,可以发现一个很有意思的规律:
左子结点的编号=父结点编号 * 2
右子结点的编号=父结点编号 * 2 + 1
考虑到数组从0开始,所以用java代码表示为:
public static int left(int i) {
return i * 2 + 1;
}
public static int right(int i) {
return i * 2 + 2;
}
调整堆
假定我们需要建立一个大顶堆,那么对于这么一个最大堆来说,父节点的值必须要比它的子结点要大。如果不满足这个要求,就需要调整。如下图所示:
这时需要调整结点4的位置,然后与子结点比较,和最大的子结点交换位置。
经交换后发现交换后的结点符合要求了,可以结点4依然不符合要求,所以再次进行交换。
再次交换后最终符合了大顶堆的要求,主要过程如下:
- 比较当前结点和它的子结点,如果当前结点小于它的任何一个子结点,则和最大的那个子结点交换。否则,当前过程结束。
- 在交换到新位置的结点重复步骤1,直到叶结点。
建最大堆
建最大堆之前需要考虑的几个因素:1、是从前往后开始调整还是从后往前开始调整?2、从哪里结点开始调整。
我们先来讨论第一个因数:如果是从前往后开始调整,如图所示:
如果我们从根结点开始,根结点元素4比它的两个子结点都大,不需要调整。而再往后面的时候子结点1调整之后被换成16。这样就出现了它的子结点比它还大的情况,因此从前往后调整的过程不可行。
而采用从后往前进行调整,能够保证当上面的父结点调整的时候,下面的子数已经满足最大堆的条件了。
第二个因素,从哪个结点开始调整:
如果直接从尾结点进行调整,毫无疑问是有问题存在的,因为可能有相当一部分的结点是没有必要的叶节点,这些结点根本没有子结点,所以是没有必要的。那么其实可以追溯到最后一个结点的父结点进行调整,这个元素的位置索引即是尾结点/2。
理解了构建堆的过程我们来看看PriorityQueue。
PriorityQueue
存储结构-字段
PriorityQueue中主要有三个字段
默认大小为11,有一个queue数组用来存储数据,size表示当前结点个数。注意的是queue也是被transient修饰的,这一点已经在《ArrayList和LinkedList剖析》里面谈过了,这里不再赘述。
功能实现-方法
构造方法
PriorityQueue有很多的构造方法,默认为自行构造一个大小为11的Object数组。也可以传入一个Collection的实现类。在传入Collection的实现类时,被先都调用heapify方法。
建堆
建堆就是调用的heapify方法,源码如下:
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
就是和上面的建堆过程一样,我们来看看shiftDown方法
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
@SuppressWarnings("unchecked")
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;
}
如果有comparator那么就调用siftDownUsingComparator方法,否则调用siftDownComparable方法。其实就是一个调整堆的过程。这里使用的是循环的版本没有使用递归,代码不难,只需要理解是从最后一个结点的父结点开始从顶向下调整就可以了。
扩容
堆里面的数组长度不是固定不变的,如果不断往里面添加新元素的时候,也会面临数组空间不够的情形,所以也需要对数组长度进行扩展。数组长度扩展的方法如下:
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);
}
和ArrayList中不同的是,这里当容量较小的时候(<64),每次只扩大2。
新增
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(i+1),这样保证了PriorityQueue永远不会溢出。另外,当插入一个新的数据时,因为是最小堆,使用的就是从底向上来进行调整。siftUp代码如下:
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
@SuppressWarnings("unchecked")
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;
}
删除
private E removeAt(int i) {
// assert i >= 0 && i < size;
modCount++;
int s = --size;
if (s == i) // removed last element
queue[i] = null;
else {
E moved = (E) queue[s];
queue[s] = null;
siftDown(i, moved);
if (queue[i] == moved) {
siftUp(i, moved);
if (queue[i] != moved)
return moved;
}
}
return null;
}
需要注意的是要调用siftDown后还需判断一次,如果不成功那么就调用siftUp
总结
其实只需要知道PriorityQueue是小顶堆,然后知道堆调整和建堆时候的从哪里开始。堆的概念熟悉后,阅读源码就没什么难度了。