淺析Java源碼之HashMap外傳-紅黑樹Treenode(已鴿)


  (這篇文章暫時鴿了,有點理解不能,點進來的小伙伴可以撤了)

 

  剛開始准備在HashMap中直接把紅黑樹也過了的,結果發現這個類不是一般的麻煩,所以單獨開一篇。

  由於紅黑樹之前完全沒接觸過,所以這篇博客相當於探索(其實之前的博客都是邊看源碼邊寫的,全是探索)。

  

  紅黑樹沒見過,樹我還是知道的,所以先上一張帥圖:

  

  紅黑樹在這個基本樹的基礎上還多了red,暫時不知道啥意思,慢慢探索。

 

  先來一個類總覽:

    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        // ...
    }

  這個紅黑樹繼承了一個另外一個類中的靜態內部類:

    static class Entry<K,V> extends HashMap.Node<K,V> {}

  這個類也繼承了一個靜態內部類,竟然是HashMap中的Node,真是無語的循環!

  這些東西雖然繞來繞去,但是總的特性就是兩個字:鏈表!!!!!!!!!

 

變量

  廢話不多說,首先來看一個這個類的內部變量:

    // 父節點
    TreeNode<K,V> parent;
    // 左右子節點
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    // ???
    TreeNode<K,V> prev;
    // 紅黑樹的精髓 => red!
    boolean red;

  這些節點的意思都比較直接,按理講在正常的樹中只有父、左、右三個,這里的prev和red暫時不清楚干嘛用的。

  

構造函數

  接下來是構造函數

    TreeNode(int hash, K key, V val, Node<K,V> next) {
        super(hash, key, val, next);
    }

  super!然后我跑去看了下LinkedHashMap.Entry的構造函數,還是super!!!!

  繞了一圈,最后還是回到了Node的構造函數,如下:

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

  沒啥好講的。

  

  需要注意的是,靜態內部類都是作為工具來使用的,所以不從常規的添加節點、查詢來講解,直接從鏈表轉紅黑樹的方法入手,看到什么方法講什么方法,Let's go!

    // tab為HashMap的數組
    final void treeify(Node<K,V>[] tab) {
        TreeNode<K,V> root = null;
        // 這里的this是需要樹轉換數組索引處的第一個鏈表元素
        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 {
                K k = x.key;
                int h = x.hash;
                Class<?> kc = null;
                for (TreeNode<K,V> p = root;;) {
                    int dir, ph;
                    K pk = p.key;
                    // 根據hash值的大小區分左右
                    if ((ph = p.hash) > h)
                        dir = -1;
                    else if (ph < h)
                        dir = 1;
                    // 當出現hash碰撞 暫時不管這個
                    else if ((kc == null &&
                            (kc = comparableClassFor(k)) == null) ||
                            (dir = compareComparables(kc, k, pk)) == 0)
                        dir = tieBreakOrder(k, pk);
                    // 這里的xp變成了root
                    TreeNode<K,V> xp = p;
                    // 這個表達式不加括號看起來真是惡心
                    // 根據dir判斷root.left或者root.right是否為null
                    if ((p = (dir <= 0) ? p.left : p.right) == null) {
                        // 設置下一個元素的parent為root
                        x.parent = xp;
                        // 設置root的left或right
                        if (dir <= 0)
                            xp.left = x;
                        else
                            xp.right = x;
                        root = balanceInsertion(root, x);
                        break;
                    }
                }
            }
        }
        moveRootToFront(tab, root);
    }

  在前面的轉換中,其實參數tab並沒有用上,所以暫時只需要關注鏈表本身。

  在balanceInsertion方法之前,只完成了兩件事:

1、將鏈表的第一個元素設置為根節點

