二叉堆

二叉堆

  二叉堆是一种特殊的堆,其实质是完全二叉树。二叉堆有两种:最大堆和最小堆。最大堆是指父节点键值总是大于或等于任何一个子节点的键值。而最小堆恰恰相反,指的是父节点键值总是小于任何一个子节点的键值。如“图1 最大堆”、“图2 最小堆”所示:


图1 最大堆

图2 最小堆

堆(heap),这里所说的堆是数据结构中的堆,而不是内存模型中的堆。常见的堆有二叉堆、左倾堆、斜堆、二项堆、斐波那契堆等等。

数据存储

与二叉树不同的是二叉堆的数据是顺序存储,而不是链式存储。如“图3 二叉堆数据的存储方式”所示:


图3 二叉堆数据的存储方式

操作

对于二叉堆,介绍以下几种操作:
插入节点;
上浮节点;
删除节点;
下沉节点;
构建二叉堆;

1.插入节点
在插入数据的时候,每插入的数据都是插入到二叉堆的最后一个位置,以最大堆为例,如“图4 插入数据”所示:

图4 插入数据

上图描述的是插入整数10,可以发现,在第二层的父节点5小于新插入的子节点10,这可怎么办?这不符合最大堆的性质呀!于是就有了上浮节点操作。

2.上浮节点
插入新数据后(子节点10),我们让子节点10与其父节点5作比较,父节点小于子节点,于是将子节点上浮与父节点转换,如“图5 子节点上浮-01”所示:

图5 子节点上浮-01

接着再做同样的操作,子节点10与父节点8作比较,同样需要上浮,如“图6 子节点上浮-02”所示:


图6 子节点上浮-02

再同样做同样操作,如“图7 子节点上浮-03”所示,直至父节点大于子节点或者子节点已经上浮到根节点即止。


图7 子节点上浮-03

3.删除节点
在删除节点的时候,每次都是删除根节点,如“图8 删除节点”所示:

图8 删除节点

在删除根节点之后,我们不可能让根节点位置无主吧?于是将二叉堆最后一个数据填充至根节点,如“图9 填充根节点”所示:


图9 填充根节点

但是,我们又可以发现,填充上来的根节点比它的子节点小,这也不符合最大堆的性质呀!于是就有了下沉节点操作。

4.下沉节点
填充数据(父节点5)之后,我们让父节点5与其两个子节点9、7作比较,看谁更大,父节点就往哪下沉,如“图10 下沉父节点-01”所示:

图10 下沉父节点-01

接着再做同样的操作,父节点5与其两个子节点6、8作比较,再下沉。最终发现两个子节点都小于父节点,于是停止下沉(如果父节点下沉至叶节点位置时,也停止下沉)。如“图10 下沉父节点-02”所示:
图10 下沉父节点-02

5.构建 二叉堆
构建二叉堆,也就是把一个无序的完全二叉树调整为二叉堆,本质上就是让所有非叶子节点依次下沉。
比如此时有一个无序的二叉树,如“图11 无序二叉树”所示:

图11 无序二叉树

首先从最后一个非叶子结点8开始,让其与两个子节点11、6作比较,然后做下沉节点操作,如“图12 节点下沉-01”所示:


图12 节点下沉-01

接着是节点2,做同样的操作,如“图13 节点下沉-02”所示:


图13 节点下沉-02

然后是节点5,如“图14 节点下沉-03”所示:


图14 节点下沉-03

最后是节点12,因为该节点都比其两个子节点大,所以无需下沉。
最终,一颗无序的二叉树就构建成了一个最大堆了,如“图15 构建后的最大堆”所示:


图15 构建后的最大堆

时间复杂度

因为二叉堆是一棵完全二叉树,所以对于一个节点数为n的堆,它的高度不会超过log2n,因此二叉堆上浮和下沉的时间复杂度都为log2(n)。

代码实现

