CHAPTER_9 提高篇(3)——数据结构(2)
9.7.1堆的定义与基本操作
堆是一颗完全二叉树,树中每个节点的值都不小于(或不大于)其左右孩子节点的值。其中,如果父亲节点的值大于或等于孩子节点的值,这样的堆称为大顶堆。如果父亲节点的值小于或等于孩子节点的值,这样的堆称为小顶堆。堆一般用于优先队列的实现,而优先队列默认情况下使用的是大顶堆。
通过上图两个堆的示例可以看到,堆是既有完全二叉树的特性,也有属于自己的特性:
(1)第i个分支节点的子节点的序号总是 2 * i + 1和 2 * i + 2。
(2)对于大顶堆,每个节点的值都是以它为根节点的子树的最大值;对于小顶堆,每个节点的值 都是以它为根节点的子树的最小值。
(3)这个堆的最深深度为 (log2n) + 1,所以第 i 个节点所在的层数也为 (log2n) + 1。
(4)堆里的第i层最多有 2^i - 1个节点。
那么,对于一个给定的初始序列,怎样把它建成一个堆呢?
建堆
首先我们要确定堆结构的实现。堆也是一棵完全二叉树,完全二叉树可以用最常用的二叉链表实现。但是对于完全二叉树,我们在9.1.3节介绍了更简洁的实现方法,使用数组存储完全二叉树,其中节点按层序存储于数组中。对于第i个节点,存储与下标为i的位置,这样i的左孩子下标为2i,右孩子下标为2i+1。于是可以按如下定义堆:
const int maxn=100;
//maxn为最大可能节点个数
int heap[maxn];
在定义堆结构之后,我们将所有节点存入heap数组,建立一棵完全二叉树。接下来要进行最关键的操作—调堆,通过调堆让该完全二叉树变成一个堆。下面介绍大顶堆的调堆过程:
调堆从总体上看是“从下往上,从右往左”的过程。对于一个有n的节点的堆,我们从最后一个非叶子节点开始,向前遍历到第一个节点,并对每个遍历的节点进行调整操作(调整操作在下面讲解)。通过完全二叉树的性质,我们很容易就获得了最后一个非叶子节点的下标: (向下取整)。例如对于上图的大顶堆,我们找到最后一个非叶子节点下标 9 / 2 = 4 。接着从4号节点向前遍历到1号根节点,对每个节点进行调整操作。
对每个节点的调整操作是这样的: 总是将当前节点V与它的左右孩子比较(如果有的话),假如孩子中存在权值比节点V大的,就将节点V与孩子中的较大者交换;交换完毕后继续让节点V继续和孩子比较,直到节点V的孩子的权值都比节点V的权值小或是节点V不存在孩子节点。
这样可以写出代码:
const int maxn=100;
int heap[maxn];
void downAdjust(int low,int high) { //调整范围[low,high]
int i=low,j=i*2; //欲调整节点i,j为其左孩子
while(j<=high) {
//如果右孩子存在且其值大于左孩子
if(j+1<=high&&heap[j+1]>heap[j]) {
j=j+1; //j存储右孩子下标
}
//如果孩子中最大节点的权值比欲调整节点i大
if(heap[j]>heap[i]) {
swap(heap[i],heap[j]); //交换两节点
i=j; //保持i为欲调整节点,j为i的左孩子
j=j*2;
}
else {
break;
}
}
}
void createHeap() {
for(int i=n/2;i>=1;i--) { //从最后一个非叶子节点开始向前依次调整
downAdjust(i,n);
}
}
以上就是大顶堆的创建方法。如果要创建小顶堆,算法步骤近乎相同,只需要把downAdjust函数中的 if(j+1<=high&&heap[j+1]>heap[j]) 改为 if(j+1<=high&&heap[j+1]<heap[j]),把 if(heap[j]>heap[i])改为 if(heap[j]<heap[i])。
删除堆顶
对于大顶堆,如果要删除堆中最大元素(也就是删除堆顶元素),只需要将堆中最后一个元素覆盖堆顶,然后对堆顶做一次调整即可:
void deleteTop() {
heap[1]=heap[n];
n--;
downAdjust(1,n);
}
对于小顶堆,删除堆顶即为删除最小元素,算法步骤与大顶堆相同。
9.7.2堆排序
堆排序是指使用堆结构对一个序列进行排序。如果要进行升序排序,我们要使用大顶堆;如果要进行降序排序,我们要使用小顶堆。
升序排序的思想如下:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。
理解了上一节的内容,我们就能很快写出升序的堆排序算法:
void heapSort() {
createHeap(); //建堆
for(int i=n;i>=1;i--) {
swap(heap[1],heap[i]); //将堆顶元素与当前最后一个元素交换
downAdjust(1,i-1); //调整堆顶
}
}
对于降序排序也是相同,只需使用小顶堆的createHeap和downAdjust函数即可。