堆排序

堆排序是一种时间复杂度为O(nlog2n)的排序算法,是基于堆这种数据结构实现的。因此,我们需要先了解什么叫堆,然后才能够更清楚地知道堆排序。

堆的相关知识

1. 堆的定义

以下堆的定义摘自《数据结构-C语言描述》(第三版)西安电子科技大学出版社。

一个大小为n的堆(heap)是一颗包含n个节点的完全二叉树,该树中每个节点的关键字值大于等于双亲节点的关键字值。完全二叉树的根称为堆顶,它的关键字值是最小的,这样定义的堆称为最小堆(MinHeap)。我们可以用类似的方式定义最大堆(MaxHeap)。

由于堆是一个完全二叉树,因此,当它采用顺序方式存储时,存在以下关系:(最小堆)kik2ikik2i1

下图是一个堆的最小堆和最大堆以及顺序存储的例子。


图1 堆的例子
(a)最小堆; (b)最大堆; (c)最小堆与最大堆的顺序存储

2. 堆的建立

1. 堆的存储结构

由于我们使用的事顺序方式存储,因此在建堆之间必须分配好它的空间大小(MaxSize),即这个堆最多需要存储多少元素。

由图1我们可以看出,我们的堆是从下标为1开始存储的,因此,我们在规划存储空间的时候需要按最多元素加1来规划。

    #define MaxSize 100   //根据需要修改

接下来考虑堆的结构类型。一个堆的结构里面必然需要包含需要建堆的元素,我们用顺序方式存储,因此需要包含一个元素的数组,然后还需要有一个值来统计该堆中有几个元素,所以给出如下的结构类型:

    typedef struct minheap{
    int Size;
    T Elements[MaxSize];
    }MinHeap;

需要注意的是,建堆的元素一定要是能够比较大小的类型,否则堆将没办法建立。

2. 向下调整运算

接下来我们来了解一下堆建立的过程——向下调整运算。现在以最小堆来说明一下向下调整运算。

若有一组数(a[i+1],a[i+2],…,a[n]),满足最小堆的关系a[j]≤a[2j]且a[j]≤a[2j+1](若a[2j]或a[2j+1]不存在,则说明这个数是二叉树的最后一层),添加一个数a[i]后,通过位置调整让这组数(a[i],a[i+1],a[i+2],…,a[n])任然满足最小堆的关系,调整方法是使用向下调整法。

具体的调整过程如下:对于新插入的数temp,情况1:若其不大于它的左右两个孩子(即啊a[i]=temp,a[2i]≥a[i],a[2i+1]≥a[i]),则这个数直接插入在当前位置,调整结束;情况2:若temp大于它的左孩子或者右孩子,则将这个数与其左右孩子中较小的那个交换位置,继续向下比较,直到满足情况1或者到达堆底为止。下图是一个向下调整法的例子。


图2 向下调整运算举例

如图2(a)所示,其中,temp=46是插入的元素,比较它的左右孩子,发现都比它小,所以跟其较小的孩子(18)交换,得到图2(b);交换完成后,再次比较temp与它的左右孩子,然后交换结果,得到如图2(c)结果。此时到达堆底,调整结束。

3. 建堆运算

从2.2中我们可以看出,对于一个满足最小堆关系的元素序列,在其前端插入一个数后,通过向下调整运算,可以得到一个新的满足最小堆关系的元素序列。因此,对于一个任意次序排列的元素序列,理论上我们从最后一个数开始,依次递减1对其进行向下调整运算,即可得到我们需要构建的一个最小堆(最大堆同理)。

但我们看到,对于有n个元素(含第0位空位)的序列,堆底必然有[n/2]个元素,这些元素进行向下调整运算时将直接结束,因此,我们不需要对其进行该运算。所以我们的建堆运算应该从第[n/2-1]位开始,依次递减1,直到下标为1的元素调整完成后,建堆结束。下面的表格表示的是一个通过向下调整运算建堆的过程。

表1 向下调整运算建堆过程

