堆和堆排序

1. 堆的基础知识

1.1 什么是堆

堆是一种特殊的二叉树,它需要满足如下两个条件

  1. 堆是一颗完全二叉树
  2. 堆中每个节点的值都大于或等于(小于或等于)它的子节点的值

我们把每个节点都大于或等于它的子节点的值的对叫做大顶堆;每个节点的值都小于等于它的子节点的堆叫做小顶堆

上面图中这几颗树,第1个和第2个是大顶堆,第3个是小顶堆,第4个不是堆,它不是一颗完全二叉树

1.2 堆的存储

根据堆是一颗完全二叉树的特性,所以我们可以使用数组来存储堆。

假如我们从数组的下标1开始存储,则如图的这样一个堆对应的存储如图中数组

从数组下标为1的位置开始存储,完全二叉树的节点关系

  1. 下标为i的节点的左孩子的下标就是i*2
  2. 下标为i的节点的右孩子的下标就是i*2+1

根据完全二叉树的特性,最后一个非叶子节点是第n/2个节点,这里从数组下标1开始存储,所以最后一个非叶子节点对应的下标就是n/2。第1到第n/2个节点是非叶子结点,第n/2+1到第n个节点是叶子结点。

1.3 堆的常见操作

堆中常见的操作有:插入一个元素删除堆顶元素

1.3.1 插入一个元素

插入一个元素可以将要插入的元素放到堆的最后面,然后对这个元素采用自下而上的方式来进行堆化

将插入的元素放到堆的最后的示意图

插入一个元素的堆化过程就是沿着节点所在的路径,自下而上和父节点比较大小,如果父子节点之间不满足大小关系,则交换两个节点的位置,然后继续拿着插入节点和新的父节点比较,直到满足大小关系或者插入节点已经是堆顶元素了

插入元素的堆化过程示意图

1.3.2 删除堆顶元素

删除堆顶元素,我们采用使用堆中最后一个元素来替代堆顶元素,然后对新的堆顶元素自顶向下的堆化的方式来实现

删除堆顶元素的过程(我们以维护一个大顶堆来讲解):第一步,拿堆中最后一个元素替换掉堆顶元素;第二步:比较新的堆顶元素和它的左右孩子中值较大的孩子之间的大小,如果不满足大小关系,则交换堆顶元素与值较大的孩子之间的位置,然后拿着刚才那个堆顶元素和它新的左右孩子中的值较大的孩子比较,重复这个过程,直到满足大小关系或者新的堆顶元素已经交换到了叶子节点的位置

删除堆顶元素的堆化过程示意图

1.4 如何创建一个堆

创建一个堆有两种思路

  1. 第一种是借助前面讲的,在堆中插入一个元素的思路。我们假设起始堆中只包含一个数据,就是下标为1的数据,然后我们调用前面讲的插入操作,将下标为2到n的数据依次插入到堆中。这样我们就将包含n个数据的数组,组织成了堆。
  2. 第二种实现思路,跟第一种截然相反。第一种建堆思路的处理过程是从前往后处理数据,并且每个数据插入堆中时,都是从下往上堆化。第二种实现思路,是从后往前处理数据,并且每个数据都是从上往下堆化。具体实现就是从最后一个非叶子节点开始,依次堆化每个非叶子节点,每个节点都从上往下完成堆化

下面我们看一个堆化过程演示,我们依次对8,19,5,7四个非叶子节点进行了从上到下的堆化


堆化过程示意图

代码实现

public static void createHeap(int[] a, int n) {
    /**
     * 构建过程:从最后一个非叶子节点开始,从后往前,对每个节点进行自上而下的堆化
     */
    for (int i = n / 2; i >= 1; i--) {
        heapify(a, n, i);
    }
}

/**
 * 堆化根节点:找出当前节点的左右孩子中较大的一个,和根节点比较,符合顺序则结束;不符合顺序,则交换并继续比较
 *
 * @param a
 * @param n
 * @param i
 */
private static void heapify(int[] a, int n, int i) {
    int current = i;
    while (true) {
        int maxPos = -1;
        int left = current * 2;//左孩子的位置
        int right = left + 1;//右孩子的位置
        if (right <= n) {
            if (a[left] < a[right]) {
                maxPos = right;
            } else {
                maxPos = left;
            }
        } else if (left <= n) {
            maxPos = left;
        }
        if (maxPos == -1) {//没有子孩子
            break;
        }
        if (a[current] >= a[maxPos]) {//根节点比子孩子大
            break;
        } else {//和子孩子交换位置,继续比较
            int temp = a[maxPos];
            a[maxPos] = a[current];
            a[current] = temp;
            current = maxPos;
        }
    }
}

2. 堆的应用:堆排序

2.1 如何利用堆进行排序

假如我们要进行从小到大的排序,那我们先将数据建立一个大顶堆,这样建堆完成之后,数组中的数据就已经是按照大顶堆的特性来组织的了。数组中的第一个元素就是堆顶元素,也就是最大的元素。我们把它和最后一个元素交换,那最大元素就放到了下标为n的位置。

这个过程有点类似于“删除堆顶元素”的操作,当堆顶元素移除之后,我们把下标为n的元素放到堆顶,然后再通过堆化的方法,将剩下的n-1个元素重新构建成堆。堆化完成之后,我们再取堆顶元素和第n-1个元素交换位置,然后再将前面的n-2个元素堆化,一直重复这个过程,直到最后堆中只剩下下标为1的一个元素,排序工作就完成了。

排序过程演示图

代码实现

public static void sort(int[] a, int n) {
    //将堆顶元素和最后一个元素交互位置,然后自上而下堆化前面n-1个元素;重复这个过程直到堆中只有一个元素
    if (n == 1) {
        return;
    }
    createHeap(a, n);
    for (int i = n; i >= 2; i--) {
        int temp = a[i];
        a[i] = a[1];
        a[1] = temp;
        heapify(a, i - 1, 1);
    }
}

2.2 堆排序的性能分析

  1. 创建堆的性能分析
  2. 删除堆顶元素的性能分析

2.2.1 创建堆的性能分析

堆化过程中,我们只需要对第1到n/2个节点进行堆化。而每个节点堆化的过程中,需要比较和交互的节点个数,跟这个节点的高度K成正比

下图表示了每一层的节点个数和对
应的高度。我们只需要将每个节点的高度求和,得出的就是建堆的时间复杂度。


节点层次和高度示意图

我们将每个非叶子节点的高度求和,就是下面的这个公式


将公式左右两侧都乘以2,得到另外一个公司S2,然后将用S2-S1,可以得到S


S的中间部分是一个等比数列,所以最后可以用等比数列求和公式来计算,最终的结果就是下面图中画的这个样子。


因为h=log2(n),代入公式S,就得到了S=O(n),所以,建堆的时间复杂度就是O(n)

2.2.2 删除堆顶元素的性能分析

删除的算法复杂度推算过程和建堆的复杂度推算过程类似,最后计算出来的复杂度是O(nlogn)。


所以堆排序总的时间复杂度是O(nLogn).

源码

完整的源码和测试用例,请查看github之堆和堆排序

说明

文中图片来源:极客时间,王争《数据结构与算法之美》

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