PriorityQueue
version:1.8
今天看一种特殊的队列“优先级队列”
优先级队列名为队列,但不满足一般队列先进先出的特性,而是优先级最高(或最低)的先出,其底层使用堆实现。
堆是一种特殊的二叉树,其不同于二叉搜索树,是不完全有序的,其从根节点到叶子节点形成的每条路径都是有序的,但各条路径之间是不要求有序的,如此保证了根节点是全局的最大(或最小)的节点。同时,堆是完全二叉树。
根据根节点是最大还是最小,可称为大根堆或小根堆。
public class PriorityQueue<E> extends AbstractQueue<E>
由于其为完全二叉树,通过数组存储,如此可通过父节点与子节点的下标大小关系快速找到父节点(n,从0开始)或子节点(2n+1,2n+2),也不会造成数组存储大量的空叶子节点。
transient Object[] queue;
查看首节点
查看很简单:
@SuppressWarnings("unchecked")
public E peek() {
return (size == 0) ? null : (E) queue[0];
}
element 继承自AbstractQueue:
public E element() {
E x = peek();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
添加节点
private int size = 0;
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;
}
先看看扩容:
其实就是确定下新的容量大小,然后将老的数组拷贝到新数组中去即可(各元素位置不变):
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);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE : //实际生效
MAX_ARRAY_SIZE;
}
然后看下其如何调整树的结构:分成两种情况,使用Comparator或使用Comparable。
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
看一种即可:
private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (comparator.compare(x, (E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = x;
}
如图所示,首先将节点插入在最后,然后顺着路径一步步向上比较,直到插入的节点大于等于当前父节点,类似于冒泡排序,由于原路径是有序的,不需要每次比较都交换,只需将父节点下沉,直到最后一步再将插入节点赋值给当前父节点。
堆只要求每条路径有序即可,其调整相对红黑树简单许多。
其不存在容量限制问题(见Queue),add与offer相同。
public boolean add(E e) {
return offer(e);
}
移除头节点
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;
}
移除节点与添加节点相类似,移除尾节点,将尾节点的值赋予根节点。
接下来就将根节点下沉以调整树的结构:
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;
}
每个父节点至少涉及两条路径,因此需要选取父节点与其两个子节点中最小的节点作为新的父节点,一步步下沉根节点,直到根节点当前位置的两个子节点的值都大于其父节点的值或根节点到了叶子节点的位置。
remove()继承自AbstractQueue:
public E remove() {
E x = poll();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
移除指定节点
public boolean remove(Object o) {
int i = indexOf(o);
if (i == -1)
return false;
else {
removeAt(i);
return true;
}
}
首先找到节点下标:
private int indexOf(Object o) {
if (o != null) {
for (int i = 0; i < size; i++)
if (o.equals(queue[i]))
return i;
}
return -1;
}
然后移除并调整:
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;
}
这里与移除根节点不同的地方在于下沉后还需要判断是否需要上浮。
如图所示:要移除的节点可能与尾节点不再同一条路径上。
如此,若其下沉,代表其肯定比父节点大,但若未下沉,则不一定,此时还需要上浮。