表中黄色表示当前插入的元素temp,灰色的两个是表示通过向下调整运算后交换的两个元素。

3. 向下调整运算和建堆运算的代码

向下调整运算中,需要传递的参数有需要运算的数组h[],待排元素的下标号i以及序列的长度n。具体实现如下:

void AdjustDown(T h[],int i,int n){
    int child=2*i;//取得待排序元素的左孩子下标
    T temp=h[i];//得到待排序元素值
    while(child<=n){//当到达堆底时退出
        if(childh[child+1])child++;//得到左右孩子中较小孩子的下标
        if(temp

建堆运算中需要传递的参数是待建堆的序列h,具体代码如下:

void CreatHeap(MinHeap* h){
    int n=h->Size;//得到序列长度,该长度包含下标为0的空位
    for(int i=n/2-1;i>0;i--){//从i=n/2开始进行向下调整运算,直到i=1为止
        AdjustDown(h->Elements,i,n);
    }
}

通过以上几个步骤即可完成最小堆的建立。对于最大堆的建立,只需要修改向下调整运算中的比较关系即可得到。下图是在Visual Studio 2010中运行得到的上面表格例子的结果。

图3 最小堆建立例子结果图

堆排序

有了上述的知识后,实现堆排序是一个非常简单的过程,具体思路如下:
(1)使用n个元素,建立一个最小堆(或者是最大堆);
(2)该最小堆(或最大堆)的堆顶元素(h[ 1 ])是这个序列的最小(或最大)元素;
(3)将堆顶元素(h[ 1 ])与该序列最后一个元素(h[n])交换,然后将交换后的堆顶元素与前n-1的元素进行向下调整运算,进行重排,得到一个新的最小(或最大堆);
(4)重复第(3)步,直至最后一个元素的下标为1为止,堆排序即可完成。

具体实现代码如下:

void HeapSort(MinHeap* h){
    int n=h->Size;
    T temp;
    CreatHeap(h);//建堆
    for(int i=n-1;i>0;i--){
        temp=h->Elements[i];  //首位交换
        h->Elements[i]=h->Elements[1];
        h->Elements[1]=temp;
        AdjustDown(h->Elements,1,i-1);//剩余元素重新建堆
    }
}

图3的例子数据通过堆排序运行结果如下图所示。

图4 堆排序运行结果图

从结果我们可以看出,排序结果是降序排列的,如果要让结果升序排列有两种方法:第一种方法是在上述基础上将数组直接进行逆序;第二种方法是建立最大堆,然后进行堆排序。(建立最大堆只需要修改向下调整运算代码即可。)

堆排序分析

从之前的内容我们知道,堆排序是利用最小堆(或最大堆)堆顶元素的关键字最小(或最大)的特征,来简单地从无序的序列中得到最小(或最大)的元素。

现在对堆排序的时间复杂度和空间复杂度进行分析。

1. 时间复杂度与空间复杂度

堆排序的过程包括堆的建立以及反复的向下调整运算(堆的重建)两个步骤,因此,时间也主要耗费在这两个步骤上。

设树高为h,则h=log2n+1,每次对堆顶元素调用向下调整运算时,需要比较的次数至多为2(h1)(每层至多比较左右两个孩子,共2次,第一层不需要比较,所以共需要比较h1层),交换元素至多h次。因此,

2[log2(n1)+log2(n2)+...+log22]<2nlog2n

堆排序在最坏情况下,时间复杂度为O(nlog2n)

在空间复杂度方面,整个过程只用了一个中间变量进行临时存储,因此,其空间复杂度为O(1)

2. 堆排序的特点

堆排序算法具有以下几个特点:
(1)堆排序是一种不稳定的排序方法;
(2)只能用于顺序结构,不能用于链式结构;
(3)堆排序适合用于记录较多的排序。由于初始建堆所需的比较次数较多,因此记录数较少时不宜采用。堆排序在最坏情况下,时间复杂度为O(nlog2n),相对于快速排序最坏情况O(n2)要好,记录较多时,其排序效率较高。

你可能感兴趣的:(堆排序)