HashMap 鏈表和紅黑樹的轉換


HashMap在jdk1.8之后引入了紅黑樹的概念,表示若桶中鏈表元素超過8時,會自動轉化成紅黑樹;若桶中元素小於等於6時,樹結構還原成鏈表形式。

原因:

紅黑樹的平均查找長度是log(n),長度為8,查找長度為log(8)=3,鏈表的平均查找長度為n/2,當長度為8時,平均查找長度為8/2=4,這才有轉換成樹的必要;鏈表長度如果是小於等於6,6/2=3,雖然速度也很快的,但是轉化為樹結構和生成樹的時間並不會太短。

還有選擇6和8的原因是:

中間有個差值7可以防止鏈表和樹之間頻繁的轉換。假設一下,如果設計成鏈表個數超過8則鏈表轉換成樹結構,鏈表個數小於8則樹結構轉換成鏈表,如果一個HashMap不停的插入、刪除元素,鏈表個數在8左右徘徊,就會頻繁的發生樹轉鏈表、鏈表轉樹,效率會很低。
————————————————
版權聲明:本文為CSDN博主「創客公元」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/weixin_37264997/article/details/106074846

jdk1.8的hashmap真的是大於8就轉換成紅黑樹,小於6就變成鏈表嗎?????

 

最近研究hashmap源碼的時候,會結合網上的一些博客來促進理解。而關於紅黑樹和鏈表相互轉換這一塊,大部分的文章都會這樣描述:hashmap中定義了兩個常量:

 /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;

當鏈表元素個數大於8的時候,就會轉換為紅黑樹;當紅黑樹元素個數小於6的時候,就會轉換回鏈表。
hashMap中確實定義了這兩個常量,但並非簡單通過元素個數的判斷來進行轉換。

鏈表轉換為紅黑樹

鏈表轉換為紅黑樹的最終目的,是為了解決在map中元素過多,hash沖突較大,而導致的讀寫效率降低的問題。在源碼的putVal方法中,有關紅黑樹結構化的分支為:

            //此處遍歷鏈表
            for (int binCount = 0; ; ++binCount) {
                //遍歷到鏈表最后一個節點
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //如果鏈表元素個數大於等於TREEIFY_THRESHOLD
                    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;
            }

即網上所說的,鏈表的長度大於8的時候,就轉換為紅黑樹,我們來看看treeifyBin方法:

    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //首先tab的長度是否小於64,
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        //小於64則進行擴容
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            //否則才將列表轉換為紅黑樹
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

可以看到在treeifyBin中並不是簡單地將鏈表轉換為紅黑樹,而是先判斷table的長度是否大於64,如果小於64,就通過擴容的方式來解決,避免紅黑樹結構化。
鏈表長度大於8有兩種情況:

  • table長度足夠,hash沖突過多
  • hash沒有沖突,但是在計算table下標的時候,由於table長度太小,導致很多hash不一致的
    第二種情況是可以用擴容的方式來避免的,擴容后鏈表長度變短,讀寫效率自然提高。另外,擴容相對於轉換為紅黑樹的好處在於可以保證數據結構更簡單。
    由此可見並不是鏈表長度超過8就一定會轉換成紅黑樹,而是先嘗試擴容

紅黑樹轉換為鏈表

