传说中的另一棵树
堆
一种特殊的树。
基本概念
堆需要满足的两个要求:
- 堆是一个完全二叉树
- 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。
大顶堆:节点的值大于等于子树中节点的值
小顶堆:节点的值小于等于字数中节点的值
![]() |
![]() |
堆的实现
因为堆是完全二叉树,用数组来存储比较方便。
节点下标为i
,则左子节点下标为2*i
,右子节点下标为2*i+1
,父节点下标为i/2
。
插入
堆中插入新数据后,需要满足堆的两个特性。调整的过程叫做“堆化”。
堆化分为两种:从上往下、从下往上。
思路:顺着节点所在的路径,向上,对比,交换。
public class Heap {
private int[] storage;
private int size;
private int count;
public Heap(int size) {
storage = new int[size];
this.size = size;
count = 0;
}
public void insert(int data) {
if (count >= size) {
return;
}
storage[++count] = data;
int idx = count;
while(idx/2 != 0 && storage[idx] > storage[idx/2]) {
int fIdx = idx/2;
int tmp = storage[fIdx];
storage[fIdx] = storage[idx];
storage[idx] = tmp;
idx = idx/2;
}
}
}
堆化虽说有两种方式,但是从上往下的堆化方式并不适合用在插入中。
删除
对于堆来说,因为堆结构由严格的要求(完全二叉树)限制,若是删除任意元素,需要重新构建堆。一般的操作需求都是删除堆顶元素(最大值或最小值),
删除,就是要减少一个,因为要保持完全二叉树的结构,用来补充删除的元素只能是从最后一个叶子节点来补。
补充之后需要再次进行堆化处理,此时就是从顶部开始(从上到下的方式)。
public void removeMax() {
if (count == 0)
return;
storage[0] = storage[count--];
heapify(storage, count);
}
private void heapify(int[] storage, int length) {
int pIdx = 1;
while(true) {
int lIdx = pIdx * 2;
int rIdx = pIdx * 2 + 1;
int swapIdx = pIdx;
if (lIdx <= length && storage[pIdx] < storage[lIdx]) {
swapIdx = lIdx;
}
if (rIdx <= length && storage[pIdx] < storage[rIdx]) {
swapIdx = rIdx;
}
if (swapIdx == pIdx) {
break;
}
int tmp = storage[swapIdx];
storage[swapIdx] = storage[pIdx];
storage[pIdx] = tmp;
pIdx = swapIdx;
}
}
一个需要注意的地方,使用数组存储二叉树的时候,左右子树的计算公式都是基于根节点的下标从1开始的前提,所以左右子结点的下标的判断时需要注意。程序中的count记录的是节点的个数,所以判断条件rIdx <= length
。
基于堆实现排序
堆排序分为建堆和排序两个步骤
建堆
建堆的可以有两种思路:
- 按照插入的思路来建堆,一个一个往数组里放数据,每放一个数据都是从下往上堆化
- 全部放进数组,然后从第一个非叶子节点开始,从后往前地进行向下堆化。
第二种思路注意两点:
- 堆化的思路还是从下往上,堆化的范围是当前处理节点的子树。
- 处理的顺序是从后往前,先处理最后一个非叶子节点,依次直到根节点。
实现如下:
public void buildHeap(int[] storage, int size) {
for (int i = size/2; i > 0; i--) {
heapify(storage, size, i);
}
}
把写删除的时候的堆化函数用起来,添加一个参数:
private void heapify(int[] storage, int length, int i) {
int pIdx = i;
while(true) {
int lIdx = pIdx * 2;
int rIdx = pIdx * 2 + 1;
int swapIdx = pIdx;
if (lIdx <= length && storage[pIdx] < storage[lIdx]) {
swapIdx = lIdx;
}
if (rIdx <= length && storage[pIdx] < storage[rIdx]) {
swapIdx = rIdx;
}
if (swapIdx == pIdx) {
break;
}
int tmp = storage[swapIdx];
storage[swapIdx] = storage[pIdx];
storage[pIdx] = tmp;
pIdx = swapIdx;
}
}
复杂度分析
规律:
- 每个节点堆化的过程中,需要比较和交换的节点个数,跟节点的高度成正比。
- 叶子节点是不需要进行堆化的。
![](https://i-blog.csdnimg.cn/blog_migrate/7a546f1d330e3cae6b2926b85a989511.jpeg)
因此,复杂度的计算就是
![](https://i-blog.csdnimg.cn/blog_migrate/501556a17eb993beb06dcab89e795b49.jpeg)
每个节点堆化的复杂度是O(logn),因此总体复杂度是O(n)
排序
利用堆顶元素是最大值或最小值,以及每次堆化,处于堆顶的值都会是当前参加堆化的节点的极值这一点,来进行排序。
总结下来就是从每次参加堆化的节点中去掉堆顶,咋去掉呢,就是拿最后一个子元素来替换堆顶。
实现
public void sort(int[] storage, int size) {
buildHeap(storage, size);
int length = size;
while(length > 1) {
int tmp = storage[length];
storage[length] = storage[1];
storage[1] = tmp;
heapify(storage, --length, 1);
}
}
总结下:堆化函数要好好写呢~
复杂度:O(nLogn)。
堆排序PK快速排序
快排在实际中应用比较多的原因:
- 堆排序对数组元素的访问不是顺序的,对CPU缓存不友好
- 堆排序过程中的交换次数多于快速排序,而且会打乱数据有序度
堆的应用
- 优先级队列
那个啥高性能定时器的例子在老白的代码里见到了呢 - 求TopK
- 求中位数:大顶堆和小顶堆配合用
![](https://i-blog.csdnimg.cn/blog_migrate/908146adcca8b907605106d9269a2fb0.jpeg)