Java8 HashMap的红黑树实现

红黑树

网络上有很多关于红黑树的详细介绍,推荐参考《算法导论》中红黑树一章,讲解的最为清晰和准确。下面我将结合《算法导论》的红黑树介绍与Java8 HashMap红黑树实现,抽取重点信息,尽可能简单的把红黑树的要点介绍出来,许多细节不进行详细说明,如果全部写出来估计与书也差不多。不再赘述,鼓励参阅《算法导论》与Java8 HashMap源代码。

红黑树介绍

红黑树性质(重点)

  1. 每个结点或是红色的,或是黑色的。
  2. 根结点是黑色的。
  3. 每个叶子结点是黑色的。
  4. 如果一个结点是红色的,则它的两个子结点都是黑色的。
  5. 对每个结点,从该结点到其所有后代叶子结点的简单路径上,均包含有相同数目的黑色结点。

红黑树的黑高:
从某结点x出发(不包含该结点)到达一个叶子结点任意一条简单路径上的黑色结点个数称为该结点的黑高(black-height)。

红黑树旋转

一种能保证二叉搜索树性质的搜索树局部操作,分为左旋(left rotate)右旋(right rotate)
如下图摘自《算法导论》,展示左旋和右旋的操作。

左旋和右旋

红黑树插入

插入过程

简单分成3步:

  • 确定插入位置,新结点作为叶子结点,按照二叉搜索树的性质向下循环寻找结点将要插入的位置。
  • 标色,新结点标记为红色
  • 修复红黑树性质。
如何修复红黑树性质

首先,分析在叶子结点处添加一个红结点,可能破坏那些红黑树性质。如下所示,如果插入的结点是根结点,破坏了性质2;如果插入结点的父结点为红色,则破坏了性质4。

1.每个结点或是红色的,或是黑色的。√
2.根结点是黑色的。×
3.每个叶子结点是黑色的。√
4.如果一个结点是红色的,则它的两个子结点都是黑色的。×
5.对每个结点,从该结点到其所有后代叶子结点的简单路径上,均包含有相同数目的黑色结点。√

其次,如果插入的结点是根结点,将结点颜色设置为黑色即可。
次之,如果破坏了性质4,会分成如下3中情况解决,假设插入的结点为z,并且z的父结点是其祖父结点的左孩子(如果是右孩子操作与左孩子相反)。

  • 情况1:z的叔结点y是红色。
    操作方法:父结点和叔结点着黑色,祖父结点着红色,z结点上移到祖父结点,循环继续遍历。


    算法导论13-5图
  • 情况2:z的叔结点y是黑色的且z是一个右孩子
    操作方法:z的父结点左旋变成情况3
  • 情况3:z的叔结点y是黑色的且z是一个左孩子
    操作方法:z的父结点着黑色,祖父结点着红色,z的祖父结点右旋,结束调整。


    算法导论13-6图

红黑树删除

假设将要删除的结点为z,实际删除的结点为y,初始时y=z,进行红黑树性质修复的结点为x。

删除结点z,寻找需要修复的结点x

红黑树的删除操作比插入更复杂一些

  1. 找到替换z的结点y
    • 如果z左子树为null,z的右孩子zr不为null,移到z位置上,x=zr,记录y.color=z.color
    • 如果z右子树为null,z的左孩子zl不为null,移到z位置上,x=zl,记录y.color=z.color
    • z左右子树均不为null,存在后继结点,y设置为z的中序遍历的后继结点,记录后继结点y的y.color,x=y.right,y替换z,z的颜色复制给y。
    • z左右子树均为null,x=null,记录y.color=z.color
  2. 此时,y.color是实际删除结点的颜色,也就是这个颜色可能破坏了红黑树的性质。
  3. x指向需要调整的结点。
删除结点可能破坏的性质
  • 实际删除结点y的颜色y.color如果是红色不会破坏红黑树性质。
  • y的颜色y.color是黑色破坏红黑树性质。
    1. 如果y是原来的根结点,而y的一个红孩子成为新的根结点,违反了性质2;
    2. x和x.p是红色的,违反了性质4;
    3. 在树中移动y导致先前包含y的任何简单路径上黑结点个数少1,违反性质5。
