简单的选择排序中没有把每一次比较的结果保存,在下一次的比较中有许多比较已经在前一次做过了,但由于没有保存,所以下一次的排序又重复执行了这些比较,因而记录的次数很多。
堆排序正是在这个部分有所优化。
堆的基本概念:
是具有一些性质的完全二叉树-----每个结点的值都大于或等于其左右孩子结点的值,称大顶堆;小于等于的,称为小顶堆
若按层序遍历的方式给结点1开始编号,则有:
Ki>=K2i Ki>=K2i+1 (小顶堆相反)
1<=i<=[n/2] ([n/2]是不超过n/2的最大整数)
这里运用到了二叉树的性质:
如果对一棵有n个结点的完全二叉树(深度为[log2n]+1)的结点按层序编号,对任一结点i(1<=i<=n)有:
1.如果 i=1,则结点i是二叉树的根,无双亲;若 i>1,其双亲结点是[i/2]
2.如果 2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i
3.如果 2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1
故对于有n个结点的二叉树,它的i值必须小于等于[n/2]
堆排序算法(Heap Sort)
基本思路:
将待排序的序列构造为一个大顶堆,此时序列最大值即为堆顶的根结点。将它与堆数组的末尾元素交换,此后末尾元素即为最大值。然后将其余的n-1个元素重新构造成一个堆,这样就会得到剩下元素中的最大值,重复步骤,得到有序序列
所用的基本结构:
#define MAX 10
//排序所用的顺序表结构
typedef struct
{
int a[MAX+1]; //存储排序数组,a[0]用作临时变量
int length; //记录顺序表的长度
}Sqlist;
//交换
void swap(Sqlist *L,int i,int j)
{
int t=L->a[i];
L->a[i]=L->a[j];
L->a[j]=t;
}
算法:
//堆排序
void HeapSort(Sqlist *L)
{
int i;
for(i=L->length/2;i>0;i--) //i=L->length/2的意义是根据二叉树的性质推出,它们都是有孩子的结点
HeapAdjust(L,i,L->length);
for(i=L->length;i>1;i--)
{
swap(L,1,i);
HeapAdjust(L,1,i-1);
}
}
解析:
第一个for将待排序的序列构成一个大顶堆
第二个for将每个最大值的根结点与末尾元素交换,并再重新调整成为大顶堆
第6行代码,i=L->length/2 即是所有有孩子的结点,以其为参数,调用 HeapAdjust 来进行一步步构建大顶堆
HeapAdjust:
//s标号的结点即为所需调整的根结点
//该算法主要是找到最大结点,然后将根节点与最大结点交换位置,完成大顶堆的调整
void HeapAdjust(Sqlist *L,int s,int m)
{
int t,j;
t=L->a[s];
for(j=2*s;j<=m;j*=2) //沿着关键字较大的孩子结点向下筛选
{
if(j<m&&L->a[j]<L->a[j+1]) //左右孩子比较大小,当右孩子大时,j+1即变为右孩子
j++;
if(t>=L->a[r]) //当根结点最大时,退出循环
break;
L->a[s]=L->a[j]; //把较大的孩子结点值赋给根结点 ,此时a[s],a[j]都是最大值
s=j; //便于下面的交换值
}
L->a[s]=t; //交换
}
解析:
该算法主要是找到最大结点,然后将根节点与最大结点交换位置,完成大顶堆的调整
for循环解析:for循环遍历其结点的孩子。
初始条件 j=2*s 意为j是s的左孩子,s的左孩子序号一定是2s,右孩子一定是2s+1
以 j*=2 为增量,意味着遍历孩子的孩子
j<m表示不是最后一个结点
第9行代码,当右孩子大于左孩子时,j++转为右孩子
第11行代码,当根结点就是最大结点时,退出循环,直接进行和自己进行交换,序列不变
......
第二个for:
for(i=L->length;i>1;i--)
{
swap(L,1,i); //将堆顶元素与最后一个序列元素互换
HeapAdjust(L,1,i-1); //重新构成大顶堆,注意i-1
}
复杂度
主要在初始构建堆和重建堆时的反复选择上耗费时间
每个非叶子结点最多进行两次比较和互换,时间复杂度O(n)
第i次取堆顶重建时用时O(logi) (完全二叉树某结点到根结点距离[log2 i]+1),且取n-1次堆顶,故重建堆复杂度O(nlogn)
综上,时间复杂度O(nlogn)
稳定性
记录的比较和交换是跳跃式的,所以堆排序不稳定
(由于初始建堆比较次数多,故堆排序不适用于待排序序列个数少的情况)