1. 程式人生 > >HashMap 詳解六

HashMap 詳解六

連結串列轉樹結構

根據詳解四, 當連結串列長度大於 8 時, 為了更高效的查詢, 需要轉成紅黑樹結構, 使用的方法是 treeifyBin. 過程是先把連結串列結構調整為雙向連結串列結構, 再把雙向連結串列結構調整為紅黑樹結構.

/**
 * tab: 陣列
 * hash: 新節點 key 的雜湊值, 通過運算就可以得出需要轉成樹的對應連結串列的對應索引值
 */
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    
    // 對於陣列為空或者陣列長度小於閾值, 需要擴容
    // 這裡閾值是 64, 陣列長度不是很長, 但是卻需要轉成樹結構,
    // 說明很多衝突了, 先擴容看看情況有沒有改善, 大概吧
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        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);
            // hd 是頭結點, t1 用來指向下一個節點
            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);
    }
}

/**
 * 通過普通連結串列節點構造樹節點
 */
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
    return new TreeNode<>(p.hash, p.key, p.value, next);
}

/**
 * 這個方法不是 HashMap 的方法, 是它內部類 TreeNode 的方法, 將雙向連結串列轉成紅黑樹結構
 */
final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null;
    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;

            // p 指向樹的當前節點
            // x 表示雙向連結串列的當前節點
            // 開始遍歷樹
            for (TreeNode<K,V> p = root;;) {
                int dir, ph;
                K pk = p.key;

                // 雙向連結串列的當前節點雜湊值和樹的當前節點雜湊值比較
                // 小於就把 dir 設定 -1
                // 大於就把 dir 設定 1
                // 等於就把 dir 設定 0
                // 這裡可以參考上一 part 的 putTreeVal 方法同樣的遍歷方式
                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);

                TreeNode<K,V> xp = p;

                // 根據 dir 決定向左遍歷還是向右遍歷, 一直到葉子節點
                // 然後把雙向連結串列的當前節點插入到葉子節點
                // 最後通過 balanceInsertion 方法調整紅黑樹, 參考上一 part
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    x.parent = xp;
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    root = balanceInsertion(root, x);
                    break;
                }
            }
        }
    }

    // 調整陣列索引值指向的根節點, 參考上一 part
    moveRootToFront(tab, root);
}

複製紅黑樹

在詳解三講 resize 方法時, 將樹節點複製到新的陣列使用方法 split, 這個方法同樣是 HashMap 內部類 TreeNode 的方法, 下面具體來分析整個複製過程.

/**
 * map: HashMap 本身
 * tab: 新陣列
 * index: 舊陣列的索引位置
 * bit: 舊陣列長度
 */
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    TreeNode<K,V> b = this;

    // 跟詳解三一樣, 先設定兩對頭尾節點表示兩個連結串列
    // 複製過程是先通過雙向連結串列的遍歷, 然後複製到兩個連結串列, 參考詳解三
    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;
        }
    }

    // 陣列索引位置指向連結串列頭結點
    // 連結串列長度小於閾值, 把樹節點轉成連結串列節點儲存
    // 這裡閾值為 6, 跟連結串列轉樹的閾值 8 不同, 不清楚為什麼要不同
    // 連結串列長度大於等於閾值, 把連結串列轉成樹結構, 參考上面的 treeify 方法
    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);
        }
    }
}

/**
 * 把樹節點轉成連結串列節點
 */
final Node<K,V> untreeify(HashMap<K,V> map) {
    Node<K,V> hd = null, tl = null;
    for (Node<K,V> q = this; q != null; q = q.next) {
        Node<K,V> p = map.replacementNode(q, null);
        if (tl == null)
            hd = p;
        else
            tl.next = p;
        tl = p;
    }
    return hd;
}

到這裡, HashMap 插入鍵值對的過程就基本介紹完了, 至於獲取鍵值對就不仔細展開討論了, 涉及到的遍歷都是一樣的. 關於刪除操作以後有機會再來具體分析.