基本原理
PriorityQueue(优先级队列)的数据结构是最小堆,采用数组作为底层数据结构。
不同于普通的遵循FIFO规则的队列,PriorityQueue每次都选出优先级最高的元素出队,优先队列里实际是维护最小堆,通过最小堆使得每次取出的元素总是优先级最高的。
/**
* 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.
*/
transient Object[] queue; // non-private to simplify nested class access
底层采用Object数组作为最小堆的实现方式
节点queue[n]的左孩子节点为queue[2*n+1] ,右孩子节点为queue[2*(n+1)]。
queue[0]表示优先级最高的节点
添加数据offer
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;
}
首先入口参数检查,PriorityQueue不支持存储null,所以如果给PriorityQueue添加null,抛出空指针异常,在错误发生后尽快检测出错误,符合Effective Java第38条原则《检查参数的有效性》。
接着修改modCount,方便在迭代的过程中发生结构性修改可以抛出ConcurrentModificationException异常
因为是添加数据,所以需要判断是不是需要扩容
if (i >= queue.length)
grow(i + 1);
如果原队列为空,就把新添加的数据添加到队首
if (i == 0)
queue[0] = e;
否则就调用siftUp进行向上筛选
else
siftUp(i, e);
筛选分两种情况,因为有两种排序规则,按照数据自身的排序规则调用siftUpComparable或者按照外部规定的排序规则调用siftUpUsingComparator
siftUp(int k, E x)的含义为:在堆的数组下标k处,放置了一个数据x,此操作有可能破坏堆的性质,因此对堆进行调整
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
siftUpComparable和siftUpUsingComparator代码逻辑类似,只是比较规则不同,因此只取其一进行分析
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;
}
首先将需要插入的数据x强制转换为可比较的对象Comparable并保存为key
然后开始向上筛选,筛选终止的条件有两个,第一个是待插入的数据比堆顶元素都小,因此会筛选到下标k==0,此时结束循环;第二个是在向上筛选的过程中找到一个父节点比待插入节点小,此时不需要继续向上筛选了,break退出迭代。
迭代的过程中,首先获取父节点在数组中的下标位置并将父节点的数据保存为e
int parent = (k - 1) >>> 1;
Object e = queue[parent];
然后判断如果待插入节点比父节点大,break退出循环,待插入节点找到了自己的位置
if (key.compareTo((E) e) >= 0)
break;
否则的话,将父节点的数据(数组下标(k - 1) >>> 1)保存到正在筛选的节点(数组下标k),正在筛选的节点的下标k设为父节点下标(k - 1) >>> 1,进行下一次迭代
queue[k] = e;
k = parent;
迭代结束后,将待插入数据保存到在堆中合适的位置
queue[k] = key;
获取并删除队首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);
return result;
}
首先检查队列容量,size==0表示队列此时没有数据,从没有数据的队列中读取数据直接返回null
然后对size和modCount进行修改,保存数组最后一个元素下标为s=size-1
int s = --size;
modCount++;
最小堆的性质保证优先级队列的队首元素queue[0]一定是最小的(优先级最高的),因此将队首元素保存为result
E result = (E) queue[0];
队首元素出队列后,将队尾元素保存到队首,队尾元素置null加速垃圾回收,这个操作有可能破坏了堆的性质,因此需要向下筛选
E x = (E) queue[s];
queue[s] = null;
if (s != 0)
siftDown(0, x);
siftDown(int k, E x)的含义为:将优先级队列数组下标为k处的数据设置为x,此操作有可能破坏了堆的性质,因此需要调整。
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
和向上筛选一样,分两种排序规则进行分类处理,但是两种情况代码大同小异,只是比较规则不一样,选其一进行分析
private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}
首先找到叶子节点的最小下标,也就是第一个叶子节点的下标,并保存为half。
int half = size >>> 1;
最小堆也是一个完全二叉树,完全二叉树的性质决定了size的一半减一(size/2-1)是非叶子节点的最大下标,size的一半(size/2)是叶子节点的最小下标
然后向下筛选,要么找到了比筛选到了叶子节点,要么找到了数据x在堆中的合适位置,这两种情况都停止筛选
筛选的过程中,需要将待筛选节点和两个孩子节点进行比较,如果待筛选节点比两个孩子都小,则向下筛选结束,否则交换较小孩子和待筛选节点的位置,进行下一轮筛选。
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
然后左右孩子比较,设置左右孩子的较小者为c,然后用c和待筛选节点x进行比较
if (comparator.compare(x, (E) c) <= 0)
break;
如果待筛选节点x比左右孩子的较小者c都小,说明待筛选节点x找到了自己的合适位置,停止筛选,break退出循环
否则进行下一轮筛选
queue[k] = c;
k = child;
将左右孩子较小者c保存到父节点queue[k] ,设置下一轮待筛选节点下标k为左右孩子较小者下标,进行新的一轮筛选
筛选过程结束,将数据x放置到自己在最小堆中的最终位置
queue[k] = x;
建堆过程
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
建堆的过程只从非叶子节点开始筛选,因为筛选的过程中会处理到叶子节点。并且筛选的过程是倒着来的,从最后一个非叶子节点开始,一直处理到数组中第一个节点,这种自底向上的思想类似于动态规划。