堆排序

目录

1.堆排序介绍

2.堆排序图文说明

3.堆排序的时间复杂度和稳定性

4.堆排序实现

堆排序介绍

堆排序(Heap Sort)是指利用堆这种数据结构所设计的一种排序算法。

因此,学习堆排序之前,有必要了解堆!若读者不熟悉堆,建议先了解(建议可以通过二叉堆左倾堆斜堆二项堆斐波那契堆等文章进行了解),然后再来学习本章。

我们知道,堆分为"最大堆"和"最小堆"。最大堆通常被用来进行"升序"排序,而最小堆通常被用来进行"降序"排序。

鉴于最大堆和最小堆是对称关系,理解其中一种即可。本文将对最大堆实现的升序排序进行详细说明。


最大堆进行升序排序的基本思想:

① 初始化堆:将数列a[1...n]构造成最大堆。

② 交换数据:将a[1]和a[n]交换,使a[n]是a[1...n]中的最大值;然后将a[1...n-1]重新调整为最大堆。 接着,将a[1]和a[n-1]交换,使a[n-1]是a[1...n-1]中的最大值;然后将a[1...n-2]重新调整为最大值。 依次类推,直到整个数列都是有序的。

下面,通过图文来解析堆排序的实现过程。注意实现中用到了"数组实现的二叉堆的性质"。

在第一个元素的索引为 0 的情形中:

性质一:索引为i的左孩子的索引是 (2*i+1);

性质二:索引为i的左孩子的索引是 (2*i+2);

性质三:索引为i的父结点的索引是 floor((i-1)/2);


1

例如,对于最大堆{110,100,90,40,80,20,60,10,30,50,70}而言:索引为0的左孩子的所有是1;索引为0的右孩子是2;索引为8的父节点是3。

堆排序图文说明


2

heap_sort_asc(a, n)的作用是:对数组a进行升序排序;其中,a是数组,n是数组长度。

heap_sort_asc(a, n)的操作分为两部分:初始化堆 和 交换数据。

maxheap_down(a, start, end)是最大堆的向下调整算法。


下面演示heap_sort_asc(a, n)对a={20,30,90,40,70,110,60,10,100,50,80}, n=11进行堆排序过程。下面是数组a对应的初始化结构:



1 初始化堆

在堆排序算法中,首先要将待排序的数组转化成二叉堆。

下面演示将数组{20,30,90,40,70,110,60,10,100,50,80}转换为最大堆{110,100,90,40,80,20,60,10,30,50,70}的步骤。


1.1 i=11/2-1,即i=4

上面是maxheap_down(a, 4, 9)调整过程。maxheap_down(a, 4, 9)的作用是将a[4...9]进行下调;a[4]的左孩子是a[9],右孩子是a[10]。调整时,选择左右孩子中较大的一个(即a[10])和a[4]交换。


1.2 i=3

上面是maxheap_down(a, 3, 9)调整过程。maxheap_down(a, 3, 9)的作用是将a[3...9]进行下调;a[3]的左孩子是a[7],右孩子是a[8]。调整时,选择左右孩子中较大的一个(即a[8])和a[4]交换。


1.3 i=2

上面是maxheap_down(a, 2, 9)调整过程。maxheap_down(a, 2, 9)的作用是将a[2...9]进行下调;a[2]的左孩子是a[5],右孩子是a[6]。调整时,选择左右孩子中较大的一个(即a[5])和a[2]交换。


1.4 i=1

上面是maxheap_down(a, 1, 9)调整过程。maxheap_down(a, 1, 9)的作用是将a[1...9]进行下调;a[1]的左孩子是a[3],右孩子是a[4]。调整时,选择左右孩子中较大的一个(即a[3])和a[1]交换。交换之后,a[3]为30,它比它的右孩子a[8]要大,接着,再将它们交换。


1.5 i=0

上面是maxheap_down(a, 0, 9)调整过程。maxheap_down(a, 0, 9)的作用是将a[0...9]进行下调;a[0]的左孩子是a[1],右孩子是a[2]。调整时,选择左右孩子中较大的一个(即a[2])和a[0]交换。交换之后,a[2]为20,它比它的左右孩子要大,选择较大的孩子(即左孩子)和a[2]交换。

调整完毕,就得到了最大堆。此时,数组{20,30,90,40,70,110,60,10,100,50,80}也就变成了{110,100,90,40,80,20,60,10,30,50,70}。



第2部分 交换数据

在将数组转换成最大堆之后,接着要进行交换数据,从而使数组成为一个真正的有序数组。

交换数据部分相对比较简单,下面仅仅给出将最大值放在数组末尾的示意图。

上面是当n=10时,交换数据的示意图。

当n=10时,首先交换a[0]和a[10],使得a[10]是a[0...10]之间的最大值;然后,调整a[0...9]使它称为最大堆。交换之后:a[10]是有序的!

当n=9时, 首先交换a[0]和a[9],使得a[9]是a[0...9]之间的最大值;然后,调整a[0...8]使它称为最大堆。交换之后:a[9...10]是有序的!

...

依此类推,直到a[0...10]是有序的。


堆排序的时间复杂度和稳定性

堆排序时间复杂度

堆排序的时间复杂度是O(N*lgN)。

假设被排序的数列中有N个数。遍历一趟的时间复杂度是O(N),需要遍历多少次呢?

堆排序是采用的二叉堆进行排序的,二叉堆就是一棵二叉树,它需要遍历的次数就是二叉树的深度,而根据完全二叉树的定义,它的深度至少是lg(N+1)。最多是多少呢?由于二叉堆是完全二叉树,因此,它的深度最多也不会超过lg(2N)。因此,遍历一趟的时间复杂度是O(N),而遍历次数介于lg(N+1)和lg(2N)之间;因此得出它的时间复杂度是O(N*lgN)。

堆排序稳定性

堆排序是不稳定的算法,它不满足稳定算法的定义。它在交换数据的时候,是比较父结点和子节点之间的数据,所以,即便是存在两个数值相等的兄弟节点,它们的相对顺序在排序也可能发生变化。

算法稳定性 -- 假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的!

堆排序实现

下面给出堆排序的三种实现:C、C++和Java。这三种实现的原理和输出结果都是一样的,每一种实现中都包括了"最大堆对应的升序排列"和"最小堆对应的降序排序"。

堆排序C实现

实现代码(heap_sort.c)

/**

* 堆排序:C 语言

*

* @author skywang

* @date 2014/03/12

*/

#include <stdio.h>

// 数组长度

#define LENGTH(array) ( (sizeof(array)) / (sizeof(array[0])) )

#define swap(a,b) (a^=b,b^=a,a^=b)

/*

* (最大)堆的向下调整算法

*

* 注:数组实现的堆中,第N个节点的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。

*    其中,N为数组下标索引值,如数组中第1个数对应的N为0。

*

* 参数说明:

*    a -- 待排序的数组

*    start -- 被下调节点的起始位置(一般为0,表示从第1个开始)

*    end  -- 截至范围(一般为数组中最后一个元素的索引)

*/

void maxheap_down(int a[], int start, int end)

{

    int c = start;            // 当前(current)节点的位置

    int l = 2*c + 1;        // 左(left)孩子的位置

    int tmp = a[c];            // 当前(current)节点的大小

    for (; l <= end; c=l,l=2*l+1)

    {

        // "l"是左孩子,"l+1"是右孩子

        if ( l < end && a[l] < a[l+1])

            l++;        // 左右两孩子中选择较大者,即m_heap[l+1]

        if (tmp >= a[l])

            break;        // 调整结束

        else            // 交换值

        {

            a[c] = a[l];

            a[l]= tmp;

        }

    }

}

/*

* 堆排序(从小到大)

*

* 参数说明:

*    a -- 待排序的数组

*    n -- 数组的长度

*/

void heap_sort_asc(int a[], int n)

