HashMap分析之红黑树树化过程

概述

HashMap是Java程序员使用频率最高的用于映射(键值对)处理的数据类型。随着JDK(Java Developmet Kit)版本的更新,JDK1.8对HashMap底层的实现进行了优化,例如引入红黑树的数据结构和扩容的优化等。本文主要分析一下HashMap中红黑树树化的过程。node

红黑树(red black tree)

  • 一个节点标记为红色或者黑色
  • 根是黑色的
  • 若是一个节点是红色的,那么它的子节点必须是黑色的(这就是为何叫红黑树)。
  • 一个节点到到一个null引用的每一条路径必须包含相同数目的黑色节点(因此红色节点不影响)

其实RB Tree和著名的AVL Tree有不少相同的地方,困难的地方都在于将一个新项插入到树中。了解AVL Tree的朋友应该都知道为了维持树的高度必须在插入一个新的项后必须在树的结构上进行改变,这里主要是经过旋转,固然在RB Tree中原理也是如此。git

两种旋转和一种典型的变换

:旋转的方向。程序员

:变换过程。github

:互相关联。数据结构

:单向关联。app

:表明红色的节点。优化

:表明黑色的节点。this

:表明一个不会破坏红黑树结构的部分,多是节点,或者是一个子树,总之不会破环当前树的结构。这个部分会因为旋转而链接到其余的节点后面,咱们能够理解成因为重力缘由它掉到了下面的节点上spa

  • 单旋转变换。
  • 双旋转变换(须要两次反方向的单旋转)。
  • 当遇到两个子几点都为红色的话执行颜色变换,由于插入 是红色的会产生冲突。若是根节点两边的子节点都是红色,两个叶子节点变成黑色,根节点变成红色,而后再将根节点变成黑色。

上面的图中描述了红黑树中三种典型的变换,其实前两种变换这正是AVL Tree中的两种典型的变换。翻译

几个问题

为何要进行旋转?

因为PX节点都为红色节点这破环了红节点下面的节点必须为黑色节点的规则。

 

新加入的节点老是红色的,这是为何呢?

由于被插入前的树结构是构建好的,一但咱们进行添加黑色的节点,不管添加在哪里都会破坏原有路径上的黑色节点的数量平等关系,因此插入红色节点是正确的选择。

 

为何要进行颜色变换?

正如第一种旋转新加入的节点X破坏了红黑树的结构不得不进行旋转,后面的就是旋转后的结果,旋转后造成新的结构,此时咱们发现两个子节点都是红色的因此执行第三个变换特性,颜色变换,由于若是子节点是红色的那么咱们在添加的时候只能添加黑色的节点,然而添加任何黑色叶子节点都会破坏树的第四条性质,因此要对其进行变换。当进行变换后叶子节点是红色的并且咱们默认添加的叶子节点是红色的,因此添加到黑色节点后并不会破坏树的第四条结构,因此这种变换颇有用

 

第二种双变换中在树的内部怎么出现的红色的节点?

正是因为上面的颜色变换致使新颜色变换后的节点与他的父节点产生了颜色冲突

 

与AVL树相比?

比AVL树相比优势是不用在节点类中保存一个节点高度这个变量,节省了内存。

并且红黑树通常不是以递归方式实现的而是以循环的形式实现。

通常的操做在最坏情形下花费O(logN)时间。

 

好了有了这些基本的概念让咱们去看一下HashMap中的代码的实现


这里咱们不仔细研究HashMap的其余机制若是对其余细节感兴趣或者不懂的话能够参考这篇文章:

http://yikun.github.io/2015/04/01/Java-HashMap工做原理及实现/

 

