排序算法之堆排序
2014-05-18 17:45
225 查看
堆排序只需要一个记录大小的辅助空间,每个待排序的记录仅占有一个存储空间。
若将和此数组序列对应的以为数组(即以一维数组作此序列的存储结构)看成是一个完全二叉树,则堆的含义表明,完全二叉树中所有非终端结点的值均不大于(或不小于)其左、右孩子结点的值。由此,若序列 { 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 建堆全过程
由此,堆排序在最坏的情况下,其时间复杂度也为O(n㏒n)。相对于快速排序来说,这是堆排序最大的优点。此外,堆排序仅需一个记录大小供交换用的辅助存储空间。
一、堆的定义及建堆完整过程
堆的定义如下: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; }
相关文章推荐
- 排序算法之堆排序
- 排序算法(1):插入排序,选择排序,希尔排序,堆排序
- 各种排序算法实现——基数排序、归并排序、插入排序、冒泡排序、选择排序、快速排序、堆排序、希尔排序
- 经典排序算法 - 堆排序Heap sort
- 几种常用的排序算法的分析及java实现(希尔排序,堆排序,归并排序,快速排序,选择排序,插入排序,冒泡排序)
- 排序算法之四--堆排序
- 排序算法之堆排序 Heap Sort
- python排序算法-冒泡排序,选择排序,直接插入排序,希尔排序,归并排序,快速排序,堆排序
- 八种常见排序算法:插入、冒泡、选择、希尔、归并、快排、堆排序、基数排序
- 常见的五类排序算法图解和实现(选择类:简单选择排序,锦标赛排序,树形选择排序,堆排序)
- 排序算法之堆排序
- 排序算法七:选择排序之堆排序
- 排序算法复习(Java实现)(二): 归并排序,堆排序,桶式排序,基数排序
- 堆排序----排序算法
- 笔试面试最常涉及到的12种排序算法(包括插入排序、二分插入排序、希尔排序、选择排序、冒泡排序、鸡尾酒排序、快速排序、堆排序、归并排序、桶排序、计数排序和基数排序)进行了详解。每一种算法都有基本介绍、算
- 排序算法2--简单选择排序、堆排序
- 排序算法2--简单选择排序、堆排序
- PHP排序算法之堆排序(Heap Sort)实例详解
- Java排序算法(三):堆排序
- 排序算法Java实现——选择排序(堆排序)