您的位置:首页 > 其它

【选择排序】堆排序

2015-12-16 16:08 197 查看
堆排序(Heap Sort)

从简单选择排序可见,选择排序的主要操作是进行关键字间的比较,因此改进简单选择排序应从如何减少“比较”出发考虑。显然,在n个关键字中选出最小值,至少进行n-1次比较,然而,进行在剩余的n-1个关键字中选择次小值并一定要进行n-2次比较,若能利用前n-1次比较所得信息,则可减少以后各趟排序中所用的比较次数。

堆排序就是基于这种思想,对简单选择排序进行了改进。堆排序是一种树形选择排序,在排序过程中,将待排序的记录r[1..n]看成是一棵完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系,在当前无序的序列中选择关键字最大(或最小)的记录。

首先给出堆的定义。n个元素的序列{k1,k2,..,kn}称之为堆,当且仅当满足以下条件时:
(1) k(i) >= k(2i)且k(i) >= k(2i+1) 或 (2) k(i) <= k(2i)且k(i) <= k(2i+1)

若将和此序列对应的一维数组(即以一维数组做此序列的存储结构)看成一个完全二叉树,则堆实质上是满足如下性质的完全二叉树:树中所有非终端结点的值均不大于(或不小于)其左、右孩子结点的值。

例如,关键字序列{96,83,27,38,11,09}和{12,,36,24,85,47,30,53,91}分别满足条件(1)和条件(2),故他们均为堆,对应的完全二叉树分别为下图所示。显然,在这两种堆中,堆顶元素(或完全二叉树的根)必须为序列中n个元素的最大值(或最小值),分别称之为大根堆和小根堆。



堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得当前无序的序列中选择关键最大(或最小)的记录变得简单。下面讨论用大根堆进行排序,堆排序的思想如下:

1)按堆的定义将待排序序列r[1..n]调整为大根堆(这个过程称为建初堆),交换r[1]和r
,则r
为关键字最大的记录。

2)将r[1..n-1]重新调整为堆,交换r[1]和r[n-1],则r[n-1]为关键字次大的记录。

3)循环n-1次,直到交换了r[1]和r[2]为止,得到一个非递减的有序序列r[1..n]。

同样,可以通过构造小根堆得到一个非递增的有序序列。由此,实现堆排序需要解决如下两个问题:

1)建初堆:如何将一个无序序列建成一个堆?

2)调整堆:去掉堆顶元素,在堆顶元素改变之后,如何调整剩余元素成为一个新的堆?

因为建初堆要用到调整堆的操作,所以先讨论调整堆的实现。

1.调整堆

先看一个例子,图(a)是一个堆,将堆顶元素97和堆中最后一个元素交换后,如图(b)所示。由于此时除根结点外,其余结点均满足堆的性质,由此仅需要自上至下进行一条路径上的结点调整即可。首先以堆顶元素38和其左右子树根结点的值进行比较,由于左子树根结点的值大于右子树根结点的值且大于根结点的值,则将38和76交换;由于38代替了76之后破坏了左子树的“堆”,则需进行上述相同的调整,直至叶子结点,调整后的状态如图(c)所示。重复上述过程,将堆顶元素76和堆中最后一个元素27交换且调整,得到图(d)所示的新的堆。



上述过程就像过筛子一样,把较小的关键字逐层筛下去,而较大的关键字逐层选上来。因此,称此方法为“筛选法”。假设r[s+1..m]已经是堆的情况下,按“筛选法”将r[s..m]调整为以r[s]为根的堆,算法实现如下。

筛选法调整堆

[算法思想]

从r[2s]和r[2s+1]中选出关键字较大者,假设r[2s]的关键字较大,比较r[s]和r[2s]的关键字。

1)若r[s]>=r[2s],说明r[s]为根的子树已经是堆,不必做任何调整。

2)若r[s]<r[s2],交换r[s]和r[2s]。交换后,以r[2s+1]为根的子树仍是堆,如果以r[2s]为根的子树不是堆,则重复上述过程,将以r[2s]为根的子树调整为堆,直至进行到叶子结点为止。

[算法描述]

void HeapAdjust(int r[],int s,int m)
{
/* 假设r[s+1..m]已经是堆,将r[s..m]调整为以r[s]为根的大根堆 */
int rc=r[s];
for(int j=2*s;j<=m;j*=2)     /* 沿key较大的孩子结点向下筛选 */
{
if(j<m&&r[j]<r[j+1]) ++j;       /* j为key较大的记录的下标 */
if(rc>=r[j]) break;             /* rc应插入到位置s上 */
r[s]=r[j];s=j;                  /* 重复上述过程 */
}
r[s]=rc;                            /* 插入 */
}

2.初建堆

要建立一个无序序列调整为堆,就必须将其所对应的完全二叉树中以每一结点的子树都调整为堆,显然,只有一个结点的树必为堆,而在完全二叉树中,所有序号大于[n/2]的结点都是叶子,因此这些结点为根的子树均已是堆,这样,只需利用筛选法,从最后一个分支点[n/2],依次将序号为[n/2]、[n/2]-1...、1的结点左为根的子树都调整为堆即可。

初建堆

[算法思想]

