选择排序之堆排序

一 堆排序的定义

堆的定义,n个关键字序列L[1...n]称为堆,当且仅当序列满足:

1. L[i]≥L[2i] 且 L[i]≥L[2i+1](大根堆)

2. L[i]≤L[2i]且L[i]≤L[2i+1] (小根堆)

i需要满足的特点为(1≤i≤⌊n/2⌋)

可以将一维数组视为一棵完全二叉树,满足条件1的堆称为大根堆,大根堆的最大元素存放在根结点,且其任一非根结点的值小于等于其双亲结点值。满足条件2的堆称为小根堆,小根堆的根节点是最小值,且其任一非根结点的值大于等于其双亲结点值。

大根堆:

                                          

对于所有具有双亲结点含义编号从大到小(⌊n/2⌋~1)做出如下调整:

  1. 若孩子结点皆小于双亲结点,则该结点的调整结束。
  2. 若存在孩子结点大于双亲结点,则将最大的孩子结点与双亲结点交换,并该孩子结点进行同样的交换,知道孩子结点为叶子结点为止。

二 堆排序思路

堆排序首先将存放在L[1...n]中的n个元素建成初始堆,由于堆本身的特点(以大根堆为例),堆顶元素就是最大值。输出堆顶元素后,通常将堆底元素送入堆顶,此时根结点已不满足大根堆的性质,堆被破坏,将堆顶元素向下调整令其继续保持大顶堆的性质,再输出堆顶元素。如此重复,直到堆中仅剩一个元素为止。

堆排序需要解决的问题:

  1. 如何将无序序列构造成初始堆。
  2. 输出堆顶元素后,如何将剩下元素调整成新的堆。

堆排序的关键是构造初始堆,n个结点的完全二叉树,最后的一个结点是第n/2个结点的孩子结点。对第n/2个结点为根的子树筛选(对于大根堆,若根结点的关键字小于左右孩子中关键字较大值,则交换),使得该子树成为堆。之后向前依次对各结点([n/2] - 1 ~ 1)为根的子树进行筛选,看该结点是否大于其左右子节点的值,若不大于,则将左右子结点中的较大值与之交换,交换后可能会破坏下一级的堆,于是继续采用上述方法构造下一级的堆。直到以该结点为根的子树构成堆为止。反复利用上述调整堆的方法建堆,直到根结点。

如图:初始时调整L[4]子树,将09与32交换,交换后满足堆的定义;向前继续调整L[3]子树,78小于左右孩子的较大者87,交换,交换后满足堆的定义;向前继续调整L[2]子树,17小于左右孩子的较大者45,交换后满足堆的定义;向前调整至根结点L[1],53 < 左右孩子的较大者87,交换,交换后破坏了L[3]子树的堆,采用上述方法堆L[3]进行调整,53 < 左右孩子的较大者78,交换,至此该完全二叉树满足堆的定义。

输出堆顶元素后,将堆的最后一个元素与堆顶元素交换,此时堆的性质被破坏,需要向下进行筛选。将09和左右孩子的较大者78进行交换,交换后破坏了L[3]子树的堆,继续对L[3]子树向下筛选,将09和左右孩子较大者65交换,交换后得到新堆。

三 堆排序的算法代码

#include <stdio.h>
#include <stdlib.h>
 
void swap(int* a, int* b)
{
    int temp = *b;
    *b = *a;
    *a = temp;
}
 
void max_heapify(int arr[], int start, int end) 
{
    //建立父节点指标和子节点指标
    int dad = start;
    int son = dad * 2 + 1;
    while (son <= end)  //若子节点指标在范围内才做比较
        {
            if (son + 1 <= end && arr[son] < arr[son + 1]) 
            //先比较两个子节点大小,选择最大的
            son++;
	    if (arr[dad] > arr[son]) //如果父节点大于子节点代表调整完毕,直接跳出函数
            return;
            else{  //否则交换父子内容再继续子节点和孙节点比较
            swap(&arr[dad], &arr[son]);
            dad = son;
            son = dad * 2 + 1;
        }
    }
}
 
void heap_sort(int arr[], int len) 
{
    int i;
    //初始化,i从最后一个父节点开始调整
    for (i = len / 2 - 1; i >= 0; i--)
        max_heapify(arr, i, len - 1);
    //先将第一个元素和已排好元素的前一位做交换,再重新调整,直到排序完毕
    for (i = len - 1; i > 0; i--) 
    {
        swap(&arr[0], &arr[i]);
        max_heapify(arr, 0, i - 1);
    }
}
 
int main() {
    int arr[] = {49,38,65,97,76,13,27,49};
    int len = (int) sizeof(arr) / sizeof(*arr);
    heap_sort(arr, len);
    int i;
    for (i = 0; i < len; i++)
        printf("%d ", arr[i]);
    printf("\n");
    return 0;
}

代码结果:

        

四 堆排序性能分析

算法最好时间最坏时间平均时间额外空间/空间复杂度稳定性
堆排序O(nlog2^n)O(nlog2^n)O(nlog2^n)O(1)不稳定

空间效率:仅使用常数个辅助单元,所以空间时间复杂度为O(1)。

时间效率:建堆的时间为O(n),之后有n-1此向下调整操作,每次调整的时间复杂度为O(log2^n),故在最好,最坏和平均情况下,堆排序的时间复杂度为O(nlog2^n)。

稳定性:进行筛选时,有可能把后面相同关键字的元素调整到前面,所以堆排序算法是一种不稳定的排序方法。表L={1,2,2}

构造初始堆时可能将2交换到堆顶,此时L={2,1,2},最终排序序列为L={1,2,2},由此可知,堆排序是一种不稳定的排序

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值