上一篇讲完PriorityQueue的添加元素是以平衡二叉堆的元素“上浮”来实现。
这一篇分析下PriorityQueue的删除操作poll
:
public E poll() {
final Object[] es;
final E result;
// 取出数组第一个元素,即是根节点
if ((result = (E) ((es = queue)[0])) != null) {
// 操作次数加1
modCount++;
final int n;
// 取出最后一个元素x,数组长度减一
final E x = (E) es[(n = --size)];
// 将数组最后一个位置置空
es[n] = null;
// 如果数组还有元素
if (n > 0) {
// 删除根节点,通过“下浮”调整堆,使其有序
final Comparator<? super E> cmp;
if ((cmp = comparator) == null)
siftDownComparable(0, x, es, n);
else
siftDownUsingComparator(0, x, es, n, cmp);
}
}
// 返回根节点元素
return result;
}
跟offer
的“上浮”差不多,poll
弄出来一个“下沉”。
/**
* 堆的"下沉"调整.
* 删除数组在位置 k 对应的元素,并重新调整堆使其有序
*
* @param k 待删除的位置,也可以理解为空位
* @param x 待插入的元素
* @param es 堆数组
* @param n 堆的大小
*/
private static <T> void siftDownComparable(int k, T x, Object[] es, int n) {
// assert n > 0;
Comparable<? super T> key = (Comparable<? super T>)x;
// 相当于n除2, 即找到索引n对应结点的父结点位置
int half = n >>> 1; // loop while a non-leaf
// 待删除位置小于父节点,才需要循环下沉调整
// 否则待删除位置可以直接覆盖
while (k < half) {
/**
* 下述代码中:
* c保存k的左右子结点中的较小结点值
* child保存较小结点对应的数组下标
*/
int child = (k << 1) + 1; // 找出k的左节点对应的数组下标
Object c = es[child];
int right = child + 1;
if (right < n &&
((Comparable<? super T>) c).compareTo((T) es[right]) > 0)
// 右节点在堆大小内,并且左节点比右节点大
// 那么c保存左节点(即是较小的节点)
c = es[child = right];
// 将待插入元素与k节点的较小子节点比较
if (key.compareTo((T) c) <= 0)
// 如果待插入元素更小,证明找到空位k,直接退出循环
break;
// 如果待插入元素更大,那么较小子节点上去补位
es[k] = c;
// 空位k下沉
k = child;
}
// 找到空位k,插入元素
es[k] = key;
}
上述代码其实是经典的堆“下沉”操作,对堆中某个顶点下沉,步骤如下:
- 找到该顶点的左右子结点中较小的那个;
- 与当前结点交换;
- 重复前2步直到当前结点没有左右子结点或比左右子结点都小。
空位置的下沉操作,类似我们抢前排座位。当第一排有人走了,坐第二排的小个子一点的同学就赶紧坐上去,空出了第二排他原来坐的位子。然后后面小个子也跟着坐上来。一直到新进来的同学也有合适的座位为止。
因为“下沉”也是折半法操作,所以时间复杂度也是O(logN)。
好,我们来总结下PriorityQueue的poll
操作:
- 空位的下沉的过程,可以想象学生抢前排座位,比较矮那位先坐上去。
- 下沉算法的关键是找出较小的子节点,再与待插入的元素比较优先级,来决定空位让给谁。
- 时间复杂度是Ο(logN)