2、將第二個元素的hash與根節點做比較,然后設置根節點的left或right為該元素

  畫個帥圖:

  接下來看balanceInsertion方法做了什么,該方法接受兩個參數:根節點、根節點的left(right)節點。

  這個方法真長,讓我深深的吸了一口氣。

    static <K, V> TreeNode<K, V> balanceInsertion(TreeNode<K, V> root,TreeNode<K, V> x) {
        // 紅屬性
        x.red = true;
        // xp => x的父節點
        // xpp => xp的父節點
        // xppl => xpp.left
        // xppr => xpp.right
        for (TreeNode<K, V> xp, xpp, xppl, xppr;;) {
            // 當x為根節點時
            // 這時xp已經被賦值 xp => root
            if ((xp = x.parent) == null) {
                x.red = false;
                return x;
            }
            // 根節點red為false 所以直接返回root 
            else if (!xp.red || (xpp = xp.parent) == null)
                return root;
                
            // more code...
        }
    }

  很遺憾,返回的特別快,這里的x為根節點的子節點,而根節點的父節點為null,所以這里直接返回root,返回后break,進入下一個循環。

  總的來說,這個函數在第一次什么都沒有做。

  下一次循環時,x為鏈表中第三個元素,這里就存在一種新情況:dir的值。

 

  首先當dir的值與上次不同時,我們假設在第二次判斷中,x的hash值小於根節點root,於是dir為-1,這樣就有:

    xp.left = x;

  而第三次,x的hash值比根節點大,而root.right此時仍為null,所以有

    xp.right = x;

  這樣,根節點的兩個兒子就集齊了。

 

  另外一種情況就是dir的值與上次相同,此時p.left即root.left不為null,所以會進入下一輪內循環。此時的p不再是root,而是root.left,即第二個鏈表元素。

  同樣,第二個鏈表元素作為父節點與當前節點的hash作比較,然后設置對應的left/right。

  此時,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) {/**/}
            else if (!xp.red || (xpp = xp.parent) == null){/**/ }
            //  此時xpp為root 父元素正好等於root.left
            if (xp == (xppl = xpp.left)) {
                // 此時xpp.right還沒有值
                if ((xppr = xpp.right) != null && xppr.red) {
                    // ...
                } else {
                    if (x == xp.right) {
                        // ...
                    }
                    if (xp != null) {
                        // ...
                    }
                }
            } else {
                // ...
            }
        }
    }

  這里的分支特別多。。。先看看當前的情況,並假設2次都是left,父、父父均無右節點,如圖:

  

  一個一個情況的討論,反正基本的塞節點已經明白了。這種情況下,會進入如下分支:

    if (xp == (xppl = xpp.left)) {
        // 此時xpp.right還沒有值
        if ((xppr = xpp.right) != null && xppr.red) {
            // ...
        } else {
            if (x == xp.right) {
                // ...
            }
            if (xp != null) {
                // 父節點黑了
                xp.red = false;
                if (xpp != null) {
                    // 父父節點紅了
                    xpp.red = true;
                    root = rotateRight(root, xpp);
                }
            }
        }
    } else {
        // ...
    }

  這種情況下,將父節點置黑,父父節點置紅,並調用另外一個方法rotateRight。

  直接看這個方法,接受兩個參數:根節點、父父節點(還是root):

    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) {
                // p.left = l.right
                if ((lr = p.left = l.right) != null)
                    lr.parent = p;
                // l.parent = p.parent
                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;
    }

  這個函數應該是叫做向右翻轉紅黑樹,在理解的時候盡量不要把p當做根節點,而是一個普通的節點。

  另外,這里就直接講解各種情況下的翻轉效果。

  每一個if語句中的賦值都會改變樹的結構,這里不太好講,用圖來一步一步解釋,當前例子:

  

  可見,翻轉后,原來的root被轉移到了l的右邊,l變成了新的root且red被置false,函數返回新的root。

 

  現在討論更加普遍的情況,首先看在什么情況下會調用右翻轉,將上一個函數中的第一個判斷分支抽出如下:

    // true
    xp == (xppl = xpp.left)
    // false
    (xppr = xpp.right) != null && xppr.red
    // true
    xp != null
    // true
    xpp != null

  false代表這是else分支。

1、父元素為父父元素的left

2、父父元素的right為null或者父父元素的red為真

3、父元素及父父元素均不為null

  很明顯,上面的翻轉符合這個條件,這里還有另外兩種情況,即:

  針對這兩種情況的翻轉給出對應的結果圖:

  可以看出,在p為根節點時,l會被轉換為新的根節點,並且有:

    l.right = p;
    p.left = l.right

  然而,在p不為根節點時,情況稍微會不一樣:

  這種情況下不會根節點替換,僅僅是p與l進行換位。

 

  下面來看第二個分支:

    // false
    xp == (xppl = xpp.left)
    // false
    xppl != null && xppl.red
    // true
    x == xp.left

  即:

1、父元素為父父元素的right

2、父父元素的left為null或者red為真

3、當前元素為父元素的left

  這里的參數不太一樣:

    root = rotateRight(root, x = xp);

  直接看圖吧!

  我要瘋了!!!!

  

  技術太渣,暫時不搞這個數據結構。。。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM