java集合的底層原理(Map的底層原理(TreeMap) 二)

TreeMap的原理

一、     數據結構

源碼定義如下  

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable

    TreeMap繼承AbstractMap,實現NavigableMap、Cloneable、Serializable三個接口。其中AbstractMap表明TreeMap爲一個Map即支持key-value的集合, NavigableMap則意味着它支持一系列的導航方法,具備針對給定搜索目標返回最接近匹配項的導航方法 。

       TreeMap中同時也包含了如下幾個重要的屬性:

//比較器,因爲TreeMap是有序的,通過comparator接口我們可以對TreeMap的內部排序進行精密的控制
        private final Comparator<? super K> comparator;
        //TreeMap紅-黑節點,爲TreeMap的內部類
        private transient Entry<K,V> root = null;
        //容器大小
        private transient int size = 0;
        //TreeMap修改次數
        private transient int modCount = 0;
        //紅黑樹的節點顏色--紅色
        private static final boolean RED = false;
        //紅黑樹的節點顏色--黑色
        private static final boolean BLACK = true;
       對於葉子節點Entry是TreeMap的內部類,它有幾個重要的屬性:

//鍵
        K key;
        //值
        V value;
        //左孩子
        Entry<K,V> left = null;
        //右孩子
        Entry<K,V> right = null;
        //父親
        Entry<K,V> parent;
        //顏色
        boolean color = BLACK;

      二、存儲

    先看源碼

