HeapSort堆排序详解

本文我们来学习下堆排序的实现原理,堆排序顾名思义就是利用了堆的特点来实现排序,首先了解堆是什么?

堆相关的一些概念

二叉树

二叉树是每个节点最多有两个子树的树结构。也就是一个节点只能分俩叉。

QQ图片20180119133858.png

完全二叉树

若二叉树的层数为h,除第 h 层外,h层以上的每个节点都挂满两个子节点。


QQ图片20180119133953.png

堆,是完全二叉树,分为大根堆和小根堆。大根堆的要求是每个节点的值都不大于其父节点的值,在数组的非降序排序中,需要使用的就是大根堆,因为根据大根堆的要求可知,最大的值一定在堆顶。

生成最大堆

生成初始堆(数组)
堆排序是依赖数组来实现的,所以首要的就是把数组和堆结合起来,比如说我们有一棵如下的二叉树:

bf7fc550-cf95-39b0-9015-ddbd8dbe4180.jpg

从上图中,我们发现,如果我们从根结点开始按照从左到右一层一层的编号的话,对这些元素的访问就构成了一个序列。比如上图中的序列按照编号顺序如下:16, 14, 10, 8, 7, 9, 3, 2, 4, 1
如果我们将这种从二叉树的结点关系转换成对应的数组形式的话,则对应的数组如下图:
d2f4c4bc-4fbf-3dff-92db-7ee06dec7bbf.jpg

从二叉树的每个结点的编码到它的左右字结点的关系,我们发现一个规律:

左子结点的数组索引号= 父结点索引号 * 2
右子结点的数组索引号=父结点索引号 * 2 + 1

这样,我们通过一定的运算对应关系将二叉树关系的元素存储到一个数组中。在实现的时候考虑到我们的数组下标是从0开始的,对应的关系为:
下标为i的左子节点下标为

public static int left(int i)  
{  
    return i * 2 + 1;  
}  

右子节点下标为

public static int right(int i)  
{  
    return i * 2 + 2;  
}  

转化为最大堆
最大堆的要求是每个节点的值都大于它的两个子节点的值,一个最简单的办法就是从最低层的结点开始起一个一个的调整大小关系。

如果我们从a[a.length -1]这样的叶子结点来调整的话,有相当一部分结点是没必要的。因为这些叶结点根本就没有子结点,连找子结点和去比较的必要都没有了。所以,我们可以从最后面往前到过来去找那些有子结点的结点,然后从这些结点开始一个个的进行堆调整。那么,我们该从哪个结点开始起进行调整呢?另外,我们可能还有这么一个疑问,为什么我们要从后面往前去调整?从前面往后调整不行吗?

两个疑问,让我们一个个的来分析。
首先第一个问题,从哪个结点开始进行调整。我们来看这棵二叉树,

e600beee-1b69-3cac-a7ab-7d52d97b12fd.jpg

很显然,它最后的一个元素也肯定就是最终的一个叶结点。那么取它的父结点应该就是有子结点的最大号的元素了。那么从它开始就是最合适的。取它的父结点可以通过一个简单的i / 2来得到,i为当前结点的下标。
然后我们再来看第二个问题,为什么要从后往前而不是从前往后。这个相对也比较好理解。我们从下面的层开始调整,保证当上面的父结点来调整的时候,下面的子树已经满足最大堆的条件了。这样出现不符合条件的父结点只需要用前面的maxheapify过程就可以。而从前面往后调整呢,我们看下面的一个示例:

e600beee-1b69-3cac-a7ab-7d52d97b12fd (1).jpg

如果我们从根结点开始,根结点元素4比它的两个子结点都大,不需要调整。而再往后面的时候它的子结点1调整之后被换成16.这样就出现了它的子结点比它还要大的情况,因此从前往后这么调整的过程不行。
经过前面的讨论,构建最大堆的过程就相当的简单了:

public static void buildMaxHeap(int[] a)  
{  
    for(int i = a.length / 2; i >= 0; i--)  
        maxHeapify(a, i);  
}  

其中maxHeapify(a, i)方法的含义是调整第i个节点使其满足最大堆的要求。

maxHeapify调整逻辑

我们maxHeapify方法就是让父结点的值必须比它的子结点要大。笼统的说,就是将这个不符合条件的结点和子结点进行比较,通过交换将最大的结点作为父结点:

  1. 比较当前结点和它的子结点,如果当前结点小于它的任何一个子结点,则和最大的那个子结点交换。否则,当前过程结束。
  2. 在交换到新位置的结点重复步骤1,直到叶结点。
public static void maxHeapify(int[] a, int i)  
{  
    int l = left(i);  
    int r = right(i);  
    int largest = i;  
  
    if(l < a.length && a[l] > a[i])  
        largest = l;  
    if(r < a.length && a[r] > a[largest])  
        largest = r;  
    if(i != largest)  
    {  
        swap(a, i, largest);  
        maxHeapify(a, largest);  
    }  
} 

堆排序

