【问题】应用堆排序 (heap sort) 方法对一个记录序列进行升序排列。堆 (heap) 是具有下列性质的完全二叉树:每个结点的值都小于或等于其左右孩子结点的值(小根堆);或者每个结点的值都大于或等于其左右孩子结点的值(大根堆)。如果将具有 n 个结点的堆按层序从 1 开始编号,则结点之间满足如下关系:
以结点的层序编号作为下标,将堆用顺序存储结构(即数组)来存储,则堆对应于一组序列,如下图所示。
【想法】堆排序是利用堆(假设利用大根堆)的特性进行排序的方法,其基本思想是:首先将待排序的记录序列构造成一个堆,此时,堆顶记录是堆中所有记录的最大者,将它从堆中移走(通常将堆顶记录和堆中最后一个记录交换),然后将剩余记录再调整成堆,这样又找出了次大记录,依此类推,直到堆中只有一个记录为止,如下图所示。
如何将一个无序序列调整为堆是堆排序算法的关键,筛选法调整是成功应用减治法的例子。如下图所示,图 (a) 是一棵完全二叉树,且根结点 28 的左右子树均是堆。为了将整个二叉树调整为堆,首先将根结点 28 与其左右子树的根结点比较,根据堆的定义,应将 28 与 35 交换,如图 (b) 所示。经过这一次交换,破坏了原来左子树的堆结构,需要对左子树再进行调整,调整后的堆如图 (c) 所示。
由这个例子可以看出,在堆调整的过程中,总是将根结点(即被调整结点)与左右子树的根结点进行比较,若不满足堆的条件,则将根结点与左右子树根结点的较大者进行交换,这个调整过程一直进行到所有子树均为堆或将被调整的结点(即原来的根结点)交换到叶子为止。这个自堆顶至叶子的调整过程称为筛选(sieve)。
【算法】假设当前要筛选结点的编号为 k,堆中最后一个结点的编号为 n,且结点 k 的左右子树均是堆(即 ~ 满足堆的条件) 筛选算法用伪代码描述如下。
算法:筛选法调整堆 SiftHeap
输入:~ 满足堆的条件,待筛选的记录
输出:{,,...,} 为大根堆
1 设置 i 和 j ,分别指向当前要筛选的结点和要筛选结点的左孩子;
2 若 已是叶子,则筛选完毕;
否则,比较要筛选结点的左右孩子结点,并将j指向值较大的结点;
3 将 和 进行比较,有以下两种情况:
3.1 如果 > ,则完全二叉树已经是堆,筛选完毕;
3.2 否则将 和 交换;令 i = j,转步骤 2 继续进行筛选。
【算法分析】算法 Sift 将根结点与左右子树的根结点进行比较,若不满足堆的条件,则将根结点与左右子树根结点的较大者进行交换,所以,每比较一次,需要调整的完全二叉树的问题规模就减少一半,因此,其时间性能是。
【算法实现】堆排序首先将无序序列调整成堆,由于叶子结点均可看成是堆,因此,可以从编号最大的分支结点直至根结点反复调用筛选算法。堆排序算法用JAVA语言描述如下:
public class SiftHeap {
public static void main(String[] args)
{
int r[]={47,33,35,2,18,71,26,13};
HeapSort(r,8);
for(int i=0;i<8;i++)
System.out.print(r[i]+" ");
}
static void SiftHeap(int r[], int k, int n)
{
int i, j, temp;
i = k; j = 2 * i +1; //置i为要筛的结点,j为i的左孩子
while (j < n) //筛选还没有进行到叶子
{
if (j < n-1 && r[j] < r[j+1]) j++; //比较i的左右孩子,j为较大者
if (r[i] > r[j]) //根结点已经大于左右孩子中的较大者
break;
else {
temp = r[i]; r[i] = r[j]; r[j] = temp; //将被筛结点与结点j交换
i = j; j = 2 * i+1; //被筛结点位于原来结点j的位置
}
}
}
static void HeapSort(int r[], int n)
{
int i, temp;
for (i = (n-1)/2; i >= 0; i--) //初始建堆,从最后一个分支结点至根结点
SiftHeap(r, i, n) ;
for (i = 1; i <= n-1; i++) //重复执行移走堆顶及重建堆的操作
{
temp = r[0]; r[0] = r[n-i]; r[n-i] = temp;
SiftHeap(r, 0, n-i); //只需调整根结点
}
}
}
运行结果如下:
from:算法设计与分析(第2版)——王红梅 胡明 编著——清华大学出版社