TreeMap原始碼剖析

weixin_33866037發表於2017-05-30

特此宣告:本文為轉載文章!尊重原創的勞動果實,嚴禁剽竊
本文轉載於: http://blog.csdn.net/ns_code/article/details/36421085
出自於:【蘭亭風雨的專欄】

前言

本文不打算延續前幾篇的風格(對所有的原始碼加入註釋),因為要理解透TreeMap的所有原始碼,對博主來說,確實需要耗費大量的時間和經歷,目前看來不大可能有這麼多時間的投入,故這裡意在通過於閱讀原始碼對TreeMap有個巨集觀上的把握,並就其中一些方法的實現做比較深入的分析。

紅黑樹簡介

TreeMap是基於紅黑樹實現的,這裡只對紅黑樹做個簡單的介紹,紅黑樹是一種特殊的二叉排序樹,關於二叉排序樹,參見:http://blog.csdn.net/ns_code/article/details/19823463,紅黑樹通過一些限制,使其不會出現二叉樹排序樹中極端的一邊倒的情況,相對二叉排序樹而言,這自然提高了查詢的效率。

二叉排序樹的基本性質如下:

  1. 每個節點都只能是紅色或者黑色
  2. 根節點是黑色
  3. 每個葉節點(NIL節點,空節點)是黑色的。
  4. 如果一個結點是紅的,則它兩個子節點都是黑的。也就是說在一條路徑上不能出現相鄰的兩個紅色結點。
  5. 從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。

正是這些性質的限制,使得紅黑樹中任一節點到其子孫葉子節點的最長路徑不會長於最短路徑的2倍,因此它是一種接近平衡的二叉樹。

說到紅黑樹,自然不免要和AVL樹對比一番。相比較而言,AVL樹是嚴格的平衡二叉樹,而紅黑樹不算嚴格意義上的平衡二叉樹,只是接近平衡,不會讓樹的高度如BST極端情況那樣等於節點的個數。其實能用到紅黑樹的地方,也都可以用AVL樹來實現,但紅黑樹的應用卻非常廣泛,而AVL樹則很少被使用。在執行插入、刪除操作時,AVL樹需要調整的次數一般要比紅黑樹多(紅黑樹的旋轉調整最多隻需三次),效率相對較低,且紅黑樹的統計效能較AVL樹要好,當然AVL樹在查詢效率上可能更勝一籌,但實際上也高不了多少。

紅黑樹的插入刪除操作很簡單,就是單純的二叉排序樹的插入刪除操作。紅黑樹被認為比較變態的地方自然在於插入刪除後對紅黑樹的調整操作(旋轉和著色),主要是情況分的很多,限於篇幅及博主的熟悉程度優先,這裡不打算詳細介紹插入刪除後調整紅黑樹的各種情況及其實現,我們有個巨集觀上的瞭解即可,如須詳細瞭解,參見演算法導論或一些相關的資料。

TreeMap原始碼剖析

儲存結構

TreeMap的排序是基於對key的排序實現的,它的每一個Entry代表紅黑樹的一個節點,Entry的資料結構如下:

   static final class Entry<K,V> implements Map.Entry<K,V> {  
        // 鍵  
        K key;  
        // 值  
        V value;  
        // 左孩子  
        Entry<K,V> left = null;  
        // 右孩子  
        Entry<K,V> right = null;  
        // 父節點  
        Entry<K,V> parent;  
        // 當前節點顏色  
        boolean color = BLACK;  
 
        // 建構函式  
        Entry(K key, V value, Entry<K,V> parent) {  
            this.key = key;  
            this.value = value;  
            this.parent = parent;  
        }  

        。。。。。。
   }
構造方法

先來看下TreeMap的構造方法。TreeMap一共有4個構造方法。

1、無參構造方法

public TreeMap() {    
    comparator = null;    
}

採用無參構造方法,不指定比較器,這時候,排序的實現要依賴key.compareTo()方法,因此key必須實現Comparable介面,並覆寫其中的compareTo方法。

2、帶有比較器的構造方法

public TreeMap(Comparator<? super K> comparator) {    
    this.comparator = comparator;    
}

採用帶比較器的構造方法,這時候,排序依賴該比較器,key可以不用實現Comparable介面。

3、帶Map的構造方法

public TreeMap(Map<? extends K, ? extends V> m) {    
    comparator = null;    
    putAll(m);    
} 

該構造方法同樣不指定比較器,呼叫putAll方法將Map中的所有元素加入到TreeMap中。putAll的原始碼如下:

// 將map中的全部節點新增到TreeMap中    
public void putAll(Map<? extends K, ? extends V> map) {    
    // 獲取map的大小    
    int mapSize = map.size();    
    // 如果TreeMap的大小是0,且map的大小不是0,且map是已排序的“key-value對”    
    if (size==0 && mapSize!=0 && map instanceof SortedMap) {    
        Comparator c = ((SortedMap)map).comparator();    
        // 如果TreeMap和map的比較器相等;    
        // 則將map的元素全部拷貝到TreeMap中,然後返回!    
        if (c == comparator || (c != null && c.equals(comparator))) {    
            ++modCount;    
            try {    
                buildFromSorted(mapSize, map.entrySet().iterator(),    
                            null, null);    
            } catch (java.io.IOException cannotHappen) {    
            } catch (ClassNotFoundException cannotHappen) {    
            }    
            return;    
        }    
    }    
    // 呼叫AbstractMap中的putAll();    
    // AbstractMap中的putAll()又會呼叫到TreeMap的put()    
    super.putAll(map);    
}   