在代码实现之前,先说明一下二叉堆在处理的过程中是如何计算下标的,如“图16 下标获取”所示:


图16 下标获取

如上图所示,假设父节点的下标 parent=0,则其左子节点下标为Lchildren=2*parent+1,右子节点下标为Rchildren=2*parent+2,如上示为例:
第 0 个数据的下标:parent = 0
第 1 个数据的下标:Lchildren = 2*parent + 1 = 2*0+1 = 1
第 2 个数据的下标:Rchildren = 2*parent + 2 = 2*0+2 = 2

代码

package Queue;

import java.util.Arrays;

public class BinaryHeap {
    /**
     * 上浮
     * @param array 数据数组
     */
    public static void upAdjust(int[] array) {
        // 先求出父子节点的下标
        int childrenIndex = array.length - 1;
        int parentIndex = (childrenIndex - 1) / 2;
        // 记录子节点数据,用于最后赋值
        int temp = array[childrenIndex];
        // 开始上浮
        while (childrenIndex > 0 && temp > array[parentIndex]) {
            // 直接单向赋值,无需做交换操作
            array[childrenIndex] = array[parentIndex];
            // 更新父子节点下标的值,下面两句代码顺序不可相反
            childrenIndex = parentIndex;
            parentIndex = (parentIndex - 1) / 2;
        }
        // 最后赋值
        array[childrenIndex] = temp;
    }

    /**
     * 下沉节点
     * @param index 要下浮的节点的下标
     * @param array 数据数组
     */
    public static void downAdjust(int index, int[] array) {
        // 先记录父节点及左子节点的下标
        int parentIndex = index;
        int childrenIndex = 2 * parentIndex + 1;
        // 记录父节点的值,用于最后赋值
        int temp = array[parentIndex];
        // 若有左子节点则继续
        while (childrenIndex <= array.length - 1) {
            // 若有右子节点,且右子节点比左子节点大,则将 childrenIndex 记录为右子节点的下标
            if (childrenIndex + 1 <= array.length - 1 && array[childrenIndex + 1] > array[childrenIndex]) {
                childrenIndex++;
            }
            // 如果子节点大于父节点,则无需下沉,直接返回
            if (temp >= array[childrenIndex]) {
                break;
            }
            // 直接单向赋值,无需做交替操作
            array[parentIndex] = array[childrenIndex];
            // 更新父子节点下标的值,下面两句代码顺序不可相反
            parentIndex = childrenIndex;
            childrenIndex = 2 * childrenIndex + 1;
        }
        // 最后赋值
        array[parentIndex] = temp;
    }

    /**
     * 构建二叉堆
     * @param array 数据数组
     */
    public static void  buildBinaryHeap(int[] array) {
        for (int i = (array.length/2)-1; i >= 0; i--) {
            downAdjust(i, array);
        }
    }

    public static void main(String[] args) {
        // 构建二叉堆
        int[] arr01 = {5, 3, 6, 9, 8, 6, 7, 2, 4, 6, 3};
        buildBinaryHeap(arr01);
        System.out.println(Arrays.toString(arr01));
        // 添加一个数据,测试上浮操作
        int[] arr02 = {9, 8, 7, 4, 6, 6, 6, 2, 3, 5, 3, 20};
        upAdjust(arr02);
        System.out.println(Arrays.toString(arr02));
        // 删除一个数据,册数下沉操作
        int[] arr03 = {3, 8, 7, 4, 6, 6, 6, 2, 3, 5};
        downAdjust(0, arr03);
        System.out.println(Arrays.toString(arr03));
    }
}

总结

1.二叉堆的核心代码是上述的upAdjust()、downAdjust()函数,在实现时要注意 while 循环条件判断时是判断childrenIndex以及在更新父子节点下标的值顺序不可倒置;
2.二叉堆是优先队列的理论基石。理解了二叉堆之后,优先队列就很简单了。

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

推荐阅读更多精彩内容