x结点的颜色

如果y是黑色,而x结点可能有三种情况,null,RED和BLACK。为了保证红黑树性质,将x结点视为还有一重额外的黑色,这样任意包含x的路径黑结点个数加1。这样旧解决了上面提到的可能破坏的性质。但是因为此时x结点包含两种颜色,违反了性质1。x结点可能的情况是,null和BLACK、RED和BLACK、BLACK和BLACK。

恢复红黑树性质

x是双重颜色状态
目的将额外的黑色沿树上移,直到

  • x指向红黑结点,将x着色为(单个)黑色
  • x指向根结点,此时可以简单的移除额外的黑色
  • 执行适当的旋转和重新着色

结束程序

x结点操作可能情况
  • 情况1:x的兄弟结点w是红色
    操作方法:设x.p是x的父结点。改变w和x.p的颜色,然后对x.p做一次左旋。转换为情况2、情况3或情况4处理。
  • 情况2:x的兄弟结点w是黑色的,而且w的两个子结点都是黑色的
    操作方法:设x.p是x的父结点。x和w去掉一重黑色,x.p上补偿一重黑色。x上移。
  • 情况3:x的兄弟结点w是黑色的,w的左孩子是红色的,w的右孩子是黑色的
    操作方法:交换w和其左孩子w.left的颜色,然后对w进行右旋。转换为情况4。
  • 情况4:x的兄弟结点w是黑色的,且w的右孩子是红色的
    操作方法:重新着色和对xp做一次左旋。符合红黑树性质,结束程序。
    如图所示:


    算法导论13-7图

HashMap红黑树源码

源码实现的每条语句不详细注释,根据前文所述的内容,在代码处只标示每段执行的含义,并且会做一些简化处理,仅粘贴重要的代码。例如某几条语句标注,执行情况3;标注左旋操作,但是不对左旋操作的代码详细标注。有兴趣的同学可以在纸上按照代码画一画,看看各个指针如何转换的,理解起来就会非常容易。

树化函数

final void treeifyBin(Node<K,V>[] tab, int hash) {
    ......
    TreeNode<K,V> hd = null;
    do {
        TreeNode<K,V> p = replacementTreeNode(e, null);//生成红黑树结点
        ......
    } while ((e = e.next) != null);
    if ((tab[index] = hd) != null)
        hd.treeify(tab);//桶的头节点,调用TreeNode结构的树化函数treeify()
}

/*链表结构转换为红黑树结构*/
final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null;//定义根结点
    for (TreeNode<K,V> x = this, next; x != null; x = next) {
        next = (TreeNode<K,V>)x.next;
        x.left = x.right = null;
        /*设置链表的第一个结点为树根*/
        if (root == null) {
            x.parent = null;
            x.red = false;
            root = x;
        }
        else {
            /*循环查找插入的位置,代码有删减*/
            for (TreeNode<K,V> p = root;;) {
                /*先比较hash值大小*/
                /*如果上一步相等,再比较Comparable接口的compareTo*/
                /*如果上一步相等或者无法比较,最后调用System.identityHashCode()函数比较*/
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                else if ((kc == null && (kc = comparableClassFor(k)) == null) 
                          || (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);
                ......
                root = balanceInsertion(root, x);//插入新结点
             }
        }
    }
}

红黑树插入函数

static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) {
    /*新结点标记为红色*/
    x.red = true;
    for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
        if ((xp = x.parent) == null) {//根结点处理
            x.red = false;
            return x;
        }
        else if (!xp.red || (xpp = xp.parent) == null)//x的父结点为黑色
            return root;
        /*x的父结点为祖父结点的左孩子*/
        if (xp == (xppl = xpp.left)) {
            /*情况1:x的叔结点y是红色*/
            if ((xppr = xpp.right) != null && xppr.red) {
                xppr.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
             }
             else {
                 /*情况2:x的叔结点y是黑色的且x是一个右孩子*/
                 if (x == xp.right) {
                     root = rotateLeft(root, x = xp);
                     xpp = (xp = x.parent) == null ? null : xp.parent;
                  }
                  /*情况3:x的叔结点y是黑色的且x是一个左孩子*/
                  if (xp != null) {
                      xp.red = false;
                      if (xpp != null) {
                          xpp.red = true;
                          root = rotateRight(root, xpp);
                      }
                  }
              }
         }
         else {/*x的父结点为祖父结点的右孩子,操作对称相反,此处省略*/}
    }
}

