您的位置:首页 > 编程语言 > C语言/C++

排序算法之堆排序

2014-05-18 17:45 162 查看
堆排序只需要一个记录大小的辅助空间,每个待排序的记录仅占有一个存储空间。

一、堆的定义及建堆完整过程

堆的定义如下:n 个元素的序列 { k1 , k2 , ... , kn }当且仅当满足下列关系时,称之为堆。



       若将和此数组序列对应的以为数组(即以一维数组作此序列的存储结构)看成是一个完全二叉树,则堆的含义表明,完全二叉树中所有非终端结点的值均不大于(或不小于)其左、右孩子结点的值。由此,若序列 { k1 , k2  ,... ,kn }是堆,则堆顶元素(或完全二叉树的根)必为序列中 n 个元素的最小值(或最大值)。例如,下列两个序列为堆,对应的完全二叉树如下图所示。



图 1   大顶堆与小顶堆
    若在输出堆顶的最小值之后,使得剩余 n-1 个元素的序列重新又建成一个堆,则得到n个元素中的次小值。如此反复执行,便能得到一个有序的序列,这个过程称之为堆排序。
     由此,实现堆排序需要解决两个问题:(1)如何由一个无序序列建成一个堆?(2)如何在输出堆顶元素之后,调整剩余元素成为一个新的堆?
     下面先讨论第二个问题。例如,下图 2 中(a)是一个堆,假设输出堆顶元素之后,以堆中最后一个元素替代之,如图(b)所示。此时根节点的左、右子树均为堆,则仅需自上至下进行调整即可。首先以堆顶元素和其左、右子树根结点的值比较之,由于右子树根结点的值小于左子树根结点的值且小于根结点的值,则将 27 和 97 交换之;由于 97 替代了 27 之后破坏了右子树的堆,则需进行和上述相同的调整,直至叶子结点,调整后的状态如图(c)所示,此时堆顶为 n-1 个元素的最小值。重发上述过程,将堆顶元素
27 和堆中最后一个元素 97 交换且调整,得到如图(d)所示新的堆。



图  2     堆的筛选过程
我们称这个自堆顶至叶子的调整过程为“筛选”。
接下来讨论第一个问题:
从一个无序序列建堆的过程就是一个反复“筛选”的过程。若将此序列看成一个完全二叉树,则最后一个非终端结点是第[n/2](向下取整,下同)个元素,由此“筛选“只需要从[n/2]个元素开始,例如,下图(a)中的二叉树表示一个有8个元素的无序序列
{ 49 , 38 , 65 , 97 , 76 , 13 , 27 , 49 }
则筛选从第 4 个元素开始,由于97 > 49 ,则交换之,交换后的序列如图(b)所示,同理,在第 3 个元素 65 被筛选之后序列的状态如图(c)所示。由于第 2 个元素38不大于其左、右子树根的值,则筛选后的序列不变。图(e)所示为筛选根元素 49 之后建成的堆。



图 3   建堆全过程

二、堆排序算法

堆排序的算法如算法1所示,其中筛选的算法如算法0所示。按从大到小排序,因此建堆是建立”大顶堆“:
// 记录的数据结构定义
typedef struct{
KeyType   key;            // 关键字项
InfoType  otherinfo;      // 其他数据项
}ElemType;   //定义待排序记录的类型

typedef struct{
ElemType     *r;            // 存储空间基址
int          length;        // 当前长度
}HeapType;
//    算法 0  筛选算法
void HeapAdjust(HeapType &H, int s, int m){
// 已知H.r[s..m]中记录的关键字除H.r[s].key之外均满足堆的定义,
// 本函数调整H.r[s..m]成为一个大顶堆(对其中记录的关键之而言)
rc = H.r[s];
for ( j=2*s; j<=m; j*=2){
if( j<m && LT(H.r[j].key,H.r[j+1].key) )    ++j;  // j为key值较大的记录的下标
if( !LT(rc.key,H.r[j].key) )	break;          // rc应插入在位置s上
H.r[s] = H.r[j];
s = j;
}
H.r[s] = rc;
} // HeapAdjust
//    算法 1   堆排序算法
void HeapSort( HeapType &H ){
// 对顺序表 H 进行堆排序
for( i=H.length/2; i>0; --i)        // 把H.r[1..H.length]建成大顶堆
HeapAdjust( H, i, H.length );
for ( i=H.length; i>1; --i){
H.r[1] ←→H.r[i];                  // 将堆顶记录和当前未经排序子序列H.r[1..i]
// 中最后一个记录相互交换
HeapAdjust(H, 1, i-1);              // 将H.r[1..i-1]重新调整为大顶堆
}
} // HeapSort


三、堆排序复杂度分析

堆排序方法对记录数较少的文件并不值得提倡,但对 n 较大的文件还是很有效的。因为其运行时间主要耗费在初始堆和建新堆是进行的反复“筛选”上。对深度为 k 的堆,筛选算法中进行的关键字比较次数之多为 2(k-1)次,则在建含 n 个元素,深度为 h 的堆时,总共进行的关键字比较次数不超过 4n 。又,n 个结点的完全二叉树的深度为[㏒n]+1,则调整建新堆时调用HeapAdjust 过程 n-1 次,总共进行的比较次数不超过下式之值,



由此,堆排序在最坏的情况下,其时间复杂度也为O(n㏒n)。相对于快速排序来说,这是堆排序最大的优点。此外,堆排序仅需一个记录大小供交换用的辅助存储空间。

四、堆排序C++代码实现

以数组为排序对象的堆排序C++代码:
#include <iostream>
using namespace std;

void HeapAdjust(int H[], int s, int m){
// 已知H[s..m]中除H[s]之外均满足堆的定义,本函数调整H[s]
// 使H[s..m]成为一个大顶堆
int rc = H[s];
for(int j = 2*s+1; j <= m; j *= 2){
if( j < m && H[j] < H[j+1])
++j;
if(rc >= H[j])
break;
H[s] = H[j];
s = j;
}
H[s] = rc;
}

void HeapSort(int H[], int last){  //last为数组最后一个元素的下标
// 对数组H进行堆排序。
int i;
for(i = last/2; i>= 0; --i){   // 把H数组建成大顶堆
HeapAdjust(H, i, last);
}
for(i = last; i >= 0; --i){
cout << H[0] << " ";       // 输出结果
H[0] = H[i];
HeapAdjust(H, 0, i);
}
}

int main(){
int H[] = {49,38,65,97,76,13,27,49};
HeapSort(H, 7);     // 调用堆排序子程序,参数为数组最后一个元素下标
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