一、简介
堆是一棵具有特定性质的二叉树。
- 如果任意节点的值总是 >= 子节点的值,称为:最大堆,大根堆,大顶堆
- 如果任意节点的值总是 <= 子节点的值,称为:最小堆,小根堆,小顶堆
除此以外,所有叶子结点都是处于第 h 或 h - 1层(h为树的高度),堆是一个完全二叉树。基本接口定义如下:
public interface Heap<E> {
int size(); // 元素的数量
boolean isEmpty(); // 是否为空
void clear(); // 清空
void add(E element); // 添加元素
E get(); // 获得堆顶元素
E remove(); // 删除堆顶元素
E replace(E element); // 删除堆顶元素的同时插入一个新元素
}
二叉堆的顶层一般用数组实现即可,索引 i 的规律(n 是元素数量)
- 如果 i=0,它是根节点
- 如果 i>0,它的父节点的索引为 floor((i-1)/ 2)
- 如果 2i + 1 <= n-1,它的左子节点的索引为 2i + 1
- 如果 2i + 2 <= n-1,它的右子节点的索引为 2i + 2
- 如果 2i + 1 > n-1,它无左子节点
- 如果 2i + 2 > n-1,它无右子节点
可以看成以下数组:
72 | 68 | 50 | 43 | 38 | 47 | 21 | 14 | 40 | 3 |
二、添加
如下图所示,添加节点 80(node 节点),添加步骤如下:
- 如果 node > 父节点(38),与父节点交换位置
- 如果node <= 父节点,或者 node 没有没有父节点,退出循环
- 循环执行步骤 1,2
最终 80 成为根节点,72 成为左子树节点,50成为右子树节点。这个过程叫做上滤,时间复杂度O(logn)
实现代码如下:不断与父节点比较,如果小于父节点退出循环,否则将父节点的值替换当前节点
private void siftUp(int index) {
E element = elements[index];
while (index > 0) {
int parentIndex = (index - 1) >> 1;
E parent = elements[parentIndex];
if (compare(element, parent) <= 0) break;
// 将父元素存储在index位置
elements[index] = parent;
// 重新赋值index
index = parentIndex;
}
elements[index] = element;
}
三、删除
如下图所示删除根节点72,步骤如下:
- 用最后一个节点(叶子节点3)覆盖根节点
- 删除最后一个节点(叶子节点3),
- 循环执行以下操作(node 节点:根节点3):如果 node < 最大的子节点,最大的子节点交换位置,如果 node >= 最大的子节点,或者没有子节点,退出循环
这个操作叫做下滤(Sift Down) 。根节点 3 不断下滤,最终下滤到索引为 7 的节点 14处,时间复杂度 O(logn)
实现代码如下:
private void siftDown(int index) {
E element = elements[index];
int half = size >> 1;
// 第一个叶子节点的索引 == 非叶子节点的数量
// index < 第一个叶子节点的索引
// 必须保证index位置是非叶子节点
while (index < half) {
// index的节点有2种情况
// 1.只有左子节点
// 2.同时有左右子节点
// 默认为左子节点跟它进行比较
int childIndex = (index << 1) + 1;
E child = elements[childIndex];
// 右子节点
int rightIndex = childIndex + 1;
// 选出左右子节点最大的那个
if (rightIndex < size && compare(elements[rightIndex], child) > 0) {
child = elements[childIndex = rightIndex];
}
if (compare(element, child) >= 0) break;
// 将子节点存放到index位置
elements[index] = child;
// 重新设置index
index = childIndex;
}
elements[index] = element;
}
四、替换
替换是将传入的元素替换堆顶元素,实现步骤如下:
- 将堆顶元素替换成传入的元素
- 不断下滤
实现代码如下:
public E replace(E element) {
E root = null;
if (size == 0) {
elements[0] = element;
size++;
} else {
root = elements[0];
elements[0] = element;
siftDown(0);
}
return root;
}
五,批量建堆
批量建堆,有 2 种做法:
自上而下的上滤,从除根节点外其他节点开始做上滤操作。效率为所有节点的深度之和
仅仅是叶子节点,就有近 n/2 个,而且每一个叶子节点的深度都是O(logn)级别,所以时间复杂度达到了O(nlogn)
实现原理:上滤后所在索引之前的元素可以满足堆的性质(相当于添加节点后,新增节点上滤,满足堆的性质)
自下而上的下滤,从最后一个非叶子节点开始做下滤操作。效率为所有节点的高度之和
从最后一个非叶子节点做下滤,只有根节点操作的深度可能达到树高(O(logn))次,时间复杂度为O(n)效率比自上而下的上滤效率高,一般都是采用这种方式批量建堆。
实现原理:每一小部分下滤后满足堆的性质,然后多个小部分再合并,合并的那个节点下滤(相当于删除节点后的操作),合并的两个部分又满足了堆的性质,不断循环,最终建堆完成。
实现代码如下:
private void heapify() {
// 自上而下的上滤
// for (int i = 1; i < size; i++) {
// siftUp(i);
// }
// 自下而上的下滤
for (int i = (size >> 1) - 1; i >= 0; i--) {
siftDown(i);
}
}
六、Top K 问题
描述:从 n 个整数中,找出最大的前 k 个数(k 远远小于 n)
- 如果使用排序算法进行全排序,需要 O(nlogn)的时间复杂度
- 如果使用二叉堆来解决,可以使用 O(nlogk)的时间复杂度来解决
解决步骤:
- 新建一个小顶堆
- 先将遍历的前 k 个数放入堆中
- 从 k + 1 个数开始,如果大于堆顶元素,就使用 replace 操作(删除堆顶元素,将 k + 1 个数添加到堆中)
- 扫描完毕后,堆中的元素就是最大的前 k 个数