堆
前言
基于JDK1.8中PriorityQueue中堆的实现分析堆实现
什么是堆
堆是一种非线性结构,可以把堆看作一棵二叉树,也可以看作一个数组,即:堆就是利用完全二叉树的结构来维护的一维数组。 堆可以分为大顶堆和小顶堆。
完全二叉树假设深度为h
,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数为一个满二叉树
大顶堆:每个结点的值都大于或等于其左右孩子结点的值。 小顶堆:每个结点的值都小于或等于其左右孩子结点的值。
如果是排序,求升序用大顶堆,求降序用小顶堆。
一般我们说 topK 问题,就可以用大顶堆或小顶堆来实现,
最大的 K 个:容量为k大小的小顶堆
最小的 K 个:容量为k大小的大顶堆
如何定位节点
假设对堆节点进行编号(层次遍历)对应于数组的下标,Root节点编号为0,那么假设一个节点编号为i,它的父子节点编号计算规则如下:
- 父节点:
(i-1)/2 - 左节点:
2i+1 - 右节点:
2i+2
入堆
入堆时是直接插入数组尾部位置,即堆中最下层第一个为null为位置。
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;
}
siftUp-向上筛选
通过比较器(Comparator)或者可比较对象(Comparable)不断比较大小然后向上筛选。具体逻辑是如果父亲节点大于等于插入节点,则交换父亲节点和插入节点,直到插入节点的父亲节点小于插入节点。
可以看到默认情况下
PriorityQueue默认是小顶堆
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)
//x>=e
break;
//父亲节点大于等于插入节点则
//交换父亲节点到插入位置
queue[k] = e;
//交换之后 从父亲位置继续
k = parent;
}
//找到插入位置
queue[k] = x;
}
//逻辑同上
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;
}
删除元素
删除时可删除任意位置的元素
删除堆顶
一般优先级队列最常使用的就是出堆顶。具体逻辑是:将尾部的元素替换堆顶,然后向下筛选-即选择左右节点中小的那个来替换,直到它小于左右节点值或者到叶子节点。
public E poll() {
if (size == 0)
return null;
//s为数组尾部 堆的最后一个 的编号
int s = --size;
modCount++;
//root 位置在0
E result = (E) queue[0];
//取尾部节点 替换堆顶
E x = (E) queue[s];
queue[s] = null;
//如果删除后的数量不为0 则不是
if (s != 0)
siftDown(0, x);
return result;
}
siftDown-向下筛选
通过比较器(Comparator)或者可比较对象(Comparable)不断比较大小然后向下筛选。具体逻辑是选择左右节点中小的那个来替换当前节点,直到它小于左右节点值或者到叶子节点。
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
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
//c为替换节点值
Object c = queue[child];
//右子树节点下标
int right = child + 1;
//右子树存在
if (right < size &&
//且 左节点大于右子节点
((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
//那么以右节点替换当前节点 否则就是不变还是左节点替换
c = queue[child = right];
//如果当前节点小于左右节点值c为可能 则不需要往下筛选了
if (key.compareTo((E) c) <= 0)
break;
//交换
queue[k] = c;
//交换后继续
k = child;
}
queue[k] = key;
}
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;
}
删除任意位置
删除任意节点和删除栈顶元素相同,但是需要多考虑一步-避免i-1位置被改动导致遍历时中断后续元素。官方解释如下:
Removes the ith element from queue. Normally this method leaves the elements at up to i-1, inclusive, untouched. Under these circumstances, it returns null. Occasionally, in order to maintain the heap invariant, it must swap a later element of the list with one earlier than i. Under these circumstances, this method returns the element that was previously at the end of the list and is now at some position before i. This fact is used by iterator.remove so as to avoid missing traversing elements.
//返回以前位于列表末尾、现在位于i之前某个位置的元素 避免
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;
}
总结
复杂度
插入和删除的复杂度都是O(log n)
应用
堆排序
堆排序就是利用大顶堆或者小顶堆进行排序。因为堆顶是极值,所以不断出堆顶元素即可完成排序。由于插入元素和删除堆顶元素的时间复杂度都是log(n)(最坏情况取决于树的高度,完全二叉树的高度为log(n),所以堆排序的时间复杂度为n*log(n),空间复杂为n,且是非稳定。
延迟队列
利用deadline作为顺序,如果堆顶元素的deadline大于当前时间戳-t,那么需要等待deadline-t后再次获取。例如ScheduledThreadPoolExecutor中DelayedWorkQueue也是用的小顶堆。