紅黑樹、TreeMap、TreeSet

事先聲明如下代碼基於JDK1.8版本html

參考資料

大部分圖片引自https://www.jianshu.com/p/e136ec79235c侵刪java

http://www.javashuo.com/article/p-gciojcqo-mx.htmlnode

http://www.javashuo.com/article/p-pwmenxbf-gg.html數據結構

http://www.javashuo.com/article/p-orraekjs-n.html函數

TreeMap用法

TreeMap這樣一種數據結構給了咱們什麼:性能

  1. 與map同樣的鍵值對結構存儲
  2. TreeMap間接繼承自SortedMap,元素有序,有序的tree也意味着這是一顆二叉搜索樹,所以它的查詢是log(n)級別的
  3. 不容許null的key
  4. 不容許重複的key
  5. 並不一樣步,若是須要同步,能夠利用Collections包裝一下Collections.synchronizedSortedMap()

若是你有上述的需求,其實也就是快速搜索,能夠考慮使用它this

紅黑樹

爲何是紅黑樹?二叉搜索樹?AVL?

TreeMap是經過什麼樣的數據結構來存儲數據的呢?紅黑樹3d

  • 首先TreeMap要求有序,因此它至少是一顆二叉搜索樹

那爲何二叉搜索樹又不行呢?code

在必定的狀況下,二叉搜索樹會退化成鏈表,失去了log(n)的搜索時間複雜度orm

以下圖所示,若是按照1,2,3,4這樣的順序去構成二叉搜索樹的話,它就會退化成一個鏈表,這樣的性能是無法接收的

image.png

那麼AVL樹又如何呢?爲何就選擇紅黑樹呢

紅黑樹因爲其特性,它並不追求徹底的平衡,更低的平衡要求對應着插入、刪除效率的提升(可能提升並不明顯甚至不如AVL,由於插入、刪除的第一步就是定位),搜索的效率下降,AVL相對紅黑樹而言,要求徹底的平衡,天然插入、刪除須要進行的操做更多,效率更低,而因爲徹底的平衡,搜索的效率就更高,綜合而言,選擇了實現起來更爲簡單,各方面更爲均衡的紅黑樹,就是如此

紅黑樹的性質

  1. 任何一個節點不是紅色就是黑色
  2. 根節點是黑色
  3. 任何兩個紅色節點不能直接相連
  4. 每一個葉子節點(Nil)節點都爲黑色,也就是null節點爲黑色
  5. 從根節點出發,到任意一個葉子節點的簡單路徑(不包含葉子節點)的黑色節點數目要相同

推論

  1. 若是一個節點存在黑色子節點,那麼它必定有兩個子節點,爲了知足性質5

左旋與右旋與着色

紅黑樹爲了維持上述5個性質,須要作出一些努力,這些努力就是旋轉着色

左旋示意圖:

image.png

語言總結一下就是:

左旋,右孩子成爲旋轉中心的父親節點,旋轉中心成爲右孩子的左孩子,此時旋轉中心的右孩子空缺,右孩子的左子樹失去了父親,把右孩子的左子樹置爲旋轉中心的右孩子

對應的TreeMap的rotateLeft方法以下

private void rotateLeft(Entry<K,V> p) {
    if (p != null) {
        Entry<K,V> r = p.right;
        //p的右孩子->r的左子樹
        p.right = r.left;
        if (r.left != null)
            //右孩子指向p.parent
            r.left.parent = p;
        r.parent = p.parent;
        //判斷p是否爲root以及p在parent中的位置
        if (p.parent == null)
            root = r;
        else if (p.parent.left == p)
            p.parent.left = r;
        else
            p.parent.right = r;
        //p成爲r的左孩子,r成爲p的父親
        r.left = p;
        p.parent = r;
    }
}

右旋示意圖

image.png

TreeMap中的右旋代碼

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;
    }
}

着色

Entry的color成員變量以下

boolean color = BLACK;

//上色方法以下
private static <K,V> void setColor(Entry<K,V> p, boolean c) {
    if (p != null)
        p.color = c;
}

紅黑樹的查找

紅黑樹本質仍是一顆二叉搜索樹,天然與二叉搜索樹的查找方式相差無幾

TreeMap中的實現以下:

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();
    @SuppressWarnings("unchecked")
    //若是有,使用自定義的排序規則
    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;
}

插入過程如何保持平衡?

插入過程的步驟
  • 利用已有的紅黑樹結構找到插入位置
  • 插入一個新的紅色節點
  • 調整
爲啥新插入的節點要是紅色呢?

因爲性質5的存在,插入節點黑色勢必致使紅黑樹的性質5的不知足,而插入紅色並不必定須要調整樹的結構

插入情景分類

1-3
  1. root爲null,直接插入而且,着色爲black便可
  2. 插入節點的key已經存在,在TreeMap中,找到對應的節點直接修改value
  3. 插入節點的父親節點爲黑色節點,直接插入便可,不會違背任何一條性質
  4. 插入節點的父親節點爲紅色節點
    1. 父親爲紅色,而且當且節點的祖父節點的另外一個子結點即叔叔節點也是紅色
    2. 父親爲紅色,叔叔節點爲黑色(包括Nil),其父親爲祖父節點的左孩子
      1. 插入節點爲父親節點的左孩子
      2. 插入節點爲父親節點的右孩子
    3. 父親爲紅色,叔叔節點爲黑色(包括Nil),其父親爲祖父節點的右孩子
      1. 插入節點爲父親節點的左孩子
      2. 插入節點爲父親節點的右孩子

4.2與4.3徹底對稱式的操做,以4.2爲例

4.1父親爲紅色,叔叔爲紅色

操做以下

  • 父親與叔叔節點置爲黑色
  • 祖父節點置爲紅色
  • 把祖父節點當成新插入的節點,向上遞歸

爲何如此操做?

當前節點爲紅,父親爲紅,違背性質4,修改父親爲黑,此時通過父親節點的路徑的黑色節點數量大於通過叔叔節點的路徑黑色節點數目,那就再把叔叔染黑,那麼此時父輩平衡,可是通過祖父的這條分支黑色節點數目多了一個,那麼再把祖父染紅便可,而後以祖父爲新插入的節點再插入便可

效果如圖:

image.png

4.2.1 父親爲紅色,叔叔爲黑色(Nil),父親爲祖父節點的左孩子,插入節點爲父親節點的左孩子

操做以下

  • 將父親置爲黑色
  • 將祖父置爲紅色
  • 以祖父爲中心,右旋

效果如圖:

image.png

爲何這麼操做?

不知足性質4,修改父親爲黑色,此時父親這邊的路徑黑色數量多1,那麼再修改祖父爲紅色,但此時通過叔叔的路徑黑色數量少1,此時以祖父爲中心,右旋,讓剛剛染色爲黑色的節點成爲父親,父親成爲叔叔的父親,便可知足平衡

4.2.2 父親爲紅色,叔叔爲黑色(Nil),父親爲祖父節點的左孩子,插入節點爲父親節點的右孩子

操做以下:

  • 以父親節點爲中心,左旋
  • 達到4.2.1的效果,而後按照上面的操做來

效果如圖:

image.png

爲何這麼操做?

就是爲了變成4.2.1,而後按照4.2.1的操做進行便可

4.3是4.2的鏡像版本,對稱過去就行了

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
		//1 若是root爲null,插入並返回便可
        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
                //2 若是找到了,直接修改value
                return t.setValue(value);
        } while (t != null);
    }
    else {
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
        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);
    //3 直接插入節點,默認color爲黑色
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}
fixAfterInsertion()方法
private void fixAfterInsertion(Entry<K,V> x) {
    //把color着色爲紅
    x.color = RED;

    while (x != null && x != root && x.parent.color == RED) {
        //父親爲祖父的左孩子
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            //y-叔叔節點
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                //叔叔爲紅--4.1,修改父親和叔叔的節點爲黑,祖父爲紅
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                //將祖父設置爲當前節點
                x = parentOf(parentOf(x));
            } else {
                //插入節點爲右孩子,則先左旋一下,從4.2.2 --> 4.2.1
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateLeft(x);
                }
                //4.2.1,修改父親爲黑色,祖父爲紅色,以祖父爲中心右旋
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateRight(parentOf(parentOf(x)));
            }
        } else {
            //對稱的,父親爲祖父的右孩子
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateRight(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }
    //若是x爲root,直接着色便可
    root.color = BLACK;
}

刪除過程如何保持平衡?

刪除過程分爲兩步:
  1. 定位到要刪除的節點,按照節點分類狀況刪除
  2. 經過旋轉、着色平衡紅黑樹
節點分類:
  1. 沒有孩子節點,直接刪除便可
  2. 有一個孩子節點,直接刪除該節點,用惟一子節點代替它的位置
  3. 有兩個孩子,尋找後繼節點(第一個比它大的節點),把後繼節點的內容複製過來,刪除後繼節點,此時的後繼節點不可能有兩個孩子節點,有的話,後繼節點必定不是它,刪除後繼節點,按照狀況1,2處理

後繼節點

後繼節點的選取,選投影到x軸上離它最近的兩個節點(確定有兩個)中的任意一個均可以,這裏選擇第一個比刪除節點大的節點,對應到樹中的位置以下

  • 若是刪除節點有右子樹,則對應着右子樹的最左節點

  • 若是沒有則對應着第一個向右走的節點,如圖所示

    image.png

    爲何是後繼節點?
    1. 後繼節點來替換,它大於>待刪除節點>待刪除節點左子樹全部節點,它是第一個大於待刪除節點的節點,它小於<右子樹的全部節點,即知足基本的二叉搜索樹的性質
    2. 只賦值,不修改顏色,即不破壞紅黑樹的性質,此時只須要考慮刪除後繼節點的影響便可

    後繼節點必定是樹末尾的節點,由於任何一個左右孩子非空的節點都不可能成爲最終的後繼節點,它遞歸下去最終到樹梢的位置

    刪除過程對應以下:

    image.png

如今完成了第一步,定位真正要刪除的節點,下面就是刪除這個節點以後如何平衡二叉樹?

刪除狀況分類

  1. 刪除節點爲紅色
  2. 刪除節點爲黑色,且爲父親節點的左孩子
    1. 兄弟節點爲紅色,(此時父節點,兄弟節點的子節點都爲黑色(Nil))
    2. 兄弟節點爲黑色,兄弟節點的兩個子結點也是黑色(Nil)
    3. 兄弟節點爲黑色,兄弟節點的左孩子爲紅色,右孩子爲黑色(Nil)
    4. 兄弟節點爲黑色,兄弟節點的右孩子爲紅色,左孩子任意
  3. 刪除節點爲黑色,且爲父親節點的右孩子,和上面是對稱的
    1. 兄弟節點爲紅色,(此時父節點,兄弟節點的子節點都爲黑色(Nil))
    2. 兄弟節點爲黑色,兄弟節點的兩個子結點也是黑色(Nil)
    3. 兄弟節點爲黑色,兄弟節點的左孩子爲紅色,右孩子爲黑色(Nil)
    4. 兄弟節點爲黑色,兄弟節點的右孩子爲紅色,左孩子任意

只討論1,2的狀況,3與2對稱操做

1 刪除節點爲紅色

紅色節點死有餘辜!!無需處理,刪除便可

2.2 刪除節點爲黑色,且爲父親節點的左孩子,兄弟節點爲黑色,兄弟節點的兩個子結點都是黑色

操做以下

  • 將兄弟節點置爲紅色
  • 將刪除節點的父親節點置爲新的待刪除節點

效果如圖:

image.png

why

假設R刪除了,那麼爲了達到平衡,P應該幫忙補一個黑色回來,那麼出問題了,任何包含P和S的路徑黑色就多出來1了,把S置爲紅色便可達到局部的平衡,把須要補黑色的需求向上傳遞

2.1 刪除節點爲黑色,且爲父親節點的左孩子,兄弟節點爲紅色

操做以下:

  • 將兄弟置爲黑色
  • 將父親置爲紅色
  • 以父親節點爲中心,左旋
  • 左旋以後從新設置兄弟節點

效果如圖:

image.png

why

向2.2/2.3/2.4靠攏

2.4 刪除節點爲黑色,且爲父親節點的左孩子,兄弟節點爲黑色,兄弟節點的右孩子爲紅色,左孩子任意

操做以下:

  • 父親節點的顏色賦值給兄弟節點
  • 將父親節點置爲黑色
  • 將兄弟節點的右孩子置爲黑色
  • 以父親節點爲中心左旋

效果如圖:

image.png

why

咱們的最終目的就是爲包含R的路徑補一個黑色節點

  1. 修改P爲黑色(不必定能保證補回一個黑色,有可能P就是黑色的)

  2. 以P爲中心左旋,讓S成爲包含R路徑的新root,那麼包含S的路徑黑色節點沒變,包含R的路徑的黑色節點因爲S的存在+1

左旋以後,P與SL爲父子關係,而P與SL又都爲任意顏色,不必定能保證性質4,因此在左旋以前將P與S顏色互換,可是此時包含SR(紅色)的右子樹黑色節點少1,將SR置爲黑色便可

2.3 刪除節點爲黑色,且爲父親節點的左孩子,兄弟節點爲黑色,兄弟節點的左孩子爲紅色,右孩子爲黑色(Nil)

操做以下:

  • 將兄弟節點的左孩子置爲黑色
  • 將兄弟節點置爲紅色
  • 以兄弟節點爲中心右旋獲得2.4的狀況
  • 按照2.4的方法處理

效果如圖:

image.png

刪除節點爲黑色,且爲父親節點的右孩子節點的操做與上述操做對稱

TreeMap中的刪除代碼

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

        V oldValue = p.value;
        deleteEntry(p);
        return oldValue;
    }