对于无序序列r[1..n],从i=n/2开始,反复调用筛选法HeapAdjust(r,i,n),依次将r[i],r[i-1],...,r[1]为根的子树调整为堆。

[算法描述]

void CreatHeap(int r[],int Lenght)
{
/* 把无序序列r[1..n]建成大根堆 */
int n=Lenght-1;
for(int i=n/2;i>0;--i) /* 反复调用HeapAdjust */
{
HeapAdjust(r,i,n);
}
}
例 已知无序序列为{49,38,65,97,76,13,27,49},用“筛选法”将其调整为一个大根堆,给出建堆的过程。

从下图(a)所示的无序序列的最后一个非终端结点开始筛选,即从第4个元素97开始,由于97>49,则无需交换。同理,第3个元素65不小于其左、右子树根的值,仍无序交换。而第2个元素38<97,被筛选之后的序列的状态如图(b)所示,然后对根元素49筛选之后得到图(c)所示的大根堆。



3.堆排序算法的实现

根据前面堆排序算法思想的描述,可知堆排序就是将无序序列建成初堆以后,反复进行交换和堆调整。在建初堆和调整堆算法实现的基础上,下面给出堆排序算法的实现。

堆排序

[算法描述]

void HeapSort(int r[],int Lenght)
{
/* 对顺序表r进行堆排序 */
CreatHeap(r,Lenght);           /* 把无序序列r[1..Lenght-1]建成大根堆 */
int x;
for(int i=Lenght-1;i>1;--i)
{
x=r[1];                    /* 将堆顶记录和当前未经排序子序列r[1..i]中最后一个记录互换 */
r[1]=r[i];
r[i]=x;
HeapAdjust(r,1,i-1); 		/* 将r[1..i-1]重新调整为大根堆 */
}
}

例 已知待排序记录的关键字序列为{49,38,65,97,76,13,27,49},给出堆排序法进行排序的过程。

首先将无序序列建初堆,过程如上图3所示。在初始大根堆的基础上,反复交换堆顶元素和最后一个元素,然后重新调整堆,直至最后得到一个有序序列,整个堆排序过程如下图4所示。





[算法分析]

1.时间复杂度


堆排序的运行时间主要耗费在建初堆和调整堆时的反复“筛选”上。

设有n个记录的初始序列所对应的完全二叉树的深度为h,建初堆时,每个非终端结点都要自上而下进行筛选。由于第i层上的结点数小于2^(i-1),且第i层结点最大下移的深度为h-i,每下移一层要做两次比较,所以建初堆时关键字总的比较次数为
(推理)<=4n。

调整建新推时要做n-1次“筛选”,每次“筛选”都要将根结点下移到合适的位置。n个结点的完全二叉树深度

[log2(n)]+1,则重建堆时关键字总的比较次数不超过 2([log2(n-1)]+[log2(n-2)+...+log2(2)])<2n([log2(n)])

由此,堆排序在最坏情况下,其时间复杂度也为O(nlog2(n))。实验研究表明,平均性能接近于最坏性能。

2.空间复杂度

仅需一个记录大小供交换用的辅助存储空间,所以空间复杂度为O(1)。

[算法特点]

1)是不稳定排序

2)只能用于顺序结构,不能用于链式结构。

3)初始建堆所需的比较次数较多,因此记录数少时不宜采用。堆排序在最坏情况下时间复杂度为O(nlog2(n)),相对于快速排序最坏情况下的O(n^2)而言是一个优点,当记录较多时较为高效。

[完整代码]

#include<iostream>
using namespace std;
void HeapAdjust(int r[],int s,int m) { /* 假设r[s+1..m]已经是堆,将r[s..m]调整为以r[s]为根的大根堆 */ int rc=r[s]; for(int j=2*s;j<=m;j*=2) /* 沿key较大的孩子结点向下筛选 */ { if(j<m&&r[j]<r[j+1]) ++j; /* j为key较大的记录的下标 */ if(rc>=r[j]) break; /* rc应插入到位置s上 */ r[s]=r[j];s=j; /* 重复上述过程 */ } r[s]=rc; /* 插入 */ }
void CreatHeap(int r[],int Lenght)
{
/* 把无序序列r[1..n]建成大根堆 */
int n=Lenght-1;
for(int i=n/2;i>0;--i) /* 反复调用HeapAdjust */
{
HeapAdjust(r,i,n);
}
}
void HeapSort(int r[],int Lenght)
{
/* 对顺序表r进行堆排序 */
CreatHeap(r,Lenght); /* 把无序序列r[1..Lenght-1]建成大根堆 */
int x;
for(int i=Lenght-1;i>1;--i)
{
x=r[1]; /* 将堆顶记录和当前未经排序子序列r[1..i]中最后一个记录互换 */
r[1]=r[i];
r[i]=x;
HeapAdjust(r,1,i-1); /* 将r[1..i-1]重新调整为大根堆 */
}}
int main()
{
int a[9]={0,49,38,65,97,76,13,27,49}; /* 从a[1]开始 */
HeapSort(a,9);
for(int i=1;i<9;i++)
cout<<a[i]<<" ";
cout<<endl;
return 0;
}
[运行结果]

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: