史上最清晰的紅黑樹講解

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

整體介紹

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

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

TreeMap_base.png

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

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

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

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

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

預備知識

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

左旋

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

TreeMap_rotateLeft.png

TreeMap中左旋代碼以下:spa

//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)方法是將指定的keyvalue對添加到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。因爲刪除操做會改變紅黑樹的結構,有可能破壞紅黑樹的約束,所以有可能要進行調整。

 

上面對Java TreeMap的插入以及插入以後的調整過程給出了詳述。下面接着以Java TreeMap爲例,從源碼層面講解紅黑樹的刪除,以及刪除以後的調整過程。

尋找節點後繼

對於一棵二叉查找樹,給定節點t,其後繼(樹種比大於t的最小的那個元素)能夠經過以下方式找到:

  1. t的右子樹不空,則t的後繼是其右子樹中最小的那個元素。
  2. t的右孩子爲空,則t的後繼是其第一個向左走的祖先。

後繼節點在紅黑樹的刪除操做中將會用到。

TreeMap_successor.png

TreeMap中尋找節點後繼的代碼以下:

// 尋找節點後繼函數successor()
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
    if (t == null)
        return null;
    else if (t.right != null) {// 1. t的右子樹不空,則t的後繼是其右子樹中最小的那個元素
        Entry<K,V> p = t.right;
        while (p.left != null)
            p = p.left;
        return p;
    } else {// 2. t的右孩子爲空,則t的後繼是其第一個向左走的祖先
        Entry<K,V> p = t.parent;
        Entry<K,V> ch = t;
        while (p != null && ch == p.right) {
            ch = p;
            p = p.parent;
        }
        return p;
    }
}

remove()

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

getEntry()函數前面已經講解過,這裏重點放deleteEntry()上,該函數刪除指定的entry並在紅黑樹的約束被破壞時進行調用fixAfterDeletion(Entry<K,V> x)進行調整。

因爲紅黑樹是一棵加強版的二叉查找樹,紅黑樹的刪除操做跟普通二叉查找樹的刪除操做也就很是類似,惟一的區別是紅黑樹在節點刪除以後可能須要進行調整。如今考慮一棵普通二叉查找樹的刪除過程,能夠簡單分爲兩種狀況:

  1. 刪除點p的左右子樹都爲空,或者只有一棵子樹非空。
  2. 刪除點p的左右子樹都非空。

對於上述狀況1,處理起來比較簡單,直接將p刪除(左右子樹都爲空時),或者用非空子樹替代p(只有一棵子樹非空時);對於狀況2,能夠用p的後繼s(樹中大於x的最小的那個元素)代替p,而後使用狀況1刪除s(此時s必定知足狀況1,能夠畫畫看)。

基於以上邏輯,紅黑樹的節點刪除函數deleteEntry()代碼以下:

// 紅黑樹entry刪除函數deleteEntry()
private void deleteEntry(Entry<K,V> p) {
    modCount++;
    size--;
    if (p.left != null && p.right != null) {// 2. 刪除點p的左右子樹都非空。
        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) {// 1. 刪除點p只有一棵子樹非空。
        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 { // 1. 刪除點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;
        }
    }
}

上述代碼中佔據大量代碼行的,是用來修改父子節點間引用關係的代碼,其邏輯並不難理解。下面着重講解刪除後調整函數fixAfterDeletion()。首先請思考一下,刪除了哪些點纔會致使調整?只有刪除點是BLACK的時候,纔會觸發調整函數,由於刪除RED節點不會破壞紅黑樹的任何約束,而刪除BLACK節點會破壞規則4。

跟上文中講過的fixAfterInsertion()函數同樣,這裏也要分紅若干種狀況。記住,不管有多少狀況,具體的調整操做只有兩種:1.改變某些節點的顏色,2.對某些節點進行旋轉。

TreeMap_fixAfterDeletion.png

上述圖解的整體思想是:將狀況1首先轉換成狀況2,或者轉換成狀況3和狀況4。固然,該圖解並不意味着調整過程必定是從狀況1開始。經過後續代碼咱們還會發現幾個有趣的規則:a).若是是由狀況1以後緊接着進入的狀況2,那麼狀況2以後必定會退出循環(由於x爲紅色);b).一旦進入狀況3和狀況4,必定會退出循環(由於x爲root)。

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

private void fixAfterDeletion(Entry<K,V> x) {
    while (x != root && colorOf(x) == BLACK) {
        if (x == leftOf(parentOf(x))) {
            Entry<K,V> sib = rightOf(parentOf(x));
            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);                   // 狀況1
                setColor(parentOf(x), RED);             // 狀況1
                rotateLeft(parentOf(x));                // 狀況1
                sib = rightOf(parentOf(x));             // 狀況1
            }
            if (colorOf(leftOf(sib))  == BLACK &&
                colorOf(rightOf(sib)) == BLACK) {
                setColor(sib, RED);                     // 狀況2
                x = parentOf(x);                        // 狀況2
            } else {
                if (colorOf(rightOf(sib)) == BLACK) {
                    setColor(leftOf(sib), BLACK);       // 狀況3
                    setColor(sib, RED);                 // 狀況3
                    rotateRight(sib);                   // 狀況3
                    sib = rightOf(parentOf(x));         // 狀況3
                }
                setColor(sib, colorOf(parentOf(x)));    // 狀況4
                setColor(parentOf(x), BLACK);           // 狀況4
                setColor(rightOf(sib), BLACK);          // 狀況4
                rotateLeft(parentOf(x));                // 狀況4
                x = root;                               // 狀況4
            }
        } else { // 跟前四種狀況對稱
            Entry<K,V> sib = leftOf(parentOf(x));
            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);                   // 狀況5
                setColor(parentOf(x), RED);             // 狀況5
                rotateRight(parentOf(x));               // 狀況5
                sib = leftOf(parentOf(x));              // 狀況5
            }
            if (colorOf(rightOf(sib)) == BLACK &&
                colorOf(leftOf(sib)) == BLACK) {
                setColor(sib, RED);                     // 狀況6
                x = parentOf(x);                        // 狀況6
            } else {
                if (colorOf(leftOf(sib)) == BLACK) {
                    setColor(rightOf(sib), BLACK);      // 狀況7
                    setColor(sib, RED);                 // 狀況7
                    rotateLeft(sib);                    // 狀況7
                    sib = leftOf(parentOf(x));          // 狀況7
                }
                setColor(sib, colorOf(parentOf(x)));    // 狀況8
                setColor(parentOf(x), BLACK);           // 狀況8
                setColor(leftOf(sib), BLACK);           // 狀況8
                rotateRight(parentOf(x));               // 狀況8
                x = root;                               // 狀況8
            }
        }
    }
    setColor(x, BLACK);
}

TreeSet

前面已經說過TreeSet是對TeeMap的簡單包裝,對TreeSet的函數調用都會轉換成合適的TeeMap方法,所以TreeSet的實現很是簡單。這裏再也不贅述。

// TreeSet是對TreeMap的簡單包裝
public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable
{
    ......
    private transient NavigableMap<E,Object> m;
    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();
    public TreeSet() {
        this.m = new TreeMap<E,Object>();// TreeSet裏面有一個TreeMap
    }
    ......
    public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }
    ......
}
相關文章
相關標籤/搜索