堆排序是利用堆的性质进行排序的过程。堆排序包括构成初始堆和利用堆排序这两个过程,堆分成大根堆和小根堆,这里我们使用大根堆来展开讨论。
例如我们有如下一组数据和其对应的完全二叉树:
{35、10、17、40、39、9、50、44、16、26}
构成初始堆就是把待排序的元素序列{R0~Rn},按照堆的定义调整成堆{R0’~Rn’},因为是一颗完全二叉树,根据完全二叉树的性质可知:
- 节点索引为i的左孩子的索引是 (2*i+1)
- 节点索引为i的左孩子的索引是 (2*i+2)
- 节点索引为i的父结点的索引是 floor((i-1)/2)
(4)节点索引为i的父结点大于其左右子节点,Ri’ >=R2i+1’ , Ri’ >=R2i+2’
为此需要从对应的完全二叉树中编号最大的非叶子节点(索引为n/2-1)起遍历至树根节点(索引为0),依次对遍历到的每个节点进行筛选,以便每个节点形成堆(即父节点不小于左右两个子节点),当遍历至根节点后,就将整棵树变成了一个初始堆。
下面介绍下如何对每个非叶子节点Ri进行筛选,以便构成以Ri为根的堆。因为当对Ri进行筛选时,比他索引大的节点都以进行过筛选,即已经构成了以各自非叶子节点形成的堆,这其中包括Ri的左右子节点(非叶子节点)R2i+1 , R2i+2为根的堆,所以对Ri进行筛选就是比较其与左右子节点的操作:首先把Ri的值与左右子节点中较大值比较,若Ri的值大于等于子节点的值则Ri节点筛选完毕,若子节点的值较大,则使用这个子节点和Ri交换值,交换值后可能会破坏该子节点的堆,还需要再将其与子节点继续比较形成堆,以此类推直至父节点的值都大于等于子节点或者子节点为空时结束。至此,以Ri为根节点的堆就形成,在对Ri节点调整成堆的过程中,若他小于子节点会被逐层下移,就像是筛子一样,小的被漏下大的被留下,所以把这个过程称为筛运算。
下图是是对待排序元素{35、10、17、40、39、9、50、44、16、26}构成的初始堆的全过程。因为节点数n=10,所以从编号n/2 -1 =4的节点起遍历至根节点,依次对每一个节点进行筛运算。
根据堆运算和上面的堆初始化过程可以知道编号为0
的节点A[0](即堆顶)是堆中最大的元素,所以利用堆排序的过程就比较简单了,首先把A[0]和A[n-1]互换,使得A[n-1]成为最大元素,接着对A[0]~A[n-2]再进行筛运算,又得到A[0]~A[n-2]区间内的最大值,再讲堆顶元素和A[n-2]互换,以此类推,经过n-1次对换和筛选后所有节点都有序,排序完毕。
在上面已经构成的堆上进行前两个元素排序流程如下图:
完整代码:
void Sift(int *A,int n,int i)
{
int x=A[i];
int j=2*i+1;
while(j<=n-1)
{
if(j<=n-1 && A[j]<A[j+1])
{
j++;
}
if(x<A[j])
{
A[i]=A[j];
i=j;
j=2*i+1;
}else{
break;
}
}
A[i]=x;
}
void HeapSort(int *A,int n)
{
int x;
int i;
for(i=n/2-1;i>=0;i--)
{
Sift(A,n,i);
}
for(i=1;i<=n-1;i++)
{
x=A[0];
A[0]=A[n-1];
A[n-1]=x;
Sift(A,n-1,0);
}
}