Java 紅黑樹(Red-Black tree)

整體介紹

  • JDK1.8的HashMap:底層實現(數組+鏈表/紅黑樹)
  • 一、爲何要從JDK1.8以前的鏈表設計,修改成鏈表或紅黑樹的設計?
  • 當某個鏈表比較長的時候,查找效率仍是會下降。
  • 爲了提升查詢效率,那麼把table[index]下面的鏈表作調整。
  • 若是table[index]的鏈表的節點的個數比較少,(8個或之內),就保持鏈表。若是超過8個,那麼就要考慮把鏈表轉爲一棵紅黑樹。
  • TREEIFY_THRESHOLD:樹化閾值,從鏈表轉爲紅黑樹的臨界值。
  • 二、何時轉化樹?
  • table[index]下的結點數一達到8個就樹化嗎?
  • 若是table[index]的節點數量已經達到8個了,還要判斷table.length是否達到64,若是沒有達到64,先擴容。

 

Java TreeMap實現了SortedMap接口,也就是說會按照key的大小順序對Map中的元素進行排序,key大小的評判能夠經過其自己的天然順序(natural ordering),也能夠經過構造時傳入的比較器(Comparator)。程序員

TreeMap底層經過紅黑樹(Red-Black tree)實現,也就意味着containsKey(), get(), put(), remove()都有着log(n)的時間複雜度。其具體算法實現參照了《算法導論》。算法

 

 

出於性能緣由,TreeMap是非同步的(not synchronized),若是須要在多線程環境使用,須要程序員手動同步;或者經過以下方式將TreeMap包裝成(wrapped)同步的:數組

SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));多線程

紅黑樹是一種近似平衡的二叉查找樹,它可以確保任何一個節點的左右子樹的高度差不會超過兩者中較低那個的一陪。具體來講,紅黑樹是知足以下條件的二叉查找樹(binary search tree):app

  1. 每一個節點要麼是紅色,要麼是黑色。
  2. 根節點必須是黑色
  3. 紅色節點不能連續(也便是,紅色節點的孩子和父親都不能是紅色),即若是一個節點是紅色的,則它的子節點必須是黑色的。。
  4. 每一個葉子節點是黑色。 [注意:這裏葉子節點,是指爲空的(null)葉子節點!]
  5. 對於每一個節點,從該點至null(樹尾端)的任何路徑,都含有相同個數的黑色節點 /從一個節點到該節點的子孫節點的全部路徑上包含相同數目的黑節點。

在樹的結構發生改變時(插入或者刪除操做),每每會破壞上述條件三、4或條件5,須要經過調整使得查找樹從新知足紅黑樹的條件。函數

 

預備知識

      說到當查找樹的結構發生改變時,紅黑樹的條件可能被破壞,須要經過調整使得查找樹從新知足紅黑樹的條件。調整能夠分爲兩類:一類是顏色調整,即改變某個節點的顏色;另外一類是結構調整,即改變檢索樹的結構關係。結構調整過程包含兩個基本操做:左旋(Rotate Left),右旋(RotateRight)性能

左旋

左旋的過程是將x的右子樹繞x逆時針旋轉,使得x的右子樹成爲x的父親,同時修改相關節點的引用。旋轉以後,二叉查找樹的屬性仍然知足。spa

TreeMap中左旋代碼以下:線程

//Rotate Left
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;
    }
}

右旋

右旋的過程是將x的左子樹繞x順時針旋轉,使得x的左子樹成爲x的父親,同時修改相關節點的引用。旋轉以後,二叉查找樹的屬性仍然知足。設計

TreeMap中右旋代碼以下:

//Rotate Right
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;
    }
}

方法剖析

  • get()

get(Object key)方法根據指定的key值返回對應的value,該方法調用了getEntry(Object key)獲得相應的entry,而後返回entry.value。所以getEntry()是算法的核心。算法思想是根據key的天然順序(或者比較器順序)對二叉查找樹進行查找,直到找到知足k.compareTo(p.key) == 0的entry。

 

具體代碼以下:

//getEntry()方法
final Entry<K,V> getEntry(Object key) {
    ......
    if (key == null)//不容許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;
}

 

  • put()

put(K key, V value)方法是將指定的key, value對添加到map裏。該方法首先會對map作一次查找,看是否包含該元組,若是已經包含則直接返回,查找過程相似於getEntry()方法;若是沒有找到則會在紅黑樹中插入新的entry,若是插入以後破壞了紅黑樹的約束,還須要進行調整(旋轉,改變某些節點的顏色)。

具體代碼以下:

public V put(K key, V value) {
    ......
    int cmp;
    Entry<K,V> parent;
    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);//建立並插入新的entry
    if (cmp < 0) parent.left = e;
    else parent.right = e;
    fixAfterInsertion(e);//調整
    size++;
    return null;
}

上述代碼的插入部分並不難理解:首先在紅黑樹上找到合適的位置,而後建立新的entry並插入(固然,新插入的節點必定是樹的葉子)。難點是調整函數fixAfterInsertion(),前面已經說過,調整每每須要1.改變某些節點的顏色,2.對某些節點進行旋轉。

調整函數fixAfterInsertion()的具體代碼以下,其中用到了上文中提到的rotateLeft()和rotateRight()函數。經過代碼咱們可以看到,狀況2實際上是落在狀況3內的。狀況4~狀況6跟前三種狀況是對稱的,所以圖解中並無畫出後三種狀況,讀者能夠參考代碼自行理解。

//紅黑樹調整函數fixAfterInsertion()
private void fixAfterInsertion(Entry<K,V> x) {
    x.color = RED;
    while (x != null && x != root && x.parent.color == RED) {
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {//若是y爲null,則視爲BLACK
                setColor(parentOf(x), BLACK);              // 狀況1
                setColor(y, BLACK);                        // 狀況1
                setColor(parentOf(parentOf(x)), RED);      // 狀況1
                x = parentOf(parentOf(x));                 // 狀況1
            } else {
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);                       // 狀況2
                    rotateLeft(x);                         // 狀況2
                }
                setColor(parentOf(x), BLACK);              // 狀況3
                setColor(parentOf(parentOf(x)), RED);      // 狀況3
                rotateRight(parentOf(parentOf(x)));        // 狀況3
            }
        } else {
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);              // 狀況4
                setColor(y, BLACK);                        // 狀況4
                setColor(parentOf(parentOf(x)), RED);      // 狀況4
                x = parentOf(parentOf(x));                 // 狀況4
            } else {
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);                       // 狀況5
                    rotateRight(x);                        // 狀況5
                }
                setColor(parentOf(x), BLACK);              // 狀況6
                setColor(parentOf(parentOf(x)), RED);      // 狀況6
                rotateLeft(parentOf(parentOf(x)));         // 狀況6
            }
        }
    }
    root.color = BLACK;
}
  • remove()

remove(Object key)的做用是刪除key值對應的entry,該方法首先經過上文中提到的getEntry(Object key)方法找到key值對應的entry,而後調用deleteEntry(Entry<K,V> entry)刪除對應的entry。因爲刪除操做會改變紅黑樹的結構,有可能破壞紅黑樹的約束,所以有可能要進行調整。

 也可參考該文https://zhuanlan.zhihu.com/p/24795143?refer=dreawer

相關文章
相關標籤/搜索