既然建了一个最大堆,能够保证最大的元素就是根结点,那么,我们如果要从小到大排序的话,最大的元素就只要取根结点就可以了。如果我们把根结点拿走了,放到结果集的最末一个元素,接着就应该找第二大的元素。通过前面的maxHeapify过程,如果我们从集合的最低一层叶结点来取,然后放到根结点进行调整的话,肯定也是可以得到剩下元素里面的最大结点的。因此我们可以得到这么一个过程:

1. 取最大堆的根结点元素。
2. 取集合最末尾的元素,放到根结点,调用maxHeapify进行调整。重复步骤1.

public static void heapSort(int[] a)  
{  
    if(a == null || a.length <= 1)  
        return;  
  
    buildMaxHeap(a);  
    int length = a.length;  
    for(int i = a.length - 1; i > 0; i--)  
    {  
        swap(a, i, 0);  
        length--;  
        maxHeapify(a, 0, length);  
    }  
}  

完整实例代码:

public class HeapSort {
    private static int[] sort = new int[] { 1, 0, 10, 20, 3, 5, 6, 4, 9, 8, 12, 17, 34, 11 };

    public static void main(String[] args) {
        buildMaxHeapify(sort);
        heapSort(sort);
        print(sort);
    }

    private static void buildMaxHeapify(int[] data) {
        // 没有子节点的才需要创建最大堆,从最后一个的父节点开始
        int startIndex = getParentIndex(data.length - 1);
        // 从尾端开始创建最大堆,每次都是正确的堆
        for (int i = startIndex; i >= 0; i--) {
            maxHeapify(data, data.length, i);
        }
    }

    /**
     * 创建最大堆
     *
     * @paramdata
     * @paramheapSize需要创建最大堆的大小,一般在sort的时候用到,因为最多值放在末尾,末尾就不再归入最大堆了
     * @paramindex当前需要创建最大堆的位置
     */
    private static void maxHeapify(int[] data, int heapSize, int index) {
        // 当前点与左右子节点比较
        int left = getChildLeftIndex(index);
        int right = getChildRightIndex(index);

        int largest = index;
        if (left < heapSize && data[index] < data[left]) {
            largest = left;
        }
        if (right < heapSize && data[largest] < data[right]) {
            largest = right;
        }
        // 得到最大值后可能需要交换,如果交换了,其子节点可能就不是最大堆了,需要重新调整
        if (largest != index) {
            int temp = data[index];
            data[index] = data[largest];
            data[largest] = temp;
            maxHeapify(data, heapSize, largest);
        }
    }

    /**
     * 排序,最大值放在末尾,data虽然是最大堆,在排序后就成了递增的
     *
     * @paramdata
     */
    private static void heapSort(int[] data) {
        // 末尾与头交换,交换后调整最大堆
        for (int i = data.length - 1; i > 0; i--) {
            print(data);
            int temp = data[0];
            data[0] = data[i];
            data[i] = temp;
            maxHeapify(data, i, 0);
        }
    }

    /**
     * 父节点位置
     *
     * @paramcurrent
     * @return
     */
    private static int getParentIndex(int current) {
        return (current - 1) >> 1;
    }

    /**
     * 左子节点position注意括号,加法优先级更高
     *
     * @paramcurrent
     * @return
     */
    private static int getChildLeftIndex(int current) {
        return (current << 1) + 1;
    }

    /**
     * 右子节点position
     *
     * @paramcurrent
     * @return
     */
    private static int getChildRightIndex(int current) {
        return (current << 1) + 2;
    }

    private static void print(int[] data) {
        int pre = -2;
        for (int i = 0; i < data.length; i++) {
            if (pre < (int) getLog(i + 1)) {
                pre = (int) getLog(i + 1);
                System.out.println();
            }
            System.out.print(data[i] + "|");
        }
    }

    /**
     * 以2为底的对数
     *
     * @paramparam
     * @return
     */
    private static double getLog(double param) {
        return Math.log(param) / Math.log(2);
    }
}

总结

堆排序粗看起来有好多个步骤,又是要建堆又是要调整的显得很复杂。其实它的过程概括起来无非就是这么两个步骤,一个就是建一个堆,然后就是每次取走根结点的元素用后面的来补,补上后进行调整,然后再重复前面的步骤。

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

推荐阅读更多精彩内容

  • 关于最大堆 什么是最大堆和最小堆?最大(小)堆是指在树中,存在一个结点而且该结点有儿子结点,该结点的data域值都...
    凌云壮志几多愁阅读 87,850评论 33 71
  • B树的定义 一棵m阶的B树满足下列条件: 树中每个结点至多有m个孩子。 除根结点和叶子结点外,其它每个结点至少有m...
    文档随手记阅读 13,149评论 0 25
  • 第一章 绪论 什么是数据结构? 数据结构的定义:数据结构是相互之间存在一种或多种特定关系的数据元素的集合。 第二章...
    SeanCheney阅读 5,735评论 0 19
  • 概述:排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部...
    每天刷两次牙阅读 3,727评论 0 15
  • 身边有没有这样一个人,他让你觉得你们的关系不止这样,却又只能这样。你们隔着几十条公路,几条山脉,几百公里,几片海...
    私塾在读阅读 718评论 4 4