HashMap—红黑树算法结构详解

最近在撸Java8-Hashmap源码的时候遇到一个问题红黑树的存储结构,当时看的时候还是有点迷糊的,而且对于左旋还是右旋,直接就缴械投降了。现在花一篇文章来梳理一下自己对这个结构算法的理解。

红黑树也叫自平衡二叉查找树或者平衡二叉B树,
时间复杂度为O(log n)
高度h <= log2(n+1)

红黑树的特点
  性质1. 节点是红色或黑色。
  性质2. 根节点是黑色。
  性质3 每个叶节点(NIL节点,空节点)是黑色的。
  性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
  性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

注意:这里有一点需要解释的就是,插入节点一定是插入一个红色的节点,为什么呢?因为这样只是有可能违背性质4,其余四个性质都是符合的,可以减小复杂度。以下我们以HashMap的红黑树实现为例。

插入实现
  /**
     * Tree version of putVal.
     */
    final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                   int h, K k, V v) {
        Class<?> kc = null;
        boolean searched = false;
        TreeNode<K,V> root = (parent != null) ? root() : this;
        for (TreeNode<K,V> p = root;;) {
            int dir, ph; K pk;
            // 比较要插入值 与 节点的 hash值等属性 的大小
            // 确定大小之后然后选择 左右子树 循环继续比较
            if ((ph = p.hash) > h)
                dir = -1;
            else if (ph < h)
                dir = 1;
            else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                return p;
            else if ((kc == null &&
                      (kc = comparableClassFor(k)) == null) ||
                     (dir = compareComparables(kc, k, pk)) == 0) {
                if (!searched) {
                    TreeNode<K,V> q, ch;
                    searched = true;
                    if (((ch = p.left) != null &&
                         (q = ch.find(h, k, kc)) != null) ||
                        ((ch = p.right) != null &&
                         (q = ch.find(h, k, kc)) != null))
                        return q;
                }
                dir = tieBreakOrder(k, pk);
            }

            TreeNode<K,V> xp = p;
            // 只有确认了 要插入的位置的 左子树 或者 右子树为空,即找到了要插入节点的位置
            if ((p = (dir <= 0) ? p.left : p.right) == null) {
                Node<K,V> xpn = xp.next;
                // 创建新的 树的叶子节点
                TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
               // 插入到指定的 分支上
                if (dir <= 0)
                    xp.left = x;
                else
                    xp.right = x;
                xp.next = x;
                x.parent = x.prev = xp;
                if (xpn != null)
                    ((TreeNode<K,V>)xpn).prev = x;
                // 这里需要注意了 这有执行了两步操作,
                // balanceInsertion这一步涉及到了 红黑树的平衡算法,我们只看它
                moveRootToFront(tab, balanceInsertion(root, x));
                return null;
            }
        }
    }

在看平衡算法之前,我们需要对红黑树的特性进行详细说明,确保知道什么时候该旋转,什么时候采用什么旋转。

第一:插入的节点是红色
第二:平衡、或者 旋转算法是对三层树形结构进行的。如果你站在整个树结构上看,你会发现无从下手
第三:什么时候该旋转,不满足h <= log2(n+1),即前两层树形结构不是满二叉树
第四:在插入节点之前,树的结构是符合红黑树的(这个前提很重要

balanceInsertion平衡算法
  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;
            }
            // 这里是二层树形结构,xp 存在,或者xpp 为空
            // 这里需要说明一下,有人会认为这不仅仅包含二层树形结构,可能还有三层
            // 从条件上来说是的,可能包含三层,但是这里有一个我们上面说的前提,那就是插入之前必须是红黑树,
             // 那也就是说!xp.red 意思就是 xp 为黑色节点,而且xp 是符合红黑树的,那么插入一个红色节点是不会改变它是红黑树的特性
            // 反过来想插入之前的问题
            // 这里我们把问题缩小一点,就插入点的三层树形结构。而且插入之前还必须符合红黑树
            // 如果xp 插入之前是黑色的并且符合红黑树,这种特性是不是跟只有一个根节点是一样的
            else if (!xp.red || (xpp = xp.parent) == null)
                return root;
            // 三层结构,如果是左分支
            if (xp == (xppl = xpp.left)) {
                // 这里是判断前两层是否是满二叉树,也就是是否满足h <= log2(n+1)
                // 如果满足是不需要进行 旋转的,因为无法对其旋转(左旋,右旋),
                // 也不需要旋转,只需要进行重新着色就可以满足红黑树的五大特性了
                if ((xppr = xpp.right) != null && xppr.red) {
                    xppr.red = false;
                    xp.red = false;
                    xpp.red = true;
                    x = xpp;
                }
                else {
                    // 接下来就是 不满足满二叉树,或者说 不满足h <= log2(n+1)
                    // 这个时候我们就需要考虑旋转了,怎样旋转呢,这里我先说结果,后面分析原因,或者为什么
                    if (x == xp.right) {
                        // 树形结构是  L — R   -->  左旋,变成 L— L树形结构
                        root = rotateLeft(root, x = xp);
                        // x、xp、xpp 重新赋值
                        xpp = (xp = x.parent) == null ? null : xp.parent;
                    }
                    if (xp != null) {
                        // xp 着色
                        xp.red = false;
                        if (xpp != null) {
                            // xpp 着色
                            xpp.red = true;
                            // L — L   --> 右旋
                           // 将三层树形结构  右旋 变成一个 二层结构,满足 h <= log2(n+1) 特性
                            root = rotateRight(root, xpp);
                        }
                    }
                }
            }
            else {
                 //  这里也是一样的
                // 判断前两层是否是满二叉树,也就是是否满足h <= log2(n+1)
                // 如果满足是不需要进行 旋转的,因为无法对其旋转(左旋,右旋),
                // 也不需要旋转,只需要进行重新着色就可以满足红黑树的五大特性了
                if (xppl != null && xppl.red) {
                    xppl.red = false;
                    xp.red = false;
                    xpp.red = true;
                    x = xpp;
                }
                else {
                   // 接下来也是 不满足满二叉树,或者说 不满足h <= log2(n+1)
                    if (x == xp.left) {
                        // 树形结构是  R — L   -->  右旋,变成 R— R树形结构
                        root = rotateRight(root, x = xp);
                        // x、xp、xpp 重新赋值
                        xpp = (xp = x.parent) == null ? null : xp.parent;
                    }
                    if (xp != null) {
                        // xp 着色
                        xp.red = false;
                        if (xpp != null) {
                            // xpp 着色
                            xpp.red = true;
                            // R — R   --> 右旋
                           // 将三层树形结构  左旋 变成一个 二层结构,满足 h <= log2(n+1) 特性
                            root = rotateLeft(root, xpp);
                        }
                    }
                }
            }
        }
    }

