堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆(下图左);或每个结点的值都小于等于其左右孩子结点的值,称为小顶堆(下图右)。
这里需要从堆的定义可知,根结点一定是堆中所有结点最大(小)值。如果按照层序遍历的方式给结点从1开始编号,则结点之间满足如下关系:
1.堆排序算法
堆排序(Heap Sort)就是利用堆进行排序的方法。它的基本思想是,将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根结点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次小值。然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次小值,如此反复执行便能得到一个有序序列了。
这种方法需要解决两个问题:
1.如何由一个无序序列构建成一个堆?
2.如果在输出堆顶元素后,调整剩余元素成为一个新的堆?
先看代码:
/* 对顺序表L进行堆排序 */
void HeapSort(SqList *L)
{
int i;
for(i=L->length/2;i>0;i--) //把L中的r构建成一个大顶堆
HeapAdjust(L,i,L->length);
for(i=L->length;i>1;i--)
{
swap(L,1,i); //将堆顶记录和当前未经排序的子序列的最后一个记录交换
HeapAdjust(L,1,i-1); //将L->r[1..i-1]重新调整为大顶堆
}
}
从代码中可以看出,整个排序过程可以分为两个for循环。第一个for循环要完成的就是将现在的待排序序列构建成一个大顶堆。第二个循环要完成的就是逐步将每个最大值的根结点与末尾元素交换,并且再调整其为大顶堆。
堆调整(HeapAdjust)函数代码如下:
/* 已知L->r[s..m]中记录的关键字除L->r[s]之外均满足堆的定义
本函数调整L->r[s]的关键字,使L->r[s..m]成为一个大顶堆 */
void HeapAdjust(SqList *L, int s, int m)
{
int temp, j;
temp = L->r[s];
for(j=2*s;j<=m;j*=2) //沿关键字较大的孩子结点向下筛选
{
if(j<m && L->r[j]<L->r[j+1])
++j; // j为关键字中较大的记录的下标
if(temp>=L->r[j])
break; //rc应插入在位置s上
L->r[s] = L->r[j];
s=j;
}
L->r[s]=temp; // 插入
}
假设我们要排序的序列是{50,10,90,30,70,40,80,60,20},那么L.length=9,函数HeapSort()第一个for循环中,代码第4行,i是从[9/2]=4开始,4→3→2→1的变量变化。
如图所示,灰色结点的编号就是1,2,3,4,它们都是有孩子的结点。所谓的将待排序的序列构建成一个大顶堆,其实就是从下往上,从右到左,将每个非终端结点(非叶结点)当做根结点,将其和其子树调整成大顶堆。i的4→3→2→1的变量变化就是30,90,10,50的结点调整过程。
既然如此,再来看看HeapAdjust()函数的实现。
至此,大顶堆的构建完成了。HeapSort()函数的4-5行循环执行完毕。接下来HeapSort()函数的第6-11行就是正式的排序过程。
for(i=L->length;i>1;i--)
{
swap(L,i,1); //将堆顶记录和当前未经排序子序列的最后一个记录交换
HeapAdjust(L,1,i-1); //将L->r[1..i-1]重新调整为大顶堆
}
如此,便完成了排序。
2.堆排序复杂度分析
堆排序的运行时间主要消耗在初始构建堆和重建堆时的反复筛选上。构建堆的时间复杂度为O(n),重建堆的时间复杂度为O(),因此总体时间复杂度为O(
),优于冒泡,直接插入、简单选择等算法。但堆排序不适合排序个数较少的情况。