全部添加的操做最终都由这个方法完成。

   final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
    {
        Node<K, V>[] tab;
        Node<K, V> p;
        int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else
        {
            Node<K, V> e;
            K k;
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                // 若是当前的bucket里面已是红黑树的话,执行红黑树的添加操做
                e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value); else
            {
                for (int binCount = 0;; ++binCount)
                {
                    if ((e = p.next) == null)
                    {
                        p.next = newNode(hash, key, value, null);
                        // TREEIFY_THRESHOLD = 8,判断若是当前bucket的位置链表长度大于8的话就将此链表变成红黑树。
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
 treeifyBin(tab, hash); break;
                    }
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null)
            { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

上面的方法经过hash计算插入的项的槽位,若是有是同样的key则根据设置的参数是否执行覆盖,若是相应槽位空的话直接插入,若是对应的槽位有项则判断是红黑树结构仍是链表结构的槽位,链表的话则顺着链表寻找若是找到同样的key则根据参数选择覆盖,没有找到则连接在链表最后面,链表项的数目大于8则对其进行树化,若是是红黑树结构则按照树的添加方式添加项。

 

让咱们看一下treeifyBin这个方法。

   final void treeifyBin(Node<K, V>[] tab, int hash)
    {
        int n, index;
        Node<K, V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            // resize()方法这里不过多介绍,感兴趣的能够去看上面的连接。
            resize();
        // 经过hash求出bucket的位置。
        else if ((e = tab[index = (n - 1) & hash]) != null)
        {
            TreeNode<K, V> hd = null, tl = null;
            do
            {
                // 将每一个节点包装成TreeNode。
                TreeNode<K, V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else
                {
                    // 将全部TreeNode链接在一块儿此时只是链表结构。
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                // 对TreeNode链表进行树化。
 hd.treeify(tab);
        }
    }

找个方法所作的事情就是将刚才九个项以链表的方式链接在一块儿,而后经过它构建红黑树。

 

看代码以前咱们先了解一下TreeNode

   static final class TreeNode<K, V> extends LinkedHashMap.Entry<K, V>
    {
        TreeNode<K, V> parent; // red-black tree links
        TreeNode<K, V> left;
        TreeNode<K, V> right;
        TreeNode<K, V> prev; // needed to unlink next upon deletion
        boolean red;

        TreeNode(int hash, K key, V val, Node<K, V> next)
        {
            super(hash, key, val, next);
        }
        
        final void treeify(Node<K,V>[] tab)
        {
            // ......
        }
        
        static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x)
        {
            // ......
        }
        
        static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root, TreeNode<K,V> p)
        {
            // ......
        }
        
        static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root, TreeNode<K,V> p)
        {
            // ......
        }
        
        // ......其他方法省略
    }

能够看出出真正的维护红黑树结构的方法并无在HashMap中,所有都在TreeNode类内部。

 

咱们看一下treeify代码

   final void treeify(Node<K, V>[] tab)
    {
        TreeNode<K, V> root = null;
        // 以for循环的方式遍历刚才咱们建立的链表。
        for (TreeNode<K, V> x = this, next; x != null; x = next)
        {
            // next向前推动。
            next = (TreeNode<K, V>) x.next;
            x.left = x.right = null;
            // 为树根节点赋值。
            if (root == null)
            {
                x.parent = null;
                x.red = false;
                root = x;
            } else
            {
                // x即为当前访问链表中的项。
                K k = x.key;
                int h = x.hash;
                Class<?> kc = null;
                // 此时红黑树已经有了根节点,上面获取了当前加入红黑树的项的key和hash值进入核心循环。
                // 这里从root开始,是以一个自顶向下的方式遍历添加。
                // for循环没有控制条件,由代码内break跳出循环。
                for (TreeNode<K, V> p = root;;)
                {
                    // dir:directory,比较添加项与当前树中访问节点的hash值判断加入项的路径,-1为左子树,+1为右子树。
                    // ph:parent hash。
                    int dir, ph;
                    K pk = p.key;
                    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);

                    // xp:x parent。
                    TreeNode<K, V> xp = p;
                    // 找到符合x添加条件的节点。
                    if ((p = (dir <= 0) ? p.left : p.right) == null)
                    {
                        x.parent = xp;
                        // 若是xp的hash值大于x的hash值,将x添加在xp的左边。
                        if (dir <= 0)
                            xp.left = x;
                        // 反之添加在xp的右边。
                        else
                            xp.right = x;
                        // 维护添加后红黑树的红黑结构。
                        root = balanceInsertion(root, x);
                        
                        // 跳出循环当前链表中的项成功的添加到了红黑树中。
                        break;
                    }
                }
            }
        }
        // Ensures that the given root is the first node of its bin,本身翻译一下。
        moveRootToFront(tab, root);
    }

第一次循环会将链表中的首节点做为红黑树的根,然后的循环会将链表中的的项经过比较hash值而后链接到相应树节点的左边或者右边,插入可能会破坏树的结构因此接着执行balanceInsertion

 

咱们看balanceInsertion

   static <K, V> TreeNode<K, V> balanceInsertion(TreeNode<K, V> root, TreeNode<K, V> x)
    {
        // 正如开头所说,新加入树节点默认都是红色的,不会破坏树的结构。
        x.red = true;
        // 这些变量名不是做者随便定义的都是有意义的。
        // xp:x parent,表明x的父节点。
        // xpp:x parent parent,表明x的祖父节点
        // xppl:x parent parent left,表明x的祖父的左节点。
        // xppr:x parent parent right,表明x的祖父的右节点。
        for (TreeNode<K, V> xp, xpp, xppl, xppr;;)
        {
            // 若是x的父节点为null说明只有一个节点,该节点为根节点,根节点为黑色,red = false。
            if ((xp = x.parent) == null)
            {
                x.red = false;
                return x;
            } 
            // 进入else说明不是根节点。
            // 若是父节点是黑色,那么大吉大利(今晚吃鸡),红色的x节点能够直接添加到黑色节点后面,返回根就好了不须要任何多余的操做。
            // 若是父节点是红色的,但祖父节点为空的话也能够直接返回根此时父节点就是根节点,由于根必须是黑色的,添加在后面没有任何问题。
            else if (!xp.red || (xpp = xp.parent) == null)
                return root;
            
            // 一旦咱们进入到这里就说明了两件是情
            // 1.x的父节点xp是红色的,这样就遇到两个红色节点相连的问题,因此必须通过旋转变换。
            // 2.x的祖父节点xpp不为空。
            
            // 判断若是父节点是不是祖父节点的左节点
            if (xp == (xppl = xpp.left))
            {
                // 父节点xp是祖父的左节点xppr
                // 判断祖父节点的右节点不为空而且是不是红色的
                // 此时xpp的左右节点都是红的,因此直接进行上面所说的第三种变换,将两个子节点变成黑色,将xpp变成红色,而后将红色节点x顺利的添加到了xp的后面。
                // 这里你们有疑问为何将x = xpp?
                // 这是因为将xpp变成红色之后可能与xpp的父节点发生两个相连红色节点的冲突,这就又构成了第二种旋转变换,因此必须从底向上的进行变换,直到根。
                // 因此令x = xpp,而后进行下下一层循环,接着往上走。
                if ((xppr = xpp.right) != null && xppr.red)
                {
                    xppr.red = false;
                    xp.red = false;
                    xpp.red = true;
                    x = xpp;
                }
                // 进入到这个else里面说明。
                // 父节点xp是祖父的左节点xppr。
                // 祖父节点xpp的右节点xppr是黑色节点或者为空,默认规定空节点也是黑色的。
                // 下面要判断x是xp的左节点仍是右节点。
                else
                {
                    // x是xp的右节点,此时的结构是:xpp左->xp右->x。这明显是第二中变换须要进行两次旋转,这里先进行一次旋转。
                    // 下面是第一次旋转。
                    if (x == xp.right)
                    {
                        root = rotateLeft(root, x = xp);
                        xpp = (xp = x.parent) == null ? null : xp.parent;
                    }
                    // 针对自己就是xpp左->xp左->x的结构或者因为上面的旋转形成的这种结构进行一次旋转。
                    if (xp != null)
                    {
                        xp.red = false;
                        if (xpp != null)
                        {
                            xpp.red = true;
                            root = rotateRight(root, xpp);
                        }
                    }
                }
            } 
            // 这里的分析方式和前面的相对称只不过所有在右测再也不重复分析。
            else
            {
                if (xppl != null && xppl.red)
                {
                    xppl.red = false;
                    xp.red = false;
                    xpp.red = true;
                    x = xpp;
                } else
                {
                    if (x == xp.left)
                    {
                        root = rotateRight(root, x = xp);
                        xpp = (xp = x.parent) == null ? null : xp.parent;
                    }
                    if (xp != null)
                    {
                        xp.red = false;
                        if (xpp != null)
                        {
                            xpp.red = true;
                            root = rotateLeft(root, xpp);
                        }
                    }
                }
            }
        }
    }

若是您的联想能力很强的话估计到这里应该已经理解这集中变换的主要的关系。

下面简述一下前面的两种种幸运的状况

  1. x自己为根节点返回x。
  2. x的父节点为黑色或者x的父节点是根节点直接返回不须要变换。

如若上述两个条件不知足的话,就要进行变换了,容许我再贴一点代码......没有代码分析起来很困难。

颜色变换

            if (xp == (xppl = xpp.left))
            {
                if ((xppr = xpp.right) != null && xppr.red)
                {
                    xppr.red = false;
                    xp.red = false;
                    xpp.red = true;
                    x = xpp;
                }
          // ......
        }

***这里是一个典型的一个黑色节点的两个子节点都是红色的因此要进行颜色变换,由于插入的都是红色节点,当检测到祖父节点的左右子节点都是红色的时候两个红色就产生了冲突,因此先将节点进行这种颜色变换,将祖父节点变成红色,而后将祖父的两个子节点变成黑色,这样咱们插入的红色节点就不会违背红黑树的规则了。

***这里有人会有疑问,若是祖父节点是根节点呢,那样的话祖父节点也会变成黑色,由于每次循环进行插入平衡的操做当进行这种颜色变换以后都会将插入节点的引用指向祖父节点,当进行下一轮循环的时候会优先检测当前节点是不是根节点,若是是根节点那就将颜色变成黑色,下面看图:

 

***当将节点指向祖父节点进行下一轮循环时:

 

两个核心旋转(左旋转和右旋转)

       // 一旦咱们进入到这里就说明了两件是情
            // 1.x的父节点xp是红色的,这样就遇到两个红色节点相连的问题,因此必须通过旋转变换。
            // 2.x的祖父节点xpp不为空。
            
            // 判断若是父节点是不是祖父节点的左节点
            if (xp == (xppl = xpp.left))
            {
                if ((xppr = xpp.right) != null && xppr.red)
                {
            // ......
                }
// 进入到这个else里面说明。 // 父节点xp是祖父的左节点xppr。 // 祖父节点xpp的右节点xppr是黑色节点或者为空,默认规定空节点也是黑色的。 // 下面要判断x是xp的左节点仍是右节点。 else { // x是xp的右节点,此时的结构是:xpp左->xp右->x。这明显是第二中变换须要进行两次旋转,这里先进行一次旋转。 // 下面是第一次旋转。 if (x == xp.right) { root = rotateLeft(root, x = xp); xpp = (xp = x.parent) == null ? null : xp.parent; } // 针对自己就是xpp左->xp左->x的结构或者因为上面的旋转形成的这种结构进行一次旋转。 if (xp != null) { xp.red = false; if (xpp != null) { xpp.red = true; root = rotateRight(root, xpp); } } } }

颜色变换完成后进入下面的else块

咱们已知xp是xpp的左节点,首先判断了x是xp的左节点仍是右节点,若是是右节点的话构成了下面的结构。

这中结构须要进行双旋转,首先先进行一次向左旋转

左旋转

 1     static <K, V> TreeNode<K, V> rotateLeft(TreeNode<K, V> root, TreeNode<K, V> p)
 2     {
 3         // r:right,右节点。
 4         // pp:parent parent,父节点的父节点。
 5         // rl:right left,右节点的左节点。
 6         TreeNode<K, V> r, pp, rl;
 7         if (p != null && (r = p.right) != null)
 8         {
 9             if ((rl = p.right = r.left) != null)
10                 rl.parent = p;
11             if ((pp = r.parent = p.parent) == null)
12                 (root = r).red = false;
13             else if (pp.left == p)
14                 pp.left = r;
15             else
16                 pp.right = r;
17             r.left = p;
18             p.parent = r;
19         }
20         return root;
21     }

1.刚进入方法时,状态以下图。(RL多是空的)

2.进入了if块后执行到第10行后。

 9             if ((rl = p.right = r.left) != null)
10                 rl.parent = p;

此时若是9行的条件符合的话执行10行RL指向P,若是RL为null的话,P的右节点指向null。

3.接着看11和12行代码。

11             if ((pp = r.parent = p.parent) == null)
12                 (root = r).red = false;

首先咱们看11行if里面的赋值语句所形成的影响。

在if里面的表达式无论符不符合条件()内的内容都会执行。

若是符合条件的话会执行12行的代码,变成了下面的结果。

因为PP为空因此只剩下这三个。

4.若是11行的条件为假的话,执行完11行()内的表达式后执行13行

13             else if (pp.left == p)
14                 pp.left = r;

 

知足条件的话R和PP互相关联。

5.因为进入了13和14行因此不进入15和16行的else语句。

15             else
16                 pp.right = r;

6.看17和18行。

17             r.left = p;
18             p.parent = r;

最终执行完了一个旋转变成了咱们开始说的第一种旋转的形式,这个结构还须要向右旋转一次。

                    if (x == xp.right)
                    {
                        root = rotateLeft(root, x = xp);
                        xpp = (xp = x.parent) == null ? null : xp.parent;
                    }
xpp = (xp = x.parent) == null ? null : xp.parent;

执行完上面的代码,旋转后调整x,xp,和xpp的关系获得下图。

右旋转

                    if (xp != null)
                    {
                        xp.red = false;
                        if (xpp != null)
                        {
                            xpp.red = true;
                            root = rotateLeft(root, xpp);
                        }
                    }                            

1.首先让XP变成黑色。

 

2.若是XPP不为空的话变成红色。

因为咱们在rotateLeft(root, xpp),传进来的是XXP因此下面的的旋转中实际上就是对XP和XXP执行了一次与上面的方向相反其余彻底相同的旋转。

接着咱们看向右旋转的代码

 1     static <K, V> TreeNode<K, V> rotateRight(TreeNode<K, V> root, TreeNode<K, V> p)
 2     {
 3         // l:left,左节点。
 4         // pp:parent parent,父节点的父节点。
 5         // lr:left right,左节点的右节点。
 6         TreeNode<K, V> l, pp, lr;
 7         if (p != null && (l = p.left) != null)
 8         {
 9             if ((lr = p.left = l.right) != null)
10                 lr.parent = p;
11             if ((pp = l.parent = p.parent) == null)
12                 (root = l).red = false;
13             else if (pp.right == p)
14                 pp.right = l;
15             else
16                 pp.left = l;
17             l.right = p;
18             p.parent = l;
19         }
20         return root;
21     }

3.刚进来的时候结构是这个样子。

在这里的P就是刚才传进来的XPP

4.这里咱们认为LR是存在的,其实这个不影响主要的旋转,为空就指向null呗,直接执行完9和10行。

 9             if ((lr = p.left = l.right) != null)
10                 lr.parent = p;

5.在这里咱们假使PP是存在的,直接执行完11的表达式再也不执行12行。(再也不分析不存在的状况)。

11             if ((pp = l.parent = p.parent) == null)
12                 (root = l).red = false;

6.因为11行的条件不符合,如今直接执行13行的表达式,不符合执行15行else,执行16行。

15             else
16                 pp.left = l;

7.最后执行层17和18行。

17             l.right = p;
18             p.parent = l;

 

最终完成两次的旋转。


 

疑问?

你们可能以为和刚才接不上实际上是这样的,刚才在右旋转前的时候的图像是这个样的。

由于咱们传进来的是XPP,因此结合上一次的向左旋转咱们在向右旋转的时候看到全图应该是这个样子的。(注:XPPP多是XPP的左父节点也多是右父节点这里不影响,并且能够是任意颜色

如今知道为何XPPP能够是任意颜色的了吧,由于旋转事后X是黑色的即使XPPP是红色,此时咱们又能够对两个红色的子节点进行颜色变换了,变换后X和XPPP有发生了颜色冲突,接着进行旋转直到根。

   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)
                return root;
            if (xp == (xppl = xpp.left))
            {
               // 插入位置父节点在祖父节点的左边。
            }
            else
            {
         // 插入位置父节点在祖父节点的右边。
            }
        }
    }

咱们值分析了插入位置父节点在祖父节点的左边的状况,并无分析另一面的对称状况,实际上是同样的由于调用的都是相同的方法。

 

以上就是在1.8中的HashMap新引进的红黑树树化的过程,与原来的链表相比当同一个bucket上存储不少entry的话树形的查找结构明显要比链表线性的的效率要高。

 

转载请注明出处。

相关文章
相关标签/搜索