public V put(K key, V value) {
    Entry<K,V> t = root;
    /**
     * 如果根節點都爲null,還沒建立起來紅黑樹,我們先new Entry並賦值給root把紅黑樹建立起來,這個時候紅
     * 黑樹中已經有一個節點了,同時修改操作+1。
     */
    if (t == null) {
        compare(key, key); 
        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    /**
     * 如果節點不爲null,定義一個cmp,這個變量用來進行二分查找時的比較;定義parent,是new Entry時必須
     * 要的參數
     */
    int cmp;
    Entry<K,V> parent;
    // cpr表示有無自己定義的排序規則,分兩種情況遍歷執行
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        /**
         * 從root節點開始遍歷,通過二分查找逐步向下找
         * 第一次循環:從根節點開始,這個時候parent就是根節點,然後通過自定義的排序算法
         * cpr.compare(key, t.key)比較傳入的key和根節點的key值,如果傳入的key<root.key,那麼
         * 繼續在root的左子樹中找,從root的左孩子節點(root.left)開始:如果傳入的key>root.key,
         * 那麼繼續在root的右子樹中找,從root的右孩子節點(root.right)開始;如果恰好key==root.key,
         * 那麼直接根據root節點的value值即可。
         * 後面的循環規則一樣,當遍歷到的當前節點作爲起始節點,逐步往下找
         *
         * 需要注意的是:這裏並沒有對key是否爲null進行判斷,建議自己的實現Comparator時應該要考慮在內
         */
        do {
            parent = t;
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    else {
        //從這裏看出,當默認排序時,key值是不能爲null的
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
        Comparable<? super K> k = (Comparable<? super K>) key;
        //這裏的實現邏輯和上面一樣,都是通過二分查找,就不再多說了
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    /**
     * 能執行到這裏,說明前面並沒有找到相同的key,節點已經遍歷到最後了,我們只需要new一個Entry放到
     * parent下面即可,但放到左子節點上還是右子節點上,就需要按照紅黑樹的規則來。
     */
    Entry<K,V> e = new Entry<>(key, value, parent);
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    /**
     * 節點加進去了,並不算完,我們在前面紅黑樹原理章節提到過,一般情況下加入節點都會對紅黑樹的結構造成
     * 破壞,我們需要通過一些操作來進行自動平衡處置,如【變色】【左旋】【右旋】
     */
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

put方法源碼中通過fixAfterInsertion(e)方法來進行自平衡處理 

再重點看下 fixAfterInsertion方法的源碼

private void fixAfterInsertion(Entry<K,V> x) {
    //新插入的節點爲紅色節點
    x.color = RED;
    //我們知道父節點爲黑色時,並不需要進行樹結構調整,只有當父節點爲紅色時,才需要調整
    while (x != null && x != root && x.parent.color == RED) {
        //如果父節點是左節點,對應上表中情況1和情況2
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            //如果叔父節點爲紅色,對應於“父節點和叔父節點都爲紅色”,此時通過變色即可實現平衡
            //此時父節點和叔父節點都設置爲黑色,祖父節點設置爲紅色
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                //如果插入節點是黑色,插入的是右子節點,通過【左右節點旋轉】(這裏先進行父節點左旋)
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateLeft(x);
                }
                //設置父節點和祖父節點顏色
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                //進行祖父節點右旋(這裏【變色】和【旋轉】並沒有嚴格的先後順序,達成目的就行)
                rotateRight(parentOf(parentOf(x)));
            }
        } else {
            //父節點是右節點的情況
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            //對應於“父節點和叔父節點都爲紅色”,此時通過變色即可實現平衡
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                //如果插入節點是黑色,插入的是左子節點,通過【右左節點旋轉】(這裏先進行父節點右旋)
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateRight(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                //進行祖父節點左旋(這裏【變色】和【旋轉】並沒有嚴格的先後順序,達成目的就行)
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }
    //根節點必須爲黑色
    root.color = BLACK;
}

光看文字有點不好理解,請結合下圖,會看得更清晰

三、讀取

get方法是通過二分查找的思想,我們看一下源碼

public V get(Object key) {
    Entry<K,V> p = getEntry(key);
    return (p==null ? null : p.value);
}
/**
 * 從root節點開始遍歷,通過二分查找逐步向下找
 * 第一次循環:從根節點開始,這個時候parent就是根節點,然後通過k.compareTo(p.key)比較傳入的key和
 * 根節點的key值;
 * 如果傳入的key<root.key, 那麼繼續在root的左子樹中找,從root的左孩子節點(root.left)開始;
 * 如果傳入的key>root.key, 那麼繼續在root的右子樹中找,從root的右孩子節點(root.right)開始;
 * 如果恰好key==root.key,那麼直接根據root節點的value值即可。
 * 後面的循環規則一樣,當遍歷到的當前節點作爲起始節點,逐步往下找
 */
//默認排序情況下的查找
final Entry<K,V> getEntry(Object key) {
    
    if (comparator != null)
        return getEntryUsingComparator(key);
    if (key == null)
        throw new NullPointerException();
    @SuppressWarnings("unchecked")
    Comparable<? super K> k = (Comparable<? super K>) key;
    Entry<K,V> p = root;
    while (p != null) {
        int cmp = k.compareTo(p.key);
        if (cmp < 0)
            p = p.left;
        else if (cmp > 0)
            p = p.right;
        else
            return p;
    }
    return null;
}
/**
 * 從root節點開始遍歷,通過二分查找逐步向下找
 * 第一次循環:從根節點開始,這個時候parent就是根節點,然後通過自定義的排序算法
 * cpr.compare(key, t.key)比較傳入的key和根節點的key值,如果傳入的key<root.key,那麼
 * 繼續在root的左子樹中找,從root的左孩子節點(root.left)開始:如果傳入的key>root.key,
 * 那麼繼續在root的右子樹中找,從root的右孩子節點(root.right)開始;如果恰好key==root.key,
 * 那麼直接根據root節點的value值即可。
 * 後面的循環規則一樣,當遍歷到的當前節點作爲起始節點,逐步往下找
 */
//自定義排序規則下的查找
final Entry<K,V> getEntryUsingComparator(Object key) {
    @SuppressWarnings("unchecked")
    K k = (K) key;
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        Entry<K,V> p = root;
        while (p != null) {
            int cmp = cpr.compare(k, p.key);
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
            else
                return p;
        }
    }
    return null;
}

 

四、刪除

 remove方法可以分爲兩個步驟,先是找到這個節點,直接調用了上面介紹的getEntry(Object key),這個步驟我們就不說了,直接說第二個步驟,找到後的刪除操作。

public V remove(Object key) {
    Entry<K,V> p = getEntry(key);
    if (p == null)
        return null;

    V oldValue = p.value;
    deleteEntry(p);
    return oldValue;
}

 

通過deleteEntry(p)進行刪除操作,刪除操作的原理我們在前面已經講過

  1. 刪除的是根節點,則直接將根節點置爲null;
  2. 待刪除節點的左右子節點都爲null,刪除時將該節點置爲null;
  3. 待刪除節點的左右子節點有一個有值,則用有值的節點替換該節點即可;
  4. 待刪除節點的左右子節點都不爲null,則找前驅或者後繼,將前驅或者後繼的值複製到該節點中,然後刪除前驅或者後繼(前驅:左子樹中值最大的節點,後繼:右子樹中值最小的節點);
    private void deleteEntry(Entry<K,V> p) {
        modCount++;
        size--;
    	//當左右子節點都不爲null時,通過successor(p)遍歷紅黑樹找到前驅或者後繼
        if (p.left != null && p.right != null) {
            Entry<K,V> s = successor(p);
            //將前驅或者後繼的key和value複製到當前節點p中,然後刪除節點s(通過將節點p引用指向s)
            p.key = s.key;
            p.value = s.value;
            p = s;
        } 
        Entry<K,V> replacement = (p.left != null ? p.left : p.right);
        /**
         * 至少有一個子節點不爲null,直接用這個有值的節點替換掉當前節點,給replacement的parent屬性賦值,給
         * parent節點的left屬性和right屬性賦值,同時要記住葉子節點必須爲null,然後用fixAfterDeletion方法
         * 進行自平衡處理
         */
        if (replacement != null) {
            //將待刪除節點的子節點掛到待刪除節點的父節點上。
            replacement.parent = p.parent;
            if (p.parent == null)
                root = replacement;
            else if (p == p.parent.left)
                p.parent.left  = replacement;
            else
                p.parent.right = replacement;
            p.left = p.right = p.parent = null;
            /**
             * p如果是紅色節點的話,那麼其子節點replacement必然爲紅色的,並不影響紅黑樹的結構
             * 但如果p爲黑色節點的話,那麼其父節點以及子節點都可能是紅色的,那麼很明顯可能會存在紅色相連的情
             * 況,因此需要進行自平衡的調整
             */
            if (p.color == BLACK)
                fixAfterDeletion(replacement);
        } else if (p.parent == null) {//這種情況就不用多說了吧
            root = null;
        } else { 
            /**
             * 如果p節點爲黑色,那麼p節點刪除後,就可能違背每個節點到其葉子節點路徑上黑色節點數量一致的規則,
             * 因此需要進行自平衡的調整
             */ 
            if (p.color == BLACK)
                fixAfterDeletion(p);
            if (p.parent != null) {
                if (p == p.parent.left)
                    p.parent.left = null;
                else if (p == p.parent.right)
                    p.parent.right = null;
                p.parent = null;
            }
        }
    }

     

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章