3分鐘讓你明白 HashMap之紅黑樹樹化過程

清早看到的一遍挺好的文章,從源碼上十分詳細的解釋的紅黑樹的樹化過程,特意作個文章的搬運工,分享給掘友,文末附原文地址!html

適合 面試裝逼以及 自我提高,前排請備好瓜子。


概述

HashMap是Java程序員使用頻率最高的用於映射(鍵值對)處理的數據類型。隨着JDK(Java Developmet Kit)版本的更新,JDK1.8對HashMap底層的實現進行了優化,例如引入紅黑樹的數據結構和擴容的優化等。本文主要分析一下HashMap中紅黑樹樹化的過程。node

紅黑樹(red black tree)

一個節點標記爲紅色或者黑色。 根是黑色的。 若是一個節點是紅色的,那麼它的子節點必須是黑色的(這就是爲何叫紅黑樹)。 一個節點到到一個null引用的每一條路徑必須包含相同數目的黑色節點(因此紅色節點不影響)。 其實RB Tree和著名的AVL Tree有不少相同的地方,困難的地方都在於將一個新項插入到樹中。瞭解AVL Tree的朋友應該都知道爲了維持樹的高度必須在插入一個新的項後必須在樹的結構上進行改變,這裏主要是經過旋轉,固然在RB Tree中原理也是如此。程序員

兩種旋轉和一種典型的變換

旋轉的方向: 面試

旋轉的方向。

變換過程: bash

互相關聯: 數據結構

單向關聯: app

表明紅色的節點: 優化

表明黑色的節點: ui

表明一個不會破壞紅黑樹結構的部分,多是節點,或者是一個子樹,總之不會破環當前樹的結構。這個部分會因爲旋轉而鏈接到其餘的節點後面,咱們能夠理解成因爲重力緣由它掉到了下面的節點上: this

  • 單旋轉變換。
  • 雙旋轉變換(須要兩次反方向的單旋轉)。
  • 當遇到兩個子幾點都爲紅色的話執行顏色變換,由於插入 是紅色的會產生衝突。若是根節點兩邊的子節點都是紅色,兩個葉子節點變成黑色,根節點變成紅色,而後再將根節點變成黑色。

上面的圖中描述了紅黑樹中三種典型的變換,其實前兩種變換這正是AVL Tree中的兩種典型的變換。

幾個問題

爲何要進行旋轉?

因爲P和X節點都爲紅色節點這破環了紅節點下面的節點必須爲黑色節點的規則。

新加入的節點老是紅色的,這是爲何呢?

由於被插入前的樹結構是構建好的,一但咱們進行添加黑色的節點,不管添加在哪裏都會破壞原有路徑上的黑色節點的數量平等關係,因此插入紅色節點是正確的選擇。

爲何要進行顏色變換?

正如第一種旋轉新加入的節點X破壞了紅黑樹的結構不得不進行旋轉,後面的就是旋轉後的結果,旋轉後造成新的結構,此時咱們發現兩個子節點都是紅色的因此執行第三個變換特性,顏色變換,由於若是子節點是紅色的那麼咱們在添加的時候只能添加黑色的節點,然而添加任何黑色葉子節點都會破壞樹的第四條性質,因此要對其進行變換。當進行變換後葉子節點是紅色的並且咱們默認添加的葉子節點是紅色的,因此添加到黑色節點後並不會破壞樹的第四條結構,因此這種變換頗有用。

第二種雙變換中在樹的內部怎麼出現的紅色的節點? 正是因爲上面的顏色變換致使新顏色變換後的節點與他的父節點產生了顏色衝突。

與AVL樹相比? 比AVL樹相比優勢是不用在節點類中保存一個節點高度這個變量,節省了內存。

並且紅黑樹通常不是以遞歸方式實現的而是以循環的形式實現。

通常的操做在最壞情形下花費O(logN)時間。

好了有了這些基本的概念讓咱們去看一下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 = falseif ((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);
                        }
                    }
                }
            }
        }
    }
複製代碼

若是您的聯想能力很強的話估計到這裏應該已經理解這集中變換的主要的關係。

下面簡述一下前面的兩種種幸運的狀況

x自己爲根節點返回x。 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執行了一次與上面的方向相反其餘徹底相同的旋轉。

接着咱們看向右旋轉的代碼

static <K, V> TreeNode<K, V> rotateRight(TreeNode<K, V> root, TreeNode<K, V> p)
    {
        // l:left,左節點。
        // pp:parent parent,父節點的父節點。
        // lr:left right,左節點的右節點。
        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)
                (root = l).red = false;
            else if (pp.right == p)
                pp.right = l;
            else
                pp.left = l;
            l.right = p;
            p.parent = l;
        }
        return root;
    }
複製代碼

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的話樹形的查找結構明顯要比鏈表線性的的效率要高。


原文地址: www.cnblogs.com/finite/p/82…

對不起我騙你了,三分鐘看不完,哈哈哈哈哈哈哈!

相關文章
相關標籤/搜索