• 【排序】堆排序算法


    特别说明:

        对于算法,重在理解其思想、解决问题的方法,思路。因此,以下内容全都假定待排序序列的存储结构为:顺序存储结构。

    完全二叉树概要

        完全二叉树的定义,不知道者请自行查阅相关书籍。由完全二叉树的性质可知,树的高度也就  ,即:从根节点到达任何叶子节点的时间复杂度最多也就  ,完全二叉树的存储结构有多种形式,其中一种便是顺序存储结构,参考图示如下:

        

        假设有如上一棵完全二叉树,则其对应的顺序存储结构为:

        

        注意:上面顺序存储结构的下标是从 0 开始的,之所以这么约定也是方便下文堆排序的介绍!

        对上面图示的说明:

            01.上图中树的每个节点底下都有绿色的编号脚标。如上图树根节点编号为 0;

            02.对应的顺序存储结构的索引下标起始值也是从0开始;

            03.根据完全二叉树的性质可知:任何节点(假设编号为  )的左子节点(如果有的话)的编号(其实也是顺序存储结构的下标)值为:

                左子节点编号 = 

            04.根据完全二叉树的性质可知:任何节点(假设编号为  )的右子节点(如果有的话)的编号(其实也是顺序存储结构的下标)值为:

                右子节点编号 = 

            05.根据完全二叉树性质可知:任何节点(假设编号为  )的父节点(如果有的话)的编号为(其实也是顺序存储结构的下标)值为:

                父节点编号 = 

            06.一棵  个节点的完全二叉树,其叶子节点个数为 ,非叶子节点个数为  ( 其实就是  ) 个

    堆概要

        堆性质(堆定义)

        堆,是一树完全二叉树,但其还满足性质:堆中的任何节点,其(关键字)值必不大于(或不小于)它的任何子节点的(关键字)值。因此,在堆中,最小(或最大)节点必在堆顶节点,即:从一个堆中取最小(或最大)元素的操作时间复杂度为 。但如果堆顶元素被取走后,为维持堆的性质,就必需要对堆进行重新调整,此时可将最后一个叶子节点取出来替补堆顶,并从此时的堆顶向下下溯。因此,取堆顶元素需要的时间复杂度主要耗在堆顶的下溯操作上。由完全二叉树的性质可知,该操作耗时最多为 。如果将 n 个元素构成的堆的堆顶元素全取走,则最多耗时 

        堆分类

        堆分为大根堆与小根堆。当堆中任何节点都不大于其任何子节点时,此种堆为小根堆;当堆中任何节点都不小于其任何子节点时,此种堆为大根堆。

        堆下溯

        特别说明:此处以建大根来说明堆的下溯操作。

        设当前待执行下溯操作的节点为 currNode,且其直接的左、右子节点分别为 LChild、RChild,则该节点的下溯操作:

            01.如果 currNode 为叶子节点,或 currNode 的关键字比 LChild、RChild 的关键字都大,则下溯操作完成;

            02.取出 LChild、RChild 中关键字较大者;

            03.将前一步取出的拥有较大关键字的那个节点与 currNode 节点的数据交换。然后回到 01 点重复执行这些步骤;

        由堆的下溯操作可看出,下溯操作目的是为了保持堆的性质。(如果有不理解的同学,可参照上面的那棵完全二叉树自行推演一遍)

        堆构建

        对于一个顺序存储的序列,如何将其构建成一个二叉堆呢?由前文介绍可知,对堆中的任何节点执行下溯操作即可保持该节点的堆的性质。因此,如果对所有节点都执行一遍下溯操作,则整个序列对应的二叉堆即构建完成。

        设总共节点个数为 ,则构建大根堆(此处就以大根堆构建为例)过程如下:

            01.设置当前正在处理的节点编号  =  ;

            02.对编号为  的节点执行下溯操作;

            03.如果  = 0,则退出,堆构建完成;否则重新设置  =  - 1,重新执行 02 步骤;

        以上便是堆构建过程。这边有同学可能会疑问为什么初始时处理的节点编号是  =  ?而不是  =  ?因为前文在介绍完全二叉树性质时已经说过,对于一棵完全二叉树,其非叶子节点个数为 ,因此,编号最大的非叶子节点编号即为 。而对于所有叶子节点来说,因为它们没有任何子节点,所以它们自己本身就可认为是一个“完整的堆”,所以没必要对它们执行下溯操作,它们即可保持堆的性质。所以刚开始时,当前处理节点可直接设置为  = 

        至此,我们分析一下堆的构建的时间复杂度。由堆构建过程可知,整个构建过程中,时间几乎全耗在了节点的下溯操作上。由于堆的根节点是深度最深的,下溯操作也是最为耗时的,因此,粗略上统计,似乎建堆的时间复杂为应当为  。但经过仔细分析,其实根本用不了这么多时间。因为根节点毕竟只有一个,随着深度层次的加深,越来越多的节点的深度都小于  。

        所以建的时间复杂度应当分别对每个节点进行统计,如下

        设堆的最大层数(即:堆(树)的深度)为  (注意:其实  =  ), 对于第  层(注意:其实   编号为  =  的节点所在的层数)的节点  ,首先将其与左、右子节点比较,找出最大节点,如果最大节点不是节点  ,则将节点  与 该最大节点交换,然后再进行递归类似处理。而这一过程最多只会执行  -  层,每层进行两次比较,总共最多进行  次比较。而第  层的节点数为  ,因此,处理第  层的节点最多需要进行  =  次比较。

        所以整个建堆过程所需要的时间比较总次数为:

        

        将上面 式子2 减去 式子1,得到建时间复杂度为:

        

        进一步整理得

        

        

        因为  = ,所以 ,代入上式得 ,因此建堆时间复杂度为 

    堆排序思想

        01.构建堆。即将待排序序列  重新构建成一个二叉堆(提示:按需要,可构建大根堆或小根堆),具体建堆细节可参阅前文介绍;

        02.取走堆顶,并重新调整堆结构以保持堆性质。调整后的堆暂且称之为新堆;

            说明:其实该步骤可以按如下顺序完成即可

                a) 直接将堆顶元素与此时堆的最后一个叶子节点元素互换位置;

                b) 由于互换位置后,堆顶元素不能保证堆的性质,于是对新生成的堆(即:互换位置后的堆)的堆顶元素执行一次下溯操作即可保证堆的性质;

                c) 由于取走堆顶元素后,新堆的元素个数就减少 1;

        03.如果新堆为空,则排序完成;否则回到 02 步骤执行直到新堆为空为止;

    堆排序分析

        由前文介绍,其实堆排序并不算复杂,就只需要两步即可:建堆 -> 重复执行:取堆顶元素,然后下溯调整以重新保证堆性质,直到所有节点都处理完即可。因此,堆排序的时间复杂度主要是有两部分组成:建堆耗时 + 取堆顶并下溯操作上。建堆耗时前文已详细分析过,其时间复杂度为  。取堆顶元素只有常量耗时,其时间复杂度为  。下溯操作时间复杂度为  ,如果要完成整个排序过程,则至少需要执行  次下溯操作,因此,总的下溯操作的时间复杂度为  。注意:在下溯操作过程中,由于每次堆的元素个数都减少 1,于是下溯操作的耗时其实也是会越来越少的。

        综上所述,堆排序的时间复杂度为:建堆 + (n - 1) 次的取堆顶元素时间 + (n - 1) 次的下溯操作时间 =  +  +  ,所以堆排序(最坏情况下)的时间复杂度为: 。

        由前文介绍可知,堆排序只需要一个额外辅助空间即可。因此,空间复杂度为 

        堆排序是不稳定排序算法,是属于选择类型的排序算法。堆排序算法效率还是很高的,像在 STL 的 Introsort 算法就用到了堆排序,再如像 A* 算法中,也多采纳应用堆排,等等。

    堆排序算法编码实现

        根据堆排序思想,堆排序算法编码参考如下:

      1 // 
      2 // summary     : 以 nParentIdx 为父节点的索引,取得该父节点及其两个(也有可能只有一个或零个)子节点中,关键字最小/大的那个节点的索引.
      3 // in param    : seqlist 序列列表
      4 // in param    : nLen 序列长度
      5 // in param    : nParentIdx 父节点索引.值范围 [0, nLen)
      6 // out param   : nMinIdx 存储关键最小/大的那个节点的索引值.值范围 [0, nLen)
      7 // return      : --
      8 // !!!note       : 01.以下实现均假设一切输入数据都合法.即:内部不对参数全法性进行校验,默认它们全都合法有效.
      9 void getMinIndex(int seqlist[/*nLen*/], const int nLen, const int nParentIdx, int& nMinIdx) {
     10     nMinIdx = nParentIdx;
     11     auto nChildIdx = nParentIdx << 1;
     12     if (++nChildIdx >= nLen) {
     13         return;
     14     } else if (seqlist[nChildIdx] < seqlist[nMinIdx]) {
     15         nMinIdx = nChildIdx;
     16     }
     17     if (++nChildIdx >= nLen) {
     18         return;
     19     } else if (nChildIdx < nLen && seqlist[nChildIdx] < seqlist[nMinIdx]) {
     20         nMinIdx = nChildIdx;
     21     }
     22 }
     23 
     24 void getMaxIndex(int seqlist[/*nLen*/], const int nLen, const int nParentIdx, int& nMaxIdx) {
     25     nMaxIdx = nParentIdx;
     26     auto nChildIdx = nParentIdx << 1;
     27     if (++nChildIdx >= nLen) {
     28         return;
     29     } else if (seqlist[nChildIdx] > seqlist[nMaxIdx]) {
     30         nMaxIdx = nChildIdx;
     31     }
     32     if (++nChildIdx >= nLen) {
     33         return;
     34     } else if (seqlist[nChildIdx] > seqlist[nMaxIdx]) {
     35         nMaxIdx = nChildIdx;
     36     }
     37 }
     38 
     39 // 
     40 // summary     : 交换两个元素.
     41 // in param    : seqlist 序列列表
     42 // in param    : nIndexA 节点A索引.值范围 [0, nLen)
     43 // in param    : nIndexB 节点A索引.值范围 [0, nLen)
     44 // return      : --
     45 // !!!note       : 01.以下实现均假设一切输入数据都合法.即:内部不对参数全法性进行校验,默认它们全都合法有效.
     46 void swapTwoItems(int seqlist[/*nLen*/], const int nIndexA, const int nIndexB) {
     47     const auto nTemp = seqlist[nIndexA];
     48     seqlist[nIndexA] = seqlist[nIndexB];
     49     seqlist[nIndexB] = nTemp;
     50 }
     51 
     52 // 
     53 // summary     : 上溯.
     54 // in param    : seqlist 待排序列表.同时也是排完序列表.
     55 // in param    : nLen 列表长度
     56 // out param   : --
     57 // return      : --
     58 // !!!note       : 01.以下实现均假设一切输入数据都合法.即:内部不对参数全法性进行校验,默认它们全都合法有效.
     59 //               02.其实上溯操作在此暂时是不需要的,但在某些应用中,却是很必要的.只要理解堆排序思想,则上溯
     60 //                  操作的实现将是容易的.(具体可参考如下编码).
     61 // void upAdjustment(int seqlist[/*nLen*/], const int nLen, const int nIndex) {
     62 //     auto nCurrIdx   = nIndex;
     63 //     auto nParentIdx = 0;
     64 //     auto nMinIndex  = 0;
     65 //     while (nCurrIdx > 0) {
     66 //         nParentIdx = (nCurrIdx - 1) >> 1;
     67 //         //getMinIndex(seqlist, nLen, nParentIdx, nMinIndex);
     68 //         getMaxIndex(seqlist, nLen, nParentIdx, nMinIndex);
     69 //         if (nMinIndex == nParentIdx) {
     70 //             break;
     71 //         }
     72 //         swapTwoItems(seqlist, nParentIdx, nMinIndex);
     73 //         nCurrIdx = nParentIdx;
     74 //     }
     75 // }
     76 
     77 // 
     78 // summary     : 下溯.
     79 // in param    : seqlist 待排序列表.同时也是排完序列表.
     80 // in param    : nLen 列表长度
     81 // out param   : --
     82 // return      : --
     83 // !!!note       : 01.以下实现均假设一切输入数据都合法.即:内部不对参数全法性进行校验,默认它们全都合法有效.
     84 void downAdjustment(int seqlist[/*nLen*/], const int nLen, const int nIndex) {
     85     if (nLen <= 1) {
     86         return;
     87     }
     88     const auto nHalfIndex = nLen >> 1;
     89     auto nCurrIdx         = nIndex;
     90     auto nMinIndex        = 0;
     91     while (nCurrIdx < nHalfIndex) {
     92         //getMinIndex(seqlist, nLen, nCurrIdx, nMinIndex);
     93         getMaxIndex(seqlist, nLen, nCurrIdx, nMinIndex);
     94         if (nMinIndex == nCurrIdx) {
     95             break;
     96         }
     97         swapTwoItems(seqlist, nCurrIdx, nMinIndex);
     98         nCurrIdx = nMinIndex;
     99     }
    100 }
    101 
    102 // 
    103 // summary     : 建堆.
    104 // in param    : seqlist 待排序列表.同时也是排完序列表.
    105 // in param    : nLen 列表长度
    106 // out param   : --
    107 // return      : --
    108 // !!!note       : 01.以下实现均假设一切输入数据都合法.即:内部不对参数全法性进行校验,默认它们全都合法有效.
    109 //               02.建堆思路:从seqlist序列的一半位置处一直往前走,对每个元素执行下溯操作,直到走到第一个元素为止.
    110 void make_heap(int seqlist[/*nLen*/], const int nLen) {
    111         if (nLen <= 1) {
    112         return;
    113     }
    114     auto nMinIndex = 0;
    115     for (auto nCurrIdx = (nLen >> 1); nCurrIdx >= 0; --nCurrIdx) {
    116         downAdjustment(seqlist, nLen, nCurrIdx);
    117     }
    118 }
    119 
    120 // 
    121 // summary     : 弹堆顶元素
    122 // in param    : seqlist 待排序列表.同时也是排完序列表.
    123 // in param    : nLen 列表长度
    124 // out param   : --
    125 // return      : --
    126 // !!!note       : 01.以下实现均假设一切输入数据都合法.即:内部不对参数全法性进行校验,默认它们全都合法有效.
    127 void heap_pop(int seqlist[/*nLen*/], const int nLen) {
    128     if (nLen <= 1) {
    129         return;
    130     }
    131     swapTwoItems(seqlist, 0, nLen - 1);
    132     downAdjustment(seqlist, nLen - 1, 0);
    133 }
    134 
    135 // 
    136 // summary     : 堆排序.
    137 // in param    : seqlist 待排序列表.同时也是排完序列表.
    138 // in param    : nLen 列表长度
    139 // out param   : --
    140 // return      : --
    141 // !!!note       : 01.以下实现均假设一切输入数据都合法.即:内部不对参数全法性进行校验,默认它们全都合法有效.
    142 //               02.排序开始前 seqlist 是无序的,排序结束后 seqlist 是有序的.
    143 void heap_sort(int seqlist[/*nLen*/], const int nLen) {
    144     make_heap(seqlist, nLen);
    145     auto nSeqLength = nLen;
    146     while (nSeqLength > 1) {
    147         heap_pop(seqlist, nSeqLength);
    148         --nSeqLength;
    149     }
    150 }
    堆排序算法(C++)编码实现参考

         测试用例参考:

    1     {   // 堆排序
    2         const int nLen    = 10;
    3         int seqlist[nLen] = { 27, 3, 33, 36, 18, 1, 57, 33, 10, 88 };
    4         print(seqlist, nLen);
    5         heap_sort(seqlist, nLen);
    6         print(seqlist, nLen);
    7         printf("------------------------------------------------------------------------
    
    ");
    8     }
    堆排序算法(C++)测试用例编码参考
  • 相关阅读:
    学习笔记——Maven 如何处理传递性依赖
    gitlab 部署
    jenkins部署
    python 使用json格式转换
    php加载memcache
    centos6.5 apache+tomcat结合 负载均衡
    linux部署tomcat
    linux安装java环境
    kickstart安装
    cobbler部署
  • 原文地址:https://www.cnblogs.com/tongy0/p/5738339.html
Copyright © 2020-2023  润新知