< 排序大全系列 > 堆排序总结

< 排序大全系列 > 堆排序总结


直观动图理解:

堆排序

算法知识导引:

  1. 什么是 “堆” ?

    堆是一种近似完全二叉树的结构,通常堆是通过 一维数组 来实现的。

    二叉树我们在学离散数学的时候都见过,那具体什么是 完全二叉树 呢?这个二叉树应该满足一下两个条件:

    1. 假设整个二叉树深度为 n,那么除了第 n 层及其树叶,其他各层的结点都达到了最大个数,有 2 个分叉
    2. 且第 n 层的树叶全部集中在左侧

从上到下以从大到小的关系形成的树可以叫做 最大堆,反之就叫做 最小堆

注意:以最大堆为例并不代表层数更高,数字就一定更大,二叉堆只需要满足父结点比子结点大就可以了。

完全二叉树如下图所示:

完全二叉树图示
  1. 那为什么可以用一维数组来表示,而不是单独写一个 完全二叉树类呢?

    是因为完全二叉树有一些独特的性质:

    就如上图所示,我们的圈圈里现在还没有装数据,现在圈里的数字是序号。像这样从上到下,从左到右地依次给完全二叉树编号后,我们就可以根据 编号与结点 之间的微妙关系得出一些结论:

    1. n 号结点的下属左结点的序号是 2n
    2. n 号结点的下属右结点的序号是 2n+1
    3. n 号结点(只要它有父结点)的父结点序号为 n/2 ,注意是计算机的整数除法

    我们通常从一个数组的 [ 1 ] 位置开始填入值而不是 [ 0 ]

    接下来我们以最大堆为例,做一个 C++ 的代码实现:

    #include <iostream>
    #include <algorithm>
    #include <cmath>
    #include <ctime>
    
    using namespace std;
    
    template<typename Item>
    class MaxHeap{
    private:
     Item *data = NULL;
     int count;          /* 有多少个有效的值 */
     
    public:
     MaxHeap(int capacity){   /* capacity 容量 */
         data = new Item[ capacity+1 ];
         /* +1 是因为是从 [1] 开始存放的,浪费了一个小格间,要满足原有那么多数据的需求,就得 +1 */
     }
     
        ~MaxHeap(){
         delete [] data;
         /* 析构函数里,完成空间的清理 */
        }
        
        int size(){
         return count;
        }
        
        bool isEmpty(){
         return count == 0;
        }
    }
    
    int main(){
     MaxHeap<int> maxheap = MaxHeap<int>(100);
     /* 一定记得这里填入的 100 代表的是空间大小!*/
     cout<<  maxheap.size() << endl;
     return 0;
    }
    
  2. 那么一个二叉堆需要包含什么操作呢?

    当有一个新的元素需要填入的时候,我们需要 SHIFT UP 操作:

    private:
     void shiftUp( int k ){
            while( k > 1 && data[k/2] < data[k] ){
                /*  k = 1 时就是根结点了,已经没有父结点可以判断了。*/
                /*  第二个判断是 比较父结点是否小于子结点,如果是,那么就把大的向上推,小的换下来。*/
                swap( data[k/2], data[k]);                 /* swap()大家都会写,就不再赘述 */
                k /= 2;
            }
     }
    
     /* 之所以把 shiftUp 写成是 private ,是因为用户没有必要调用该函数。*/
    
    public:
     void insert( Item item ){
         data[count+1] = item;  /* 在数组末尾,添加进新的一个元素*/
            ++ count;
     }
    

    当我们需要取出一个元素的时候,我们必须要保证取出后,堆依然满足自身需要的那些条件,所以需要 SHIFT DOWN 操作:

    /*   格式基本同上:*/
    private:
        void shitDown(){
            while( 2*k <= count ){
                int j = 2*k;   // 在此轮循环中,data[k] 和 data[j]交换位置
                if( j+1 <= count && data[j+1] > data[j])
                    j += 1;
                
                if( data[k] >= data[j] ){
                    break;
                }
                
                swap( data[k], data[j] );
                k = j;
            }
        }
    
    public:
        Item extractMax(){
         assert( count > 0 );
            
            Item ret = data[1];       /* 把此时的顶部元素保存下来 作为返回值*/
            
            swap( data[1] , data[count] );
            count --;
            shiftDown(1);
            /* 用最末的元素来 shiftDown 一遍整个二叉堆,达到维护的效果。*/
            
            return ret;
        }
    