基本思想是當紅黑樹中的元素減少並小於一定數量時,會切換回鏈表。而元素減少有兩種情況:
1、調用map的remove方法刪除元素

 final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            //根據hash值以及key判斷當前的是否相等,如果相等直接返回
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                //判斷是否為紅黑樹結構
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    //如果不是則為鏈表結構
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                //判斷當前桶是否是紅黑樹結構,如果是的話
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

   final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                                  boolean movable) {
            int n;
            if (tab == null || (n = tab.length) == 0)
                return;
            int index = (n - 1) & hash;
            TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
            TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
            if (pred == null)
                tab[index] = first = succ;
            else
                pred.next = succ;
            if (succ != null)
                succ.prev = pred;
            if (first == null)
                return;
            if (root.parent != null)
                root = root.root();
            //判斷是否解除紅黑樹結構
            if (root == null || root.right == null ||
                (rl = root.left) == null || rl.left == null) {
                tab[index] = first.untreeify(map);  // too small
                return;
            }
            TreeNode<K,V> p = this, pl = left, pr = right, replacement;
            if (pl != null && pr != null) {
                TreeNode<K,V> s = pr, sl;
                while ((sl = s.left) != null) // find successor
                    s = sl;
                boolean c = s.red; s.red = p.red; p.red = c; // swap colors
                TreeNode<K,V> sr = s.right;
                TreeNode<K,V> pp = p.parent;
                if (s == pr) { // p was s's direct parent
                    p.parent = s;
                    s.right = p;
                }
                else {
                    TreeNode<K,V> sp = s.parent;
                    if ((p.parent = sp) != null) {
                        if (s == sp.left)
                            sp.left = p;
                        else
                            sp.right = p;
                    }
                    if ((s.right = pr) != null)
                        pr.parent = s;
                }
                p.left = null;
                if ((p.right = sr) != null)
                    sr.parent = p;
                if ((s.left = pl) != null)
                    pl.parent = s;
                if ((s.parent = pp) == null)
                    root = s;
                else if (p == pp.left)
                    pp.left = s;
                else
                    pp.right = s;
                if (sr != null)
                    replacement = sr;
                else
                    replacement = p;
            }
            else if (pl != null)
                replacement = pl;
            else if (pr != null)
                replacement = pr;
            else
                replacement = p;
            if (replacement != p) {
                TreeNode<K,V> pp = replacement.parent = p.parent;
                if (pp == null)
                    root = replacement;
                else if (p == pp.left)
                    pp.left = replacement;
                else
                    pp.right = replacement;
                p.left = p.right = p.parent = null;
            }

            TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);

            if (replacement == p) {  // detach
                TreeNode<K,V> pp = p.parent;
                p.parent = null;
                if (pp != null) {
                    if (p == pp.left)
                        pp.left = null;
                    else if (p == pp.right)
                        pp.right = null;
                }
            }
            if (movable)
                moveRootToFront(tab, r);
        }

可以看到,此處並沒有利用到網上所說的,當節點數小於UNTREEIFY_THRESHOLD時才轉換,而是通過紅黑樹根節點及其子節點是否為空來判斷。

2、resize的時候,對紅黑樹進行了拆分

resize的時候,判斷節點類型,如果是鏈表,則將鏈表拆分,如果是TreeNode,則執行TreeNode的split方法分割紅黑樹,而split方法中將紅黑樹轉換為鏈表的分支如下:

 final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
            TreeNode<K,V> b = this;
            // Relink into lo and hi lists, preserving order
            TreeNode<K,V> loHead = null, loTail = null;
            TreeNode<K,V> hiHead = null, hiTail = null;
            int lc = 0, hc = 0;
            for (TreeNode<K,V> e = b, next; e != null; e = next) {
                next = (TreeNode<K,V>)e.next;
                e.next = null;
                if ((e.hash & bit) == 0) {
                    if ((e.prev = loTail) == null)
                        loHead = e;
                    else
                        loTail.next = e;
                    loTail = e;
                    ++lc;
                }
                else {
                    if ((e.prev = hiTail) == null)
                        hiHead = e;
                    else
                        hiTail.next = e;
                    hiTail = e;
                    ++hc;
                }
            }
            //在這之前的邏輯是將紅黑樹每個節點的hash和一個bit進行&運算,
            //根據運算結果將樹划分為兩棵紅黑樹,lc表示其中一棵樹的節點數
            if (loHead != null) {
                if (lc <= UNTREEIFY_THRESHOLD)
                    tab[index] = loHead.untreeify(map);
                else {
                    tab[index] = loHead;
                    if (hiHead != null) // (else is already treeified)
                        loHead.treeify(tab);
                }
            }
            if (hiHead != null) {
                if (hc <= UNTREEIFY_THRESHOLD)
                    tab[index + bit] = hiHead.untreeify(map);
                else {
                    tab[index + bit] = hiHead;
                    if (loHead != null)
                        hiHead.treeify(tab);
                }
            }
        }

這里才用到了 UNTREEIFY_THRESHOLD 的判斷,當紅黑樹節點元素小於等於6時,才調用untreeify方法轉換回鏈表

總結

1、hashMap並不是在鏈表元素個數大於8就一定會轉換為紅黑樹,而是先考慮擴容,擴容達到默認限制后才轉換。
2、hashMap的紅黑樹不一定小於6的時候才會轉換為鏈表,而是只有在resize的時候才會根據 UNTREEIFY_THRESHOLD 進行轉換。


免責聲明!

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



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