Java集合之TreeMap


Map的單元是對鍵值對的處理,之前分析過的兩種Map,HashMap和LinkedHashMap都是用哈希值去尋找我們想要的鍵值對,優點是由O(1)的查找速度。

那如果我們在一個對查找性能要求不那么高,反而對有序性要求比較高的應用場景呢?

這個時候HashMap就不再適用了,我們需要一種新的Map,在JDK中提供了一個接口:SortedMap,我想分析一下具體的實現中的一種:TreeMap.

HahMap是Key無序的,而TreeMap是Key有序的。

1.看一下基本成員:

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
    private final Comparator<? super K> comparator;
    private transient Entry<K,V> root = null;
    private transient int size = 0;
    private transient int modCount = 0;
    public TreeMap() {
        comparator = null;
    }    
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }
    //后面省略
}

TreeMap繼承了NavigableMap,而NavigableMap繼承自SortedMap,為SortedMap添加了搜索選項,NavigableMap有幾種方法,分別是不同的比較要求:floorKey是小於等於,ceilingKey是大於等於,lowerKey是小於,higherKey是大於。

注意初始化的時候,有一個Comparator成員,這是用於維持有序的比較器,當我們想做一個自定義數據結構的TreeMap時,可以重寫這個比較器

2.我們看一下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;
    //后續省略
}

咦?木有了熟悉了哈希值,多了left,right,parent,這是我們的樹結構,最后看到color,明白了:TreeMap是基於紅黑樹實現的!而且默認的節點顏色是黑色。

至於紅黑樹,想必多多少少都聽過,這是一種平衡的二叉查找樹,是2-3樹的一種變體,即擁有二叉查找樹的高效查找,擁有2-3樹的高效平衡插入能力。

紅黑樹巧妙的增加了顏色這個維度,對2-3樹的樹本身進行了降維成了二叉樹,這樣樹的調整不會再如2-3樹那么繁瑣。

有的同學看到這里會質疑我,你這個胡說八道,和算法導論里講的不一樣!

對,CLRS中確實沒有這段,這段選自《Algorithms》,我覺得提供了一種有趣的理解思路,所以如果之前只看了CLRS,建議去看一下這本書,互相驗證。

不過為了尊重JDK的作者,后面的還是按照CLRS中的講解來吧,畢竟在JDK源碼的注釋中寫着:From CLR。

我們在紅黑樹中的一切插入和刪除后,為了維護樹的有序性的動作看起來繁復,但都是為了維護下面幾個紅黑樹的基本性質

(1)樹的節點只有紅與黑兩種顏色
(2)根節點為黑色的
(3)葉子節點為黑色的
(4)紅色節點的字節點必定是黑色的
(5)從任意一節點出發,到其后繼的葉子節點的路徑中,黑色節點的數目相同

紅黑樹的第4條性質保證了這些路徑中的任意一條都不存在連續的紅節點,而紅黑樹的第5條性質又保證了所有的這些路徑上的黑色節點的數目相同。因而最短路徑必定是只包含黑色節點的路徑,而最長路徑為紅黑節點互相交叉的路徑,由於所有的路徑的起點必須是黑色的,而紅色節點又不能連續存在,因而最長路徑的長度為全為黑色節點路徑長度的二倍。

回到TreeMap本身,看看它的put方法:

public V put(K key, V value) {
    Entry<K,V> t = root;
    if (t == null) {
        compare(key, key); // type (and possibly null) check

        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    Entry<K,V> parent;
    // split comparator and comparable paths
    Comparator<? super K> cpr = comparator;
    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);
    }
    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;
}
View Code

此處就是二叉樹的比較查找到合適的位置,然后插入,需要注意的是

(1)先檢測root節點是不是null,如果為null,則新插入的節點為root節點。

(2)最好自定義自己的Comparator,否則將會繼承原始的比較方法,可能會出現問題

(3)插入的鍵值不能為null,否則會拋出空指針的異常。

(4)插入新節點后,調用fixAfterInsertion(e)方法來修復紅黑樹。

看一下get方法,這里會調用getEntry方法,就是二叉查找樹的查找:

final Entry<K,V> getEntry(Object key) {
    // Offload comparator-based version for sake of performance
    if (comparator != null)
        return getEntryUsingComparator(key);
    if (key == null)
        throw new NullPointerException();
    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;
}
View Code

還有一個remove方法,這里最后調用的是deleteEntry()方法,在deleteEntry()方法中最后調用fixAfterDeletion方法來修復樹的順序。

紅黑樹的刪除操作復雜的讓人發指,對着CLRS慢慢看吧:

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

    V oldValue = p.value;
    deleteEntry(p);
    return oldValue;
}
private void deleteEntry(Entry<K,V> p) {
    modCount++;
    size--;

    // If strictly internal, copy successor's element to p and then make p
    // point to successor.
    if (p.left != null && p.right != null) {
        Entry<K,V> s = successor(p);
        p.key = s.key;
        p.value = s.value;
        p = s;
    } // p has 2 children

    // Start fixup at replacement node, if it exists.
    Entry<K,V> replacement = (p.left != null ? p.left : p.right);

    if (replacement != null) {
        // Link replacement to parent
        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;

        // Null out links so they are OK to use by fixAfterDeletion.
        p.left = p.right = p.parent = null;

        // Fix replacement
        if (p.color == BLACK)
            fixAfterDeletion(replacement);
    } else if (p.parent == null) { // return if we are the only node.
        root = null;
    } else { //  No children. Use self as phantom replacement and unlink.
        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;
        }
    }
}
View Code

上面所做的一切繁瑣操作都是為了紅黑樹的基本性質,而修復順序的操作中最基本的就是左旋和右旋了,下面是左旋和右選的源碼。

/** From CLR */
private void rotateLeft(Entry<K,V> p) {
    if (p != null) {
        Entry<K,V> r = p.right;
        p.right = r.left;
        if (r.left != null)
            r.left.parent = p;
        r.parent = p.parent;
        if (p.parent == null)
            root = r;
        else if (p.parent.left == p)
            p.parent.left = r;
        else
            p.parent.right = r;
        r.left = p;
        p.parent = r;
    }
}

/** From CLR */
private void rotateRight(Entry<K,V> p) {
    if (p != null) {
        Entry<K,V> l = p.left;
        p.left = l.right;
        if (l.right != null) l.right.parent = p;
        l.parent = p.parent;
        if (p.parent == null)
            root = l;
        else if (p.parent.right == p)
            p.parent.right = l;
        else p.parent.left = l;
        l.right = p;
        p.parent = l;
    }
}

其實所有的操作都是關於紅黑樹的操作,

決定了TreeMap的有序性,對於TreeMap的增刪改查的效率都是O(Log(n))的。

 到這里,TreeMap其實就差不多了,最關鍵的還是對紅黑樹的操作,希望這種數據結構的知識能掌握的比較扎實吧,多看書,多編程,夯實基礎,與諸君共勉。


免責聲明!

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



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