堆排序(Heap Sort)就是利用堆(假设是大顶堆)进行排序的方法。它的基本思想是,将待排序的序列构成一个大顶堆。此时,整个序列的最大值就是堆顶的根节点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次大值。如此反复执行,便能得到一个有序序列了。
1.堆
堆实际上是一棵完全二叉树,其任何一非叶节点满足性质:
Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]或者Key[i]>=Key[2i+1]&&key>=key[2i+2]
即任何一非叶节点的关键字不大于或者不小于其左右孩子节点的关键字。
堆分为大顶堆和小顶堆,满足Key[i]>=Key[2i+1]&&key>=key[2i+2]称为大顶堆,满足 Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]称为小顶堆。由上述性质可知大顶堆的堆顶的关键字肯定是所有关键字中最大的,小顶堆的堆顶的关键字是所有关键字中最小的。
2.堆排序的思想
利用大顶堆(小顶堆)堆顶记录的是最大关键字(最小关键字)这一特性,使得每次从无序中选择最大记录(最小记录)变得简单。
其基本思想为(大顶堆):
1)将初始待排序关键字序列(R1,R2....Rn)构建成大顶堆,此堆为初始的无序区;
2)将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,......Rn-1)和新的有序区(Rn),且满足R[1,2...n-1]<=R[n];
3)由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,......Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2....Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
操作过程如下:
1)初始化堆:将R[1..n]构造为堆;
2)将当前无序区的堆顶元素R[1]同该区间的最后一个记录交换,然后将新的无序区调整为新的堆。
因此对于堆排序,最重要的两个操作就是构造初始堆和调整堆,其实构造初始堆事实上也是调整堆的过程,只不过构造初始堆是对所有的非叶节点都进行调整。
3. 下面举例说明:
我们要排序的序列是 {50,10,90,30,70,40,80,60,20} length=9
一开始待排序的序列的完全二叉树如上图所示。
1).将其变为大顶堆
从下往上,从右到左,将每个非终端节点(非叶节点)当做根节点(即上图1,2,3,4,节点都是非叶子节点),将其和其子树调整为大顶堆。
就是从编号为4的节点开始,先将其子树调整为大顶堆,然后到3节点——>2节点——>1节点
调整的过程:
①. i =4时可以看到4节点的子树比它还大,所以我们交换它们两个,第二个图是调整完这步以后的结果
②. i=3时,节点3大于其子树,所以没有任何改变
③. i=2时,节点2的子树比它大,两个相互交换位置
④i=1时,节点1的子树比它大,两个相互交换位置
C++代码:
//数组r[....s....m] ,s,m均为数组的下标 ,已知数组中的关键字除了r[s]之外均满足堆的定义
//本函数调整r[s],使r[s..m]成为一个大顶堆
void HeapAdjust(int r[],int s,int m){
int temp,j;
temp=r[s];
for(j=2*s;j<m;j=j*2){ //沿关键字较大的孩子节点向下帅选,调整完以后保证以此节点为顶点的树是大顶堆了
if(j<m&&r[j]<r[j+1]) //如果左子树比右子树
++j; //j为关键字中较大记录的下标
if(temp>=r[j]) //父节点比子树中最大的数还要大
break; //不需要调整,直接退出
r[s]=r[j]; //让父节点变为子树中较大的节点
s=j; //交换父节点和 子树中较大的节点
}
r[s]=temp;
}
2.接下来就是正式排序的过程,由于有了前面充分的准备,其实这个排序就比较轻松了
for(i=lenght;i>1;i--){
swap(r,1,i);//将堆顶记录和当前未排序子序列的最后一个记录交换
HeapAdjust(r,1,i-1);//将r[1....i-1]重新调整为大顶堆
}
如下图:
②.当i=8时,交换30与80,并将30与70交换,再与60交换,此时序列变为{70,60,50,30,10,40,20,80,90}
③. 后面的变化完全类似,看下图
4.堆排序的复杂度分析
堆排序的运行时间主要消耗在初始建堆和重复建堆时的反复帅选上。
在构建堆的过程中,因为我们是完全二叉树从从下层最右边的非终端节点开始构建,将它与其孩子进行比较和若有必要的交换,对于每个非终端节点来说,其实最多进行两次比较和交换操作,因此整个构架堆的时间复杂度为O(n).
在正实排序时,第i次取堆顶记录重建堆需要用O(logi)的时间(完全二叉树的某个结点到根节点的距离为log2i+1),并且需要取n-1次堆顶记录,因此,重建对的时间复杂度为O(nlogn).
所以总体来说,堆排序的时间复杂度是O(nlogn)。由于堆排序对原始记录排序状态并不敏感,因此它无论最好、最坏和平均时间复杂度均为O(nlogn)。
空间复杂度上,它只用一个用来交换的暂存单元,也非常的不错。不过由于记录的比较和交换是跳跃式进行,因此堆排序也是一种不稳定的排序方法。
排序方法 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |