定义
堆是一种特殊的树:
- 堆是一个完全二叉树。
- 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。大于叫大顶堆(此文以大顶堆为例),小于叫小顶堆。
因为这两个特性,堆的查找效率很低,所以一般也不实现其查找元素。我们后面再讲其应用场景。先来看看图示: 完全二叉树适合用数组存储:如图2
- 节点:下标i
- 左节点:$i*2$
- 右节点:$i*2+1$
- 父节点:$i/2$
- 第一个节点:array[1]
图2
实现
插入
思路:把插入的元素放在最后一个位置,再从下往上,和父节点对比,不满足特性就交换元素即可。
public class Heap {
private int[] a;
// 容纳元素个数
private int capacity;
// 当前元素数量
private int count;
public Heap(int capacity) {
a = new int[capacity + 1];
this.capacity = capacity;
count = 0;
}
private void insert(int data) {
if (count >= capacity) {
return;
}
count++;
// 先放中最后
a[count] = data;
int i = count;
while (i / 2 > 0 && a[i] > a[i / 2]) {
// 大于父节点
swap(a, i, i / 2);
i = i / 2;
}
}
时间复杂度:一个包含N个节点的完全二叉树,树的高度不会超过log2N,所以一复杂度为O($\log n$)
删除堆顶元素
思路:
- 删除堆顶,需要找到左右子最大的元素替换。被替换的元素作为顶,继续往下遍历删除。但会出现问题,可能不满足完全二叉树的条件;
- 删除堆顶元素,把最后一个元素替换到堆顶,然后『从上往下』对比交换元素,因为都是交换元素,不会出现某个叉空出来的情况,满足完全二叉树的特性。
public void removeMax() {
if (count == 0) {
return;
}
a[1] = a[count];
count--;
heapify(a, count, 1);
}
private void heapify(int[] a, int count, int i) {
while (true) {
int maxPos = i;
if (i * 2 <= capacity && a[maxPos] < a[i * 2]) {
maxPos = i * 2;
}
if (i * 2+1 <= capacity && a[maxPos] < a[i * 2+1]) {
maxPos = i * 2+1;
}
if (maxPos == i) {
break;
}
swap(a, i, maxPos);
i = maxPos;
}
}
时间复杂度也是O($\log n$)
建堆
插入式建堆
先把已有数组按特性重新组织,也叫建堆,如果是从第一个开始读取元素,采用前面讲的插入的方法即可。
每个元素插入时间复杂度O(logN),所以总时间复杂度为:O(N*logN)
。
如果是已经存在的数组,想原地(不需要new一个新数组存堆)建堆怎么做?也很简单,从下标为2的元素开始遍历,使用插入就行。如果是下标为i的元素,相当于i插入已经放中堆尾,然后就是往上(i/2)遍历交换元素,整个过程不影响i+1之后的元素。
从上往下
和删除堆顶的操作类似,从上往下看,保证左右节点小于自己。但因为此数组本身每堆化。如果从堆顶开始,1次遍历下来,只能保证路径上的元素满足条件。
换一种思路,如果从后往前遍历呢?
- 叶子节点:没有子,满足;
- 第一个非叶子节点a(兄弟为b):对比两个子节点,和最大的元素替换,此时以a为顶的树满足堆的条件。
- 遍历到a的父节点A:因为a已经满足了堆的条件,如果A,a,b中a最大,和a交换元素。然后就相当于前面讲到的,删除堆顶a时最后一个元素替换上来的操作。
a-1,执行第3步直到堆顶即可。另外第一步每必要遍历,因为叶子节点天然满足条件。而完全二叉树,下标从n/2+1
到 的节点都是叶子节点。于是只需要从n/2往前遍历即可。
private void buildHeap(int[] a, int n) {
for (int i = n / 2; i >= 1; --i) {
heapify(a, n, i);
}
}
时间复杂度:
简单分析:每个元素的堆化时间复杂度为O($\log n$),需要堆化的元素 n/2 + 1个,去掉常量,所以时间复杂度为O(N*logN)
,但实际上是O(N)
,详见推到过程:
// TODO
插入式建堆
堆排序
建堆之后,如果是大顶堆,第一个元素就是最大的元素,把他和最后一个元素(n)交换,此时就类似删除堆顶元素的操作。完成后,又把堆顶和n-1的元素交换,直到只剩下一个元素。
时间复杂度:O(N)
+ N* O(logN)
= O(N*logN)
应用
- 优先级队列
- topK
- 利用堆求中位数