顯然,如果Map裡的元素是排好序的,就呼叫buildFromSorted方法來拷貝Map中的元素,這在下一個構造方法中會重點提及,而如果Map中的元素不是排好序的,就呼叫AbstractMap的putAll(map)方法,該方法原始碼如下:

public void putAll(Map<? extends K, ? extends V> m) {    
    for (Map.Entry<? extends K, ? extends V> e : m.entrySet())    
        put(e.getKey(), e.getValue());    
}

很明顯它是將Map中的元素一個個put(插入)到TreeMap中的,主要因為Map中的元素是無序存放的,因此要一個個插入到紅黑樹中,使其有序存放,並滿足紅黑樹的性質。

4、帶有SortedMap的構造方法

public TreeMap(SortedMap<K, ? extends V> m) {    
    comparator = m.comparator();    
    try {    
        buildFromSorted(m.size(), m.entrySet().iterator(), null, null);    
    } catch (java.io.IOException cannotHappen) {    
    } catch (ClassNotFoundException cannotHappen) {    
    }    
}    

首先將比較器指定為m的比較器,這取決於生成m時呼叫構造方法是否傳入了指定的構造器,而後呼叫buildFromSorted方法,將SortedMap中的元素插入到TreeMap中,由於SortedMap中的元素師有序的,實際上它是根據SortedMap建立的TreeMap,將SortedMap中對應的元素新增到TreeMap中。

插入刪除

插入操作即對應TreeMap的put方法,put操作實際上只需按照二叉排序樹的插入步驟來操作即可,插入到指定位置後,再做調整,使其保持紅黑樹的特性。put原始碼的實現:

  
    public V put(K key, V value) {  
        Entry<K,V> t = root;  
        // 若紅黑樹為空,則插入根節點  
        if (t == null) {  
        // TBD:  
        // 5045147: (coll) Adding null to an empty TreeSet should  
        // throw NullPointerException  
        //  
        // compare(key, key); // type check  
            root = new Entry<K,V>(key, value, null);  
            size = 1;  
            modCount++;  
            return null;  
        }  
        int cmp;  
        Entry<K,V> parent;  
        // split comparator and comparable paths  
        Comparator<? super K> cpr = comparator;  
        // 找出(key, value)在二叉排序樹中的插入位置。  
        // 紅黑樹是以key來進行排序的,所以這裡以key來進行查詢。  
        if (cpr != null) {  
            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 {  
            if (key == null)  
                throw new NullPointerException();  
            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-value)新建節點  
        Entry<K,V> e = new Entry<K,V>(key, value, parent);  
        if (cmp < 0)  
            parent.left = e;  
        else 
            parent.right = e;  
        // 插入新的節點後,呼叫fixAfterInsertion調整紅黑樹。  
        fixAfterInsertion(e);  
        size++;  
        modCount++;  
        return null;  
    }  

這裡的fixAfterInsertion便是節點插入後對樹進行調整的方法,這裡不做介紹。

刪除操作及對應TreeMap的deleteEntry方法,deleteEntry方法同樣也只需按照二叉排序樹的操作步驟實現即可,刪除指定節點後,再對樹進行調整即可。deleteEntry方法的實現原始碼如下:

// 刪除“紅黑樹的節點p”    
private void deleteEntry(Entry<K,V> p) {    
    modCount++;    
    size--;    
  
    if (p.left != null && p.right != null) {    
        Entry<K,V> s = successor (p);    
        p.key = s.key;    
        p.value = s.value;    
        p = s;    
    }   
  
    Entry<K,V> replacement = (p.left != null ? p.left : p.right);    
  
    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;    
  
        if (p.color == BLACK)    
            fixAfterDeletion(replacement);    
    } else if (p.parent == null) {   
        root = null;    
    } else {  
        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;    
        }    
    }    
}    

後面的fixAfterDeletion方法便是節點刪除後對樹進行調整的方法,這裡不做介紹。
其他很多方法這裡不再一一介紹。

幾點總結

本文對TreeMap的分析較前幾篇文章有些淺嘗輒止,TreeMap用的沒有HashMap那麼多,我們有個巨集觀上的把我和比較即可。

  1. TreeMap是根據key進行排序的,它的排序和定位需要依賴比較器或覆寫Comparable介面,也因此不需要key覆寫hashCode方法和equals方法,就可以排除掉重複的key,而HashMap的key則需要通過覆寫hashCode方法和equals方法來確保沒有重複的key。

  2. TreeMap的查詢、插入、刪除效率均沒有HashMap高,一般只有要對key排序時才使用TreeMap。

  3. TreeMap的key不能為null,而HashMap的key可以為null。

注:對TreeSet和HashSet的原始碼不再進行剖析,二者分別是基於TreeMap和HashMap實現的,只是對應的節點中只有key,而沒有value,因此對TreeMap和HashMap比較瞭解的話,對TreeSet和HashSet的理解就會非常容易。

相關文章