《算法导论》一书中提到几个经典的排序算法,今天所写的是我所理解到的堆排序,如果有误,恳请大神提出修改意见。
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
——来自维基百科
堆排序:由于它在直接选择排序的基础上利用了比较结果形成。效率提高很大。它完成排序的总比较次数为O(nlog2n)。它是对数据的有序性不敏感的一种算法。但堆排序将需要做两个步骤:-是建堆,二是排序(调整堆)。所以一般在小规模的序列中不合适,但对于较大的序列,将表现出优越的性能。
——来自百度文库
理解各种排序的优缺点对于排序的学习有很大的帮助哦。下面我将以题目的形式来讲述堆排序。1.如果有一个数组,假设是p{3,6,9,0,20,0,100,1,2,100},是以0为起始(有些书是以1起始的,所以有所不同)。我们可以把这个数组想象成一个完全二叉树(堆排序经常结合完全二叉树理解,但实际上没有建树,直接在数组上操作)。
其中3在数组的下标是0,6是1,9是2,依次类推。
再明确一个重要的关系,就是3的左右孩子是6和9,对应的下标是1和2,也就是0*1+1和0*1+2。归纳可以知道给出一个下标i,它的左孩子是2*i+1,右孩子是2*i+2(假设均不超出数组的范围)
2.有了上面的思路,接下来就是建堆的过程。
1).首先从上面的树看,建堆只需要从20(下标为4)开始,而不是100(下标为9),原因是20为最后一个父亲节点了。所以先找到最后一个父亲节点(p.length/2-1),从此处开始执行维护堆的操作,一直回到根节点。C代码如下:
void BuildMaxHeap(int *p,int length)
{
int i;
for(i=length/2-1;i>=0;i--){
MaxHeapify(p,i,length); //该方法是维护堆操作
}
}
2).在此列举几步建堆的操作,过程是类似的,可以举一反三。注意到堆的特点子结点的键值或索引总是小于(或者大于)它的父节点,在此所建的堆是子结点的键值总是小于它的父节点,也就是我们常说的大顶堆。
2.1.由于20小于它的左孩子100,那么就把20与100交换位置,然后20就去到数组的最后,不需要继续执行维护堆的操作。见下图:
2.2.接着下标退回到3,也就是0执行维护堆的操作,比较发现右孩子2比左孩子1大,所以0和2交换。见下图:
2.3.跳过下标为2的操作(与前面的操作一样),直接去到下标为1的操作。由于6比100小,所以6和100交换,又由于交换完之后6还是有左孩子20,继续比较,6和20交换。如下图:
2.4.最终的建堆结果如下:
维护堆的操作函数为:
void MaxHeapify(int *p,int i,int length)
{
int l = Left(i); //获取左孩子下标
int r = Right(i); //获取右孩子下标
int largest = i;
if(l<=length-1&&l>=0&&p[l]>p[i]){
largest = l; //父亲比左孩子小,记下左孩子下标,以便交换
}
if(r<=length-1&&r>=0&&p[r]>p[largest]){
largest = r;
}
if(largest != i){ //如果不相等,要进行交换操作
int temp = p[i];
p[i] = p[largest];
p[largest] = temp;
MaxHeapify(p,largest,length); //继续执行维护堆的操作,知道左右孩子下标都超过了数组的范围
}
}
3.建好堆之后,还需要进行的步骤是排序(建好的堆并没有按照从大到小的顺序排好)。
回到数组讲述,第一步首先将怕p[0]和p[9]交换,因为p[0]肯定是最大的那个数了,所以将其放到最后,接下来对前面的8个数进行维护堆的操作,也就是重新建成一个小一点的堆,直到根节点结束。代码如下:
void Heapsort(int *p,int length)
{
int i;
int psize = length - 1 ;
for(i=length-1;i>=1;i--){
int temp = p[i];
p[i] = p[0];
p[0] = temp;
MaxHeapify(p,0,psize--);
}
}