deleteEntry()
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.
    //left與right都非空,即尋找後繼節點
    if (p.left != null && p.right != null) {
        Entry<K,V> s = successor(p);
        //將s的值賦值給p,而後讓p指向s
        p.key = s.key;
        p.value = s.value;
        p = s;
    } // p has 2 children

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

    if (replacement != null) {
        // Link replacement to parent
        //把replace連接到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.
        //for GC
        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節點
        root = null;
    } else { //  No children. Use self as phantom replacement and unlink.
        //左右孩子均爲null
        if (p.color == BLACK)
            fixAfterDeletion(p);
		//斷開p與parent的鏈接
        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;
        }
    }
}
successor()
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
    if (t == null)
        return null;
    else if (t.right != null) {
        //右子樹的最左下節點
        Entry<K,V> p = t.right;
        while (p.left != null)
            p = p.left;
        return p;
    } else {
        //第一個向右走的節點
        Entry<K,V> p = t.parent;
        Entry<K,V> ch = t;
        while (p != null && ch == p.right) {
            ch = p;
            p = p.parent;
        }
        return p;
    }
}
fixAfterDeletion()
private void fixAfterDeletion(Entry<K,V> x) {
    while (x != root && colorOf(x) == BLACK) {
        //待刪除節點爲父親節點的左孩子且爲黑色
        if (x == leftOf(parentOf(x))) {
            //sib--兄弟節點
            Entry<K,V> sib = rightOf(parentOf(x));
			//2.1-->2.2/2.3/2.4
            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rotateLeft(parentOf(x));
                sib = rightOf(parentOf(x));
            }
			//2.2 設置兄弟節點爲紅色,x修改成parent
            if (colorOf(leftOf(sib))  == BLACK &&
                colorOf(rightOf(sib)) == BLACK) {
                setColor(sib, RED);
                x = parentOf(x);
            } else {
                //2.3-->2.4
                if (colorOf(rightOf(sib)) == BLACK) {
                    setColor(leftOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateRight(sib);
                    sib = rightOf(parentOf(x));
                }
                //2.4
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(rightOf(sib), BLACK);
                rotateLeft(parentOf(x));
                x = root;
            }
        } else { // symmetric
            Entry<K,V> sib = leftOf(parentOf(x));

            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rotateRight(parentOf(x));
                sib = leftOf(parentOf(x));
            }

            if (colorOf(rightOf(sib)) == BLACK &&
                colorOf(leftOf(sib)) == BLACK) {
                setColor(sib, RED);
                x = parentOf(x);
            } else {
                if (colorOf(leftOf(sib)) == BLACK) {
                    setColor(rightOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateLeft(sib);
                    sib = leftOf(parentOf(x));
                }
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(leftOf(sib), BLACK);
                rotateRight(parentOf(x));
                x = root;
            }
        }
    }
	//若是x爲red,直接設置爲x,便可彌補回丟失的黑色節點,若是是黑色,無法經過setColor來補回黑色,經過平衡的方式來補回黑色
    setColor(x, BLACK);
}

紅黑樹總結

總結來講,不管是插入也好,刪除也罷,紅黑樹保持平衡的策略是自底向下追求紅黑樹的平衡,把矛盾交給上層的節點解決

TreeMap的keySet、EntrySet、values淺析

TreeMap紅黑樹的代碼已經混雜在上面,再也不進行梳理

keySet()

public NavigableSet<K> navigableKeySet() {
    KeySet<K> nks = navigableKeySet;
    //若是已經建立了nks,直接返回,不然new一個內部類KeySet
    return (nks != null) ? nks : (navigableKeySet = new KeySet<>(this));
}
//成員變量navigableKeySet
private transient KeySet<K> navigableKeySet;

KeySet

//成員變量m
private final NavigableMap<E, ?> m;
//構造方法,其實就把TreeMap傳入了
KeySet(NavigableMap<E,?> map) { m = map; }

//簡單看幾個方法,發現都是調用m的方法
public int size() { return m.size(); }
public boolean isEmpty() { return m.isEmpty(); }
public boolean contains(Object o) { return m.containsKey(o); }
public void clear() { m.clear(); }

//包括iterator也是經過m訪問另外一個內部類
public Iterator<E> iterator() {
    if (m instanceof TreeMap)
        return ((TreeMap<E,?>)m).keyIterator();
    else
        return ((TreeMap.NavigableSubMap<E,?>)m).keyIterator();
}

EntrySet、Values的實現也都相差無幾,有點相似於外觀類,提供受限的方法調用

TreeSet簡析

看一下TreeSet的構造函數便可

public TreeSet() {
    this(new TreeMap<E,Object>());
}

TreeSet(NavigableMap<E,Object> m) {
    this.m = m;
}

private transient NavigableMap<E,Object> m;

add()

private static final Object PRESENT = new Object();

public boolean add(E e) {
    return m.put(e, PRESENT)==null;
}

顯然TreeSet基於TreeMap實現它的基本功能,能夠說它就是一個TreeSet,只不過每次add的value是一個final的obj

相關文章
相關標籤/搜索