(1)堆的概念:对n个关键字序列k1,k2,k3,...,kn,当且仅当满足下述关系是成为堆:
ki<=k(2i)且ki<=k(2i+1)(1≤i≤ n/2),当然,这是小根堆,大根堆则换成>=号。k(i)相当于二叉树的非叶子结点,K(2i)则是左子节点,k(2i+1)是右子节点。
若将此序列所存储的向量k[1..n]看做是一棵完全二叉树的存储结构,则堆实质上是满足如下性质的完全二叉树:树中任一非叶子结点的关键字均不大于(或不小于)其左右孩子(若存在)结点的关键字。
(2)堆排序思想(小根堆)
对n个待排序的记录,首先根据各记录的关键字按堆的定义排成一个序列(即建立初始堆),从而由堆顶得到最小关键字的记录,如此反复进行出堆和将剩余记录调整为堆的过程,当堆仅剩下一个记录出堆时,则n个记录已按出堆的次序排成有序序列。因此,堆排序的过程分为两步:
a、建立初始堆
首先将待排序的n个关键字分别放到一颗完全二叉树的各个节点,此时完全二叉树各个节点不一定具备堆得性质。由二叉树的性质可知,所有序号大于n/2的树叶节点已经是堆(因为叶子节点没有子节点)。故初始建堆事宜序号为n/2的最后一个非终端节点开始堆的构建的,n/2,n/2-1,n/2-2,...,为根节点的子树满足堆的定义,直到序号为1的根节点为止,则n个关键字构成一个堆。在对根节点序号为i的子树建堆过程中,可能要对节点的位置进行调整以满足堆的定义。但是这种调整可能会导致原来的堆的下一次子树不满足堆的定义,这就需要对下一层进行调整,直到树叶节点。
b、调整成新堆
堆顶点的关键字输出后,如何将剩下的n-1个节点调整为堆?首先,将堆中序号为n的最后一个节点与待出堆的序号为1的堆顶节点交换,这时只需要是序号从1~n-1的节点满足堆的定义,即可由这1~n-1个节点构成新的堆。相对于原来的堆,此时堆顶节点发生了改变,而其余n-2个节点任然满足堆的定义,我们只需要堆这个新的堆顶节点进行调整就ok了。
调整方法:将根节点与左、右孩子节点中关键值较小的节点进行交换,若与左孩子交换,则左子树堆被破坏,且仅左子树的根节点不满足堆的定义。若与右孩子交换,则右子树堆被破坏,且仅右子树的根节点不满足堆的定义。对不满足堆定义的子树继续进行上述的交换操作,这种调整需要持续到叶节点或者到某个节点满足堆的定义为止。
(3)堆排序的过程
堆n个关键字序列先建成堆(构建初始堆),然后执行n-1趟排序。第一趟将序号为1的根节点与序号为n的节点交换(第n个节点用于存储出堆节点),并调整前n-1个节点为新的堆;第二趟将序号为1的根节点与序号为n-1的节点交换(第n-1个节点用于存储出堆节点),并调整前n-2个节点为新的堆...第n-1趟将序号为1的根节点与序号为2的根节点交换。此时,待调整的堆仅为序号为1的根节点故无需进行调整,整个堆排序过程结束。
注意:小根堆,对应降序排列(小的被放到后边,大的放到前边);大根堆,对应升序排列(大的放后边,小的放前边)
<pre name="code" class="cpp">/********************************************************************
*This function to do HeapSort
*Author:xiefanfan
*Time:2015-04-04 10:25
*All Rights Reseved
********************************************************************/
#include "stdafx.h"
#include <stdio.h>
/*******************this function to do heap sort based on large root heap*************************/
/*********************对R[s]~R[t]除R[s]外均满足堆得定义,即只对R[s]进行调整,使R[s]为根的完全二叉树成为一个堆***********************/
void HeapAdjust(int R[],int s,int t)
{
int i,j;
int temp = R[s]; //temprory variable
i=s;
for (j=2*i;j<=t;j=2*j) //沿关键字较大的孩子向下调整,先假定为左孩子
{
if (j<t&&R[j]<R[j+1]) //如果左孩子比右孩子小,则沿右孩子向下调整
{
j=j+1;
}
if (temp>R[j]) //如果temp比左右孩子都大,满足大根堆的定义了,不在向下调整
{
break;
}
R[i]=R[j]; //将关键字较大的孩子节点和双亲节点交换
i=j; //定位于孩子节点 继续向下调整
}
R[i]=temp; //找到合适的位置,将temp放置进去
}
//进行堆排序,R[]是一个数组,n是数组的个数
void HeapSort(int R[],int n)
{
int i;
for (i=n/2;i>0;i--) //完全二叉树 非终端节点需要进行堆定义调整,从R[n/2],R[n/2-1],.....R[0]
{
HeapAdjust(R,i,n);
}
int temp;
for (i=n;i>0;i--) //对初始堆进行n-1趟排序
{
temp = R[1];
R[1] = R[i];
R[i] = temp;
HeapAdjust(R,1,i-1); //将未排序的前i-1个节点重新调整为堆
}
}
int main()
{
int shuzu[] = {0,61,33,48,82,72,11,25,48};
HeapSort(shuzu,8); //调用堆排序函数
int index;
for(index=1;index<9;++index)
{
printf("%d\t",shuzu[index]);
}
}
堆排序花费的时间主要在构建初始堆和n-1趟堆排序。最坏情况下的堆排序时间复杂度为O(nlogn),所以最坏情况下的堆排序时间复杂度要低于快速排序。
由于堆排序初始建堆比较此时较多,因此堆排序不适宜记录较少的情况。对大量记录的排序来说,堆排序非常有效。此外,堆排序只需要一个辅助空间,空间复杂度为O(1)。而且,堆排序是不稳定的方法。