一.数据结构
Priority queue 是一个 平衡二项堆(平衡二叉树);树中所有的子节点必须大于等于父节点,而无需维护大小关系,是一个最小堆
- 父节点与子节点的索引关系:
① 假设父节点为queue[n],那么左孩子节点为queue[2n+1],右孩子节点为queue[2(n+1)]
② 假设孩子节点(无论是左孩子节点还是右孩子节点)为queue[n],n>0。那么父节点为queue[(n-1) >>> 1]
- 叶子节点和非叶子节点:
① 一个长度为size的优先级队列,当index >= size >>> 1时,该节点为叶子节点。否则,为非叶子节点
二.常用操作
2.1 插入节点
add 和offer 都是添加节点, add 就是调用offer 接口 来实现的。插入的时间复杂度为O(log(n))
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;
}
代码中 有两个需要注意的点:
1. 扩容
如果oldCapacity 比较小,小于 64, 则double,申请两倍的; 否则提升50%.
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));
申请新的容量之后需要 对newCapacity 进行 处理,看是否 上下溢出。
Jvm 数组 最大的容量限制:
/**
* The maximum size of array to allocate.
* Some VMs reserve some header words in an array.
* Attempts to allocate larger arrays may result in
* OutOfMemoryError: Requested array size exceeds VM limit
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
Integer 的上限:
/**
* A constant holding the maximum value an {@code int} can
* have, 2<sup>31</sup>-1.
*/
@Native public static final int MAX_VALUE = 0x7fffffff;
调整后的容量 :
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
3. siftUp 最小堆化
插入节点的时候,要保证最小堆的结构,要求仍能满足第一部分中的最小堆的特点,需要堆化:
通过“int parent = (k - 1) >>> 1;”获取到当前要插入节点位置的父节点,比较父节点和待插入节点,如果待插入节点小于父节点,则将父节点插入到子节点的位置,然后在获取父节点的父节点循环上面的操作,直到待插入节点大于等于父节点,则在相应位置插入这个节点。最终保证代表优先级队列的平衡二叉树中,所有的子节点都大于它们的父节点,但同一层的子节点间并不需要维护大小关系。
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;
}
2.2 删除元素
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;
}
删除元素分三种情况:
- 删除的为队尾元素, 直接 queue[i] = null;
- 删除的为 叶子节点元素:
上面代码中 先执行siftDown, siftDown 只对 非叶子节点 有效:
private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1; // 在第一节中说过,k < half 的可以 认为是 非叶子节点
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;
}
所以此处只会执行 queue[k] = x, 将 队尾节点 放在 K 位置,然后 执行siftUp, 即 前面讲的siftUp 最小化堆的过程, 将该节点与其父节点进行比较, 只要其比父节点大,就往上移
- 删除的为 非叶子节点元素, 时间复杂度为O(n)+2O(log(n)):
有两部需要处理: siftDown 和 siftUp。
siftDown: 先将 队尾节点放到 要删除的节点处,然后 比较该节点与其孩子节点的大小,如果他比孩子节点大,则下沉, 一直循环下去,找到最终该节点的存放位置;
siftUp: 再将经过 siftDown 处理后的该节点进行提升,将其与父节点进行比较,如果比父节点小,则进行提升。
删除示例:
三.应用
求TopK 问题:如何在一个包含n个数据的 数组中 查找前K 个最大的?
我们可以维护一个大小为 K 的小顶堆,顺序遍历数组,从数组中取出的元素与堆顶元素 比较,如果比堆顶元素大,我们就插入堆中,依次循环,直到最后结束,堆中的元素即为topk ,代码