那么接下来,我们就要正式开始实现 堆排序 了:我们给出了两种实现:

  1. 第一种是将 所需要排序的 arr[] 数组的元素 通过 insert() 函数一个一个填入堆中:

    代码如下:

    template<typename>
    void heapSort(T arr[], int n){
        
        MaxHeap<T> maxheap = MaxHeap<T>(n);
        for( int i =0; i<n ; i ++){
         maxheap.insert(arr[i]);
        }
        
        for( int i = n-1; i>=0 ; i--){
         /* 这里展示的是 从小到大的顺序,当然如果想改为从大到小的话,本来每次 extractMax() 就是取出最大值,那么改为 初始化 i = 0 就好了*/
         arr[i] = maxheap.extractMax();
        }
    }
    
  2. 第二种,是在构造函数层面完成的,将 arr[] 数组传入 MaxHeap 类中

    代码如下:

    首先先需要在原 MaxHeap 类内添加一个新的构造函数:
    public:
        MaxHeap(Item arr[], int n){
         data = new Item[ n+1 ];
            capacity = n;
            for( int i=0; i<n ; i++ )
                data[i+1] = arr[i];
            count = n;
            
            for( int i = count/2 ; i>=1 ; i-- ){
                shiftDown(i);
            }
        }
    

    我们是从最后一个叶子节点开始进行 shiftDown 操作,而在实际情况中,其实真正移动了的元素并不多,这样大大提高了效率,比一个一个地插入要好很多。

    template<typename>
    void heapifySort(T arr[], int n){
        
        MaxHeap<T> maxheap = MaxHeap<T>(arr, n);  
        for( int i = n-1; i>=0 ; i--){
         arr[i] = maxheap.extractMax();
        }
    }
    

下面我们来看看经过测试后的结果:

Test For Radom Array, size = 1000000, radom range [0, 1000000]:
heapSort Time: 0.616283s 
heapify  Time: 0.573522s
--------------------------------
Test For Radom Nearly Ordered Array, size = 1000000, radom range [0, 1000000]:
heapSort Time: 0.620693s 
heapify  Time: 0.341337s
--------------------------------
Test For Radom Array, size = 1000000, radom range [0, 10]:
heapSort Time: 0.361128s 
heapify  Time: 0.322639s
--------------------------------

Heapify 的算法复杂度比较:

将n个元素逐个插入到一个空堆中,算法复杂度是 O( nlogn )

heapifySort 的过程,算法复杂度是 O( n )


值得改进的地方 —— 原地堆排序

我们上面的两个算法都有一个问题,那就是都需要新开辟一个数组,然后有序地填入值来形成一个有序数组。

可是这样的算法显然是不够好的,能不能就在原地空间内,将数据整合有序呢?

思路是这样的:

  1. 通过刚才新的 MaxHeap 的构造函数,可以让一个数组成为一个最大堆,那么假如有一个 max 指针,指向的一定是该数组的第一项。

    Step 1

    此时我们把它移到最末尾,那么此时 上图中的 v 找到了最合适的位置,因为它是最大值,所以放在了最末尾。

    Step 2

    但此时,w已经不是最大值,前面从 [ 0 ] 到 倒数第二个 元素之间,就不再是一个最大堆 Max Heap 了。所以我们对 w 执行一次 shiftDown 操作,使得橙色部分重新成为一个 最大堆。

    Step 3

    那么第一个元素又是该最大堆中相对的最大值了,所以继续甩到末尾排列。

    Step 4

如此递归往复,就可以在原有数组内,完成原地的堆排序。


各项指标:

分类 -------------- 内部比较排序
数据结构 ---------- 数组
最差时间复杂度 ---- O(nlogn)
最优时间复杂度 ---- O(nlogn)
平均时间复杂度 ---- O(nlogn)
所需辅助空间 ------ O(1)
稳定性 ------------ 不稳定

堆排序是不稳定的排序算法,不稳定发生在堆顶元素与A[i]交换的时刻。

比如序列:{ 9, 5, 7, 5 },堆顶元素是9,堆排序下一步将9和第二个5进行交换,得到序列 { 5, 5, 7, 9 },再进行堆调整得到{ 7, 5, 5, 9 },重复之前的操作最后得到{ 5, 5, 7, 9 }从而改变了两个5的相对次序。


参考资料:

  1. https://www.bilibili.com/video/av23849333/?p=1

    数据结构与算法之堆(完整版) (主要是 p1p6 )Blibili uploader:Python空间

  2. https://www.cnblogs.com/eniac12/p/5329396.html#s4

    CSDN精品博客文章:常用排序算法总结(一) Posted on 2016-03-28 22:13

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

推荐阅读更多精彩内容

  • 各校历年复试机试试题 清华、北大、华科试题详细笔记部分,少笔记部分与少数leetcode【含个人整理笔记】 一、详...
    十里江城阅读 1,183评论 0 1
  • 在C语言中,五种基本数据类型存储空间长度的排列顺序是: A)char B)char=int<=float C)ch...
    夏天再来阅读 3,340评论 0 2
  • //可以获取到具体的设备版本(已更新到iPhone 6s、iPhone 6s Plus),缺点是:采用的硬编码。具...
    猛大不萌阅读 664评论 0 0
  • 我和爸爸很像,又很不像。 像的地方 都爱看书,喜欢文史类而不是理科类,数字感觉一般。这可从某次冬天,妈妈在家买了几...
    怡儿话书影阅读 660评论 0 0
  • 清晨,自动醒来模式。 10天读书精读营已结束了。每天早上起床完成打卡,生活有了目标,按照计划表逐步完成...
    青果1阅读 305评论 1 1