[数据结构]堆排序
算法思想
堆排序是一种树形选择排序方法,它的特点是:在排序过程中,将L[1…n]看成是一棵完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系,在当前无序区中选择关键字最大(或者最小)的元素。
堆的定义如下:n个关键字序列L[1…n]称为堆,当且仅当该序列满足:
①L(i)≤L(2i)且L(i)≤L(2i)或 ②L(i)≥L(2i)且L(i)≥L(2i+1)
满足第一种情况的堆称为小根堆,满足第二种情况的称为大根堆。显然,在大根堆中,最大元素存放在根结点中,且对其中任意一个非根结点,它的值小于或等于其双亲结点值。小根堆的定义刚好相反,根结点是最小元素。
堆排序的关键是构造初始堆,对初始序列建堆,就是一个反复筛选的过程。n个结点的完全二叉树,最后一个结点是第[n/2]个结点的孩子。对第[n/2]哥结点为根的子树筛选(对于大根堆:若根结点的关键字小玉左右子女中关键字较大者,则交换),使该子树成为堆。之后向前依次对各结点([n/2]-1~1)为根的子树进行筛选,看该结点值是否大于其左右子结点的值,若不是,将左右子结点中较大值与其交换,交换后可能会破坏下一级的堆,于是继续采用上述方法构造下一级的堆,直到以该结点为根的子树构成堆位置。反复利用上述调整堆的方法建堆,直到根结点。
算法代码
void BuildMaxHeap(Elemtype A[],int len){
for(int i=len/2;i>0;i--)//从i=[n/2]~1,反复调整堆
AdjustDown(A,i,len);
}
void AdjustDown(Elemtype A[],int k,int len){
//函数AdjustDown将元素k向下进行调整
A[0]=A[k];//A[0]暂存元素
for(i=2*k;i<=len;i*=2){//沿key较大的子结点向下筛选
if(i<len&&A[i]<A[i+1])
i++;//取key较大的子结点的下标
if(A[0]>=A[i]) reak;//筛选结束
else{
A[k]=A[i];//将A[i]调整到双亲结点上
k=i;//修改k的值,以便继续向下筛选
}
}
A[k]=A[0];//被筛选结点的值放入最终位置
}
向下调整的时间和树高有关,为O(h),建堆过程中每次向下调整时,大部分结点的高度都较小。因此,可以证明在元素个数为n的序列上建堆,其时间复杂度为O(n),这说明可以在线性时间内,将一个无序数组建成一个大根堆。
应用堆这种数据结构进行排序的思路很简单,首先将存放在L[1…n]中的n个元素建成初始堆,由于堆本身的特点(以大根堆为例),堆顶元素就是最大值。输出堆顶元素后,通常将堆底元素送入堆顶,此时根结点已不满足大顶堆的性质,堆被破坏,将堆顶元素向下调整使其继续保持大顶堆的性质,再输出堆顶元素。如此重复,直到堆中仅剩下一个元素为止。
下面是堆排序算法:
void HeapSort(Elemtype A[],int len){
BuildMaxHeap(A,len);//初始建堆
for(i=len;i>1;i--){//n-1趟的交换和建堆过程
swap(A[i],A[1]);//输出堆顶元素(和堆底元素交换)
AdjustDown(A,1,i-1);//整理,把剩余的i-1个元素整理成堆
}
}
同时,堆也支持删除和插入操作。由于堆顶元素或为最大值或最小值,删除堆顶元素时,先将堆的最后一个元素与堆顶元素交换。由于此时堆的性质被破坏,将堆顶元素向下调整操作。对堆进行插入操作时,先将新结点放在堆的末端,再对这个新结点执行向上调整操作。
下面是向上调整堆的算法:
void AdjustUp(Elemtype A[],int k){
//参数k为向上调整的结点,也为堆的元素个数
A[0]=A[k];
int i=k/2;//若结点值大于双亲结点,则将双亲结点向下调,并继续向上比较
while(i>0&&A[i]<A[0]){
A[k]=A[i];//双亲结点下调
k=i;
i=k/2;//继续向上比较
}
A[k]=A[0];//复制到最终位置
}
算法复杂度
空间复杂度:仅使用了常数个辅助单元,所以空间复杂度为O(1)。
时间复杂度:建堆时间为O(n),之后有n-1次向下调整的操作,每次调整的时间复杂度为O(log₂n)。所以时间复杂度为O(nlog₂n)。
稳定性:在进行筛选时,有可能把后面相同关键字的元素调整到前面,所以堆排序算法是一种不稳定的排序方法。例如,表L={1,2,2},构造初始堆时,可能将2交换到堆顶,此时L={2,1,2},最终排序序列为L={1,2,2},显然,2和2的相对次序已经发生了变化。