一、基本定义
堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其根节点的值。根据大小关系进行分类,将根结点不小于其左右孩子结点值的堆称为大顶堆。将根结点不大于其左右孩子结点值的堆称为小顶堆。堆一般用于优先队列的实现,优先队列默认使用大顶堆。
二、堆的基本操作
1、给定一个序列,如何建堆。根据初始序列,先从左到右,从上到下,建立一棵度为2的树。之后对树中各个结点进行调整,直到该树是大顶堆(或小顶堆,此处演示大顶堆)。具体调整方法:首先找到最下最右边的一个非叶子结点,假设编号为x,让x与以x为根结点的下一级分支结点进行比较,如果存在大于x的左右分支结点,让x与其交换,保证当前小分支里最大值为根结点,交换之后,再让x与下一级分支结点进行比较,知道该分支内结点值都小于x或者x没有下一级分支(x为叶子结点。)。
此处可以用数组存储堆,第一个结点存储在数组一号位,若结点在数组i号位,则左孩子结点为2*i,右孩子结点为2*1+i。
const int maxn=100;
int heap[maxn];
调整树,完成建堆的过程都是把结点从上往下进行调整,这叫向下调整。(判断某个结点是否是叶子结点依据:其左孩子2*i是否大于结点总个数n,大于是叶子结点。)
//low为欲调整结点的下标,high为堆最后一个结点的数组下标n。
void downAdjust(int low,int high){
int i=low,j=i*2; //j为i的左孩子结点。
while(j<=high){ //判断j是否是叶子结点。
if(j+1<high&&heap[j+1]>heap[j]){ //找左右结点中最大值。
j=j+1;
}
if(heap[j]>heap[low]){
swap(heap[low],heap[j]);
i=j; //保持i为欲调整结点。
j=i*2;
}else{
break;
}
}
}
假设序列中共有n个结点,由于是完全二叉树,所以叶子结点个数为n/2+1(向上取整)。非叶子结点数组下标区间为【1,n/2】。可以从n/2处倒着枚举结点(一定要倒着枚举,正着枚举无法保证每一个小分支下根节点都是该分支中的最大值。读者可自行举例验证。),对每个结点进行【n/2,n】范围内的调整。
//建堆
void createHeap(){
for(int i=n/2;i>=1;--i){
downAdjust(i,n);
}
}
2、向堆中添加一个元素。把要添加的结点放在数组最后,然后进行向上调整操作,该操作总是把欲调整的结点与该结点所对应的根结点进行比较,如果比根结点值大,则交换该结点与其根结点,反复比较,直到到达堆顶,或者该结点不大于其根结点。
//添加元素x。
void insert(int x){
heap[++n]=x;
upAdjust(1,n);
}
//low一般设为1,high表示要调整的结点数组下标。在heap【low,high】范围内进行向上调整
void upAdjust(int low,int high){
int i=high,j=i/2;
while(j>=low){
if(heap[j]<heap[i]){
swap(heap[j],heap[i]);
i=j;
j=i/2;
}else{
break;
}
}
}
3、堆排序。堆排序是指用堆结构对一个序列进行排序,是对一个已经建好的堆进行操作,此处建立大顶堆(非递增排序)。对一个堆来说,堆顶元素是最大的,堆排序思想就是,每次都取堆顶元素,然后用堆中最后一个结点替换至堆顶,接着对堆进行一次向下调整。反复实现,直到堆中只剩一个元素。在实际中,为了节省时间,倒着遍历数组,假设当前为i号位,将堆顶与i结点互换,在【1,i-1】范围内进行向下调整操作。
//堆排序
void heapSort(){
createHeap();
for(int i=n;i>1;--i){
printf("%d ",heap[i]);
swap(heap[i],heap[1]);
downAdjust(1,i-1);
}
}