背景
所谓TopK问题,即给定数据集,取最大或最小K条数据。通常来说都使用大根堆或小根堆解决,时间复杂度为nlog(k)。PriorityQueue 则是JDK提供的大根堆/小根堆实现
大根堆是棵二叉树,且根元素大于左右子树的所有元素,小根堆则相反(另外需要注意的是,*根堆和平衡二叉树的是两种算法)
PriorityQueue 中使用数组作为载体,基于数组使用二叉树的"视图"实现了log(k)的时间复杂度。
以下仅讨论大根堆
关键点
存储结构
PriorityQueue 以数组为存储结构,通过对数组下标的进行解释,创建了一个二叉树的视图,图示如下
可能切换成二进制会更好理解一些
操作-新增
核心代码在方法siftUp
中
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;
这里k是数组下标,而其中仅使用 (k - 1) >>> 1
即完成了对数组的二叉树视图遍历
整个流程为,新增元素到数组末尾(可能存在扩容),根据二叉树视图,向上查找,如果大于根节点,则和根节点调换位置,直到根节点大于新增的元素为止。
操作-取根
核心代码在方法siftDown
中
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;
这里x是末尾节点,k为0。half表示下标最小的叶子节点。从根节点开始,拿左右子树的值与末尾的节点比较。如果小于,则末尾节点晋升为当前子树的根。否则,取左右子树较小的树;再重复上述操作。