{

    int i;

    // 从(n/2-1) --> 0逐次遍历。遍历之后,得到的数组实际上是一个(最大)二叉堆。

    for (i = n / 2 - 1; i >= 0; i--)

        maxheap_down(a, i, n-1);

    // 从最后一个元素开始对序列进行调整,不断的缩小调整的范围直到第一个元素

    for (i = n - 1; i > 0; i--)

    {

        // 交换a[0]和a[i]。交换后,a[i]是a[0...i]中最大的。

        swap(a[0], a[i]);

        // 调整a[0...i-1],使得a[0...i-1]仍然是一个最大堆。

        // 即,保证a[i-1]是a[0...i-1]中的最大值。

        maxheap_down(a, 0, i-1);

    }

}

/*

* (最小)堆的向下调整算法

*

* 注:数组实现的堆中,第N个节点的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。

*    其中,N为数组下标索引值,如数组中第1个数对应的N为0。

*

* 参数说明:

*    a -- 待排序的数组

*    start -- 被下调节点的起始位置(一般为0,表示从第1个开始)

*    end  -- 截至范围(一般为数组中最后一个元素的索引)

*/

void minheap_down(int a[], int start, int end)

{

    int c = start;            // 当前(current)节点的位置

    int l = 2*c + 1;        // 左(left)孩子的位置

    int tmp = a[c];            // 当前(current)节点的大小

    for (; l <= end; c=l,l=2*l+1)

    {

        // "l"是左孩子,"l+1"是右孩子

        if ( l < end && a[l] > a[l+1])

            l++;        // 左右两孩子中选择较小者

        if (tmp <= a[l])

            break;        // 调整结束

        else            // 交换值

        {

            a[c] = a[l];

            a[l]= tmp;

        }

    }

}

/*

* 堆排序(从大到小)

*

* 参数说明:

*    a -- 待排序的数组

*    n -- 数组的长度

*/

void heap_sort_desc(int a[], int n)

{

    int i;

    // 从(n/2-1) --> 0逐次遍历每。遍历之后,得到的数组实际上是一个最小堆。

    for (i = n / 2 - 1; i >= 0; i--)

        minheap_down(a, i, n-1);

    // 从最后一个元素开始对序列进行调整,不断的缩小调整的范围直到第一个元素

    for (i = n - 1; i > 0; i--)

    {

        // 交换a[0]和a[i]。交换后,a[i]是a[0...i]中最小的。

        swap(a[0], a[i]);

        // 调整a[0...i-1],使得a[0...i-1]仍然是一个最小堆。

        // 即,保证a[i-1]是a[0...i-1]中的最小值。

        minheap_down(a, 0, i-1);

    }

}

void main()

{

    int i;

    int a[] = {20,30,90,40,70,110,60,10,100,50,80};

    int ilen = LENGTH(a);

    printf("before sort:");

    for (i=0; i<ilen; i++)

        printf("%d ", a[i]);

    printf("\n");

    heap_sort_asc(a, ilen);            // 升序排列

    //heap_sort_desc(a, ilen);        // 降序排列

    printf("after  sort:");

    for (i=0; i<ilen; i++)

        printf("%d ", a[i]);

    printf("\n");

}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,864评论 6 494
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,175评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,401评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,170评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,276评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,364评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,401评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,179评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,604评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,902评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,070评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,751评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,380评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,077评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,312评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,924评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,957评论 2 351

推荐阅读更多精彩内容

  • 在C语言中,五种基本数据类型存储空间长度的排列顺序是: A)char B)char=int<=float C)ch...
    夏天再来阅读 3,340评论 0 2
  • 堆是一棵满足一定性质的二叉树,具体的讲堆具有如下性质:父节点的键值总是不大于它的孩子节点的键值(小顶堆), 堆可以...
    9527Roy阅读 618评论 0 0
  • 总结一下常见的排序算法。 排序分内排序和外排序。内排序:指在排序期间数据对象全部存放在内存的排序。外排序:指在排序...
    jiangliang阅读 1,336评论 0 1
  • 如果说 以前 是我的有恃无恐 幼稚自私才让你对我慢慢失望的 现在 我愿意堵上我的未来 希望我们彼此都没看错 亲爱的 晚安
    张喆坤阅读 79评论 0 0
  • 很多时候,当我们心情不好,过得不爽,我们总会鬼使神差的把希望寄托给明天。总会相信明天会更好。 可是,当明天来了,我...
    微群策划阅读 200评论 0 0