史上最清晰的紅黑樹講解(上)

本文github地址java

本文以Java TreeMap爲例,從源代碼層面,結合詳細的圖解,剝繭抽絲地講解紅黑樹(Red-Black tree)的插入,刪除以及由此產生的調整過程。git

整體介紹

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

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

TreeMap_base.png

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

SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));markdown

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

  1. 每一個節點要麼是紅色,要麼是黑色。
  2. 根節點必須是黑色
  3. 紅色節點不能連續(也便是,紅色節點的孩子和父親都不能是紅色)。
  4. 對於每一個節點,從該點至null(樹尾端)的任何路徑,都含有相同個數的黑色節點。

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

預備知識

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

左旋

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

TreeMap_rotateLeft.png

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_rotateRight.png

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) == 0entry

TreeMap_getEntry.png

具體代碼以下:

//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.對某些節點進行旋轉。

TreeMap_put.png

調整函數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。因爲刪除操做會改變紅黑樹的結構,有可能破壞紅黑樹的約束,所以有可能要進行調整。

有關remove()的具體講解將放到下一篇博文當中,敬請期待!

相關文章
相關標籤/搜索