接下来我们重点说一下,左旋和右旋,首先不管是左旋还是右旋,都是只对两个节点进行修改,有一个节点的位置是不发生改变的。

  • 第一什么时候需要进行旋转 ?,以及需要旋转几次 ?

判断树的结构是否满足h <= log2(n+1),如果不满足则需要进行旋转,如果满足只需要重新进行着色即可。

  • 第二是什么叫做左旋,还是右旋 ?

判断采用左旋,还是右旋,就是当前这两个节点的位置关系,是左分支,还是右分支,则相应的旋转之后是相反的分支,例如:现有是 左分支的关系, 旋转之后就是右分支,则是进行了右旋操作,同样现有关系是右分支的关系,旋转之后就是左分支,则是进行了 左旋操作
把上面balanceInsertion平衡算法统计一下,我们可以得到下面
L(左分支)——L(左分支) → R (右旋)
L(左分支)——R(右分支)→ L(左旋)——R (右旋)
R(右分支)——R(右分支)→ L(左旋)
R(右分支)——L(左分支)→ R (右旋)——L(左旋)

下面我们以图来看一看包含两步的旋转过程来,对照一下
L(左分支)——R(右分支)→ L(左旋)——R (右旋)

R(右分支)——L(左分支)→ R (右旋)——L(左旋)
  • 第三:如何确定我该做左旋,还是右旋

个人理解,如果原来的树曲曲折折,需要先掰直了下面部分,如果是直线的树了 再把上面部分掰弯,形成一个倒V型。这个只是方面理解,真正的原因是,如果不采用上面的步骤进行旋转,旋转顺序换掉,都会得到一种结果那就是,整个二叉树不成立了,失去了二叉树的意义了。当然我们也可以一步到位,以空间换时间,这是没有问题的,从复杂度来讲左旋和右旋是最精炼的,最基础的,可以完美适配的。

rotateLeft(左旋)
  static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root, TreeNode<K,V> p) {
        TreeNode<K,V> r, pp, rl;
        if (p != null && (r = p.right) != null) {
            if ((rl = p.right = r.left) != null)
                rl.parent = p;
            if ((pp = r.parent = p.parent) == null)
                // 只有当p 已经探到了树的底部的时候,左旋,会改变根的指向,所以这里需要修改掉root 的指向
                (root = r).red = false;
            else if (pp.left == p)
                pp.left = r;
            else
                pp.right = r;
            r.left = p;
            p.parent = r;
        }
        return root;
    }
rotateRight(右旋)
  static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root, TreeNode<K,V> p) {
        TreeNode<K,V> l, pp, lr;
        if (p != null && (l = p.left) != null) {
            if ((lr = p.left = l.right) != null)
                lr.parent = p;
            if ((pp = l.parent = p.parent) == null)
                // 只有当p 已经探到了树的底部的时候,左旋,会改变根的指向,所以这里需要修改掉root 的指向
                (root = l).red = false;
            else if (pp.right == p)
                pp.right = l;
            else
                pp.left = l;
            l.right = p;
            p.parent = l;
        }
        return root;
    }

我相信看完上面的解释,你对左旋和右旋的实现会有了一个更为清晰的认知了。
好了,分析到这里了。

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

推荐阅读更多精彩内容

  • R-B Tree简介 R-B Tree,全称是Red-Black Tree,又称为“红黑树”,它一种特殊的二叉查找...
    张晨辉Allen阅读 9,222评论 5 30
  • 1、红黑树介绍 红黑树又称R-B Tree,全称是Red-Black Tree,它是一种特殊的二叉查找树,红黑树的...
    文哥的学习日记阅读 9,849评论 1 35
  • 基于树实现的数据结构,具有两个核心特征: 逻辑结构:数据元素之间具有层次关系; 数据运算:操作方法具有Log级的平...
    yhthu阅读 4,237评论 1 5
  • 这个周看算法导论,看到红黑树,看的我云里雾里绕啊。虽然最后看懂了,据我估计,要是过一个星期不看保证忘干净,因此决定...
    充满活力的早晨阅读 1,931评论 0 3
  • 辛夷坞1 木末芙蓉花2,山中发红萼3。 涧户寂无人4,纷纷开且落5。 《辛夷坞》是唐代诗人王维《辋川集》诗二十首之...
    无人知所去阅读 590评论 0 0