红黑树删除函数

static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root, TreeNode<K,V> x) {
    for (TreeNode<K,V> xp, xpl, xpr;;)  {
        if (x == null || x == root)/*x初始为null或者是根结点*/
            return root;
        else if ((xp = x.parent) == null) {/*上移之后x为根结点*/
            x.red = false;
            return x;
        }
        else if (x.red) {/*x指向红黑结点,将x着色为黑色*/
            x.red = false;
            return root;
        }
        /*x为父结点的左孩子*/
        else if ((xpl = xp.left) == x) {
            /*情况1:x的兄弟结点w是红色*/
            if ((xpr = xp.right) != null && xpr.red) {
                xpr.red = false;
                xp.red = true;
                root = rotateLeft(root, xp);
                xpr = (xp = x.parent) == null ? null : xp.right;
            }
             /*x的兄弟结点w是黑色*/
            if (xpr == null)/*x的兄弟结点w为null,直接上移*/
                x = xp;
            else {
                TreeNode<K,V> sl = xpr.left, sr = xpr.right;
                /*情况2:x的兄弟结点w是黑色的,而且w的两个子结点都是黑色的*/
                if ((sr == null || !sr.red) && (sl == null || !sl.red)) {
                    xpr.red = true;
                    x = xp;
                }
                else {
                    /*情况3:x的兄弟结点w是黑色的,w的左孩子是红色的,w的右孩子是黑色的*/
                    if (sr == null || !sr.red) {
                        if (sl != null)
                            sl.red = false;
                            xpr.red = true;
                            root = rotateRight(root, xpr);
                            xpr = (xp = x.parent) == null ? null : xp.right;
                        }
                    /*情况4:x的兄弟结点w是黑色的,且w的右孩子是红色的*/
                    if (xpr != null) {
                    xpr.red = (xp == null) ? false : xp.red;
                    if ((sr = xpr.right) != null)
                        sr.red = false;
                    }
                    if (xp != null) {
                        xp.red = false;
                        root = rotateLeft(root, xp);
                    }
                    x = root;
                }
            }
        }
        else { /*x为父结点的右孩子,代码省略*/}
    }
}

总结

Java8 HashMap的红黑树编程用的就是《算法导论》中介绍的红黑树经典算法。许多人可能在学习算法的时候红黑树都没有看懂,也有许多人看懂了《算法导论》中的红黑树算法却从来没有写代码实现过,那么《算法导论》的红黑树一章与Java8 HashMap就是一套最好的理论与实践的结合,具体说是Java的实践。

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

推荐阅读更多精彩内容

  • 0.目录 1.算法导论的红黑树本质上是2-3-4树 2.红黑树的结构和性质 3.红黑树的插入 4.红黑树的删除 5...
    王侦阅读 2,391评论 1 2
  • 这个周看算法导论,看到红黑树,看的我云里雾里绕啊。虽然最后看懂了,据我估计,要是过一个星期不看保证忘干净,因此决定...
    充满活力的早晨阅读 1,866评论 0 3
  • 目录 0.树0.1 一般树的定义0.2 二叉树的定义 1.查找树ADT 2.查找树的实现2.1 二叉查找树2.2 ...
    王侦阅读 6,989评论 0 3
  • 原文链接 二叉查找树 由于红黑树本质上就是一棵二叉查找树,所以在了解红黑树之前,咱们先来看下二叉查找树。 二叉查找...
    非典型程序员阅读 2,784评论 2 5
  • 1 名字起的这么高大上,究竟是什么效果呢,先来张图片欣赏一下 【这种轮播的方式突出了中间的内容(重点),可以滑动查...
    新林吃遍世界阅读 3,248评论 1 0