伍老师的《大话数据结构》是本思路清晰又讲解形象的书,十分适合入门者学习。我对堆排序以前很是害怕,所以特意仔细阅读了相关堆排序的内容,感觉完全理解啦。摘抄下来存档,方便自己随时查阅和复习。
堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
如果按照层序遍历的方式给结点从1开始编号,则结点之间满足如下关系:k[i]≥k[2i]&&k[i]≥k[2i+1] 或者 k[i]≤k[2i]&&k[i]≤k[2i+1],其中,0<i<n/2。为什么i<n/2呢?现在回忆一条完全二叉树的性质:一棵完全二叉树,如果i=1,则结点 i 是二叉树的根,无双亲;如果i>1,则其双亲是结点⌊i/2⌋。那么对于有n个结点的二叉树而言,它的 i 值自然就是小于等于⌊n/2⌋了。
堆排序(Heap Sort)就是利用堆(假设利用大顶堆)进行排序的方法。它的基本思想是,将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根结点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次小值。如此反复执行,便能得到一个有序序列了。
不过要实现它还需要解决两个问题:
1.如何由一个无序序列构建一个堆?
2.如果在输出堆顶元素后,如何调整剩余元素成为一个新的堆?
程序代码说明:
#define MAXSIZE 10 /* 用于要排序数组个数最大值,可根据需要修改 */
typedef struct
{
int r[MAXSIZE+1]; /* 用于存储要排序数组,r[0]用作哨兵或临时变量 */
int length; /* 用于记录顺序表的长度 */
}SqList;
/* 交换L中数组r的下标为i和j的值 */
void swap(SqList *L,int i,int j)
{
int temp=L->r[i];
L->r[i]=L->r[j];
L->r[j]=temp;
}
/* 已知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; /*插入*/
}
/* 对顺序表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]重新调整为大顶堆*/
}
}
复杂度分析:
它的运行时间主要是消耗在初始构建堆和在重建堆时的反复筛选上。
在构建堆的过程中,因为我们是完全二叉树从最下层最右边的非终端结点开始构建,将它与其孩子进行比较和若有必要的互换,对于每个非终端结点来说,其实最多进行两次比较和互换操作,因此整个构建堆的时间复杂度为O(n)。
在正式排序时,第 i 次取堆顶记录重建堆需要用O(logi)的时间(完全二叉树的某个结点到根结点的距离为⌊log2i⌋+1),并且需要取n-1次堆顶记录,因此,重建堆的时间复杂度为O(nlogn)。
所以总体来说,堆排序的时间复杂度为O(nlogn)。由于堆排序对原始记录的排序状态并不敏感,因此它无论是最好、最坏和平均时间复杂度均为O(nlogn)。这在性能上显然要远远好过于冒泡、简单选择、直接插入的O(n平方)的时间复杂度了。
空间复杂度上,它只有一个用来交换的暂存单元,也非常的不错。不过由于记录的比较与交换是跳跃式进行,因此堆排序也是一种不稳定的排序方法。
另外,由于初始构建堆所需的比较次数较多,因此,它并不适合待排序序列个数较少的情况。