面試舊敵之紅黑樹(直白介紹深刻理解)

讀完本文你將瞭解到:html

 

 

上篇文章 《重溫數據結構:二叉排序樹的查找、插入、刪除》 咱們提到,二叉排序樹的性能取決於二叉樹的層數:java

  • 最好的狀況是 O(logn),存在於徹底二叉排序樹狀況下,其訪問性能近似於折半查找;
  • 最差時候會是 O(n),好比插入的元素是有序的,生成的二叉排序樹就是一個鏈表,這種狀況下,須要遍歷所有元素才行(見下圖 b)。

shixinzhang

爲了改變排序二叉樹存在的不足,Rudolf Bayer 在 1972 年發明了另外一種改進後的排序二叉樹:紅黑樹,他將這種排序二叉樹稱爲「對稱二叉 B 樹」,而紅黑樹這個名字則由 Leo J. Guibas 和 Robert Sedgewick 於 1978 年首次提出。node

本文介紹了紅黑樹的基本性質和基本操做。算法

什麼是紅黑樹

紅黑樹本質上是一種二叉查找樹,但它在二叉查找樹的基礎上額外添加了一個標記(顏色),同時具備必定的規則。這些規則使紅黑樹保證了一種平衡,插入、刪除、查找的最壞時間複雜度都爲 O(logn)。數據結構

它的統計性能要好於平衡二叉樹(AVL樹),所以,紅黑樹在不少地方都有應用。好比在 Java 集合框架中,不少部分(HashMap, TreeMap, TreeSet 等)都有紅黑樹的應用,這些集合均提供了很好的性能。框架

因爲 TreeMap 就是由紅黑樹實現的,所以本文將使用 TreeMap 的相關操做的代碼進行分析、論證。性能

黑色高度

從根節點到葉節點的路徑上黑色節點的個數,叫作樹的黑色高度。ui

紅黑樹的 5 個特性

shixinzhang

紅黑樹在原有的二叉查找樹基礎上增長了以下幾個要求:spa

  1. Every node is either red or black.
  2. The root is black.
  3. Every leaf (NIL) is black.
  4. If a node is red, then both its children are black.
  5. For each node, all simple paths from the node to descendant leaves contain the same number of black nodes.

中文意思是:.net

  1. 每一個節點要麼是紅色,要麼是黑色;
  2. 根節點永遠是黑色的;
  3. 全部的葉節點都是是黑色的(注意這裏說葉子節點實際上是上圖中的 NIL 節點);
  4. 每一個紅色節點的兩個子節點必定都是黑色;
  5. 從任一節點到其子樹中每一個葉子節點的路徑都包含相同數量的黑色節點;

注意: 
性質 3 中指定紅黑樹的每一個葉子節點都是空節點,並且並葉子節點都是黑色。但 Java 實現的紅黑樹將使用 null 來表明空節點,所以遍歷紅黑樹時將看不到黑色的葉子節點,反而看到每一個葉子節點都是紅色的。

性質 4 的意思是:從每一個根到節點的路徑上不會有兩個連續的紅色節點,但黑色節點是能夠連續的。 
所以若給定黑色節點的個數 N,最短路徑的狀況是連續的 N 個黑色,樹的高度爲 N - 1;最長路徑的狀況爲節點紅黑相間,樹的高度爲 2(N - 1) 。

性質 5 是成爲紅黑樹最主要的條件,後序的插入、刪除操做都是爲了遵照這個規定。

紅黑樹並非標準平衡二叉樹,它以性質 5 做爲一種平衡方法,使本身的性能獲得了提高。

紅黑樹的左旋右旋

shixinzhang

紅黑樹的左右旋是比較重要的操做,左右旋的目的是調整紅黑節點結構,轉移黑色節點位置,使其在進行插入、刪除後仍能保持紅黑樹的 5 條性質。

好比 X 左旋(右圖轉成左圖)的結果,是讓在 Y 左子樹的黑色節點跑到 X 右子樹去。

咱們以 Java 集合框架中的 TreeMap 中的代碼來看下左右旋的具體操做方法:

指定節點 x 的左旋 (右圖轉成左圖):

//這裏 p 表明 x
private void rotateLeft(Entry p) {
    if (p != null) {
        Entry r = p.right; // p 是上圖中的 x,r 就是 y
        p.right = r.left;       // 左旋後,x 的右子樹變成了 y 的左子樹 β 
        if (r.left != null)         
            r.left.parent = p;  //β 確認父親爲 x
        r.parent = p.parent;        //y 取代 x 的第一步:認 x 的父親爲爹
        if (p.parent == null)       //要是 x 沒有父親,那 y 就是最老的根節點
            root = r;
        else if (p.parent.left == p) //若是 x 有父親而且是它父親的左孩子,x 的父親如今認 y 爲左孩子,不要 x 了
            p.parent.left = r;
        else                            //若是 x 是父親的右孩子,父親就認 y 爲右孩子,拋棄 x
            p.parent.right = r;
        r.left = p;     //y 逆襲成功,之前的爸爸 x 如今成了它的左孩子
        p.parent = r;
    }
}

能夠看到,x 節點的左旋就是把 x 變成 右孩子 y 的左孩子,同時把 y 的左孩子送給 x 當右子樹。

簡單點記就是:左旋把右子樹裏的一個節點(上圖 β)移動到了左子樹。

指定節點 y 的右旋(左圖轉成右圖):

private void rotateRight(Entry p) {
    if (p != null) {
        Entry 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;
    }
}

同理,y 節點的右旋就是把 y 變成 左孩子 x 的右孩子,同時把 x 的右孩子送給 x 當左子樹。

簡單點記就是:右旋把左子樹裏的一個節點(上圖 β)移動到了右子樹。

瞭解左旋、右旋的方法及意義後,就能夠了解紅黑樹的主要操做:插入、刪除。

紅黑樹的平衡插入

紅黑樹的插入主要分兩步:

  • 首先和二叉查找樹的插入同樣,查找、插入
  • 而後調整結構,保證知足紅黑樹狀態 
    • 對結點進行從新着色
    • 以及對樹進行相關的旋轉操做

紅黑樹的插入在二叉查找樹插入的基礎上,爲了從新恢復平衡,繼續作了插入修復操做。

二叉查找樹的插入

上篇文章 介紹過,二叉查找樹的就是一個二分查找,找到合適的位置就放進去。

插入後調整紅黑樹結構

紅黑樹的第 5 條特徵規定,任一節點到它子樹的每一個葉子節點的路徑中都包含一樣數量的黑節點。也就是說當咱們往紅黑樹中插入一個黑色節點時,會違背這條特徵。

同時第 4 條特徵規定紅色節點的左右孩子必定都是黑色節點,當咱們給一個紅色節點下插入一個紅色節點時,會違背這條特徵。

所以咱們須要在插入黑色節點後進行結構調整,保證紅黑樹始終知足這 5 條特徵。

調整思想

前面說了,插入一個節點後要擔憂違反特徵 4 和 5,數學裏最經常使用的一個解題技巧就是把多個未知數化解成一個未知數。咱們這裏採用一樣的技巧,把插入的節點直接染成紅色,這樣就不會影響特徵 5,只要專心調整知足特徵 4 就行了。這樣比同時知足 四、5 要簡單一些。

染成紅色後,咱們只要關心父節點是否爲紅,若是是紅的,就要把父節點進行變化,讓父節點變成黑色,或者換一個黑色節點當父親,這些操做的同時不能影響 不一樣路徑上的黑色節點數一致的規則。

注:插入後咱們主要關注插入節點的父親節點的位置,而父親節點位於左子樹或者右子樹的操做是相對稱的,這裏咱們只介紹一種,即插入位置的父親節點爲左子樹。

【插入、染紅後的調整有 2 種狀況:】

狀況1.父親節點和叔叔節點都是紅色:

shixinzhang

假設插入的是節點 N,這時父親節點 P 和叔叔節點 U 都是紅色,爺爺節點 G 必定是黑色。

紅色節點的孩子不能是紅色,這時無論 N 是 P 的左孩子仍是右孩子,只要同時把 P 和 U 染成黑色,G 染成紅色便可。這樣這個子樹左右兩邊黑色個數一致,也知足特徵 4。

可是這樣改變後 G 染成紅色,G 的父親若是是紅色豈不是又違反特徵 4 了? 
這個問題和咱們插入、染紅後一致,所以須要以 爺爺節點 G 爲新的調整節點,再次進行調整操做,以此循環,直到父親節點不是紅的,就沒有問題了。

狀況2.父親節點爲紅色,叔叔節點爲黑色:

shixinzhang

假設插入的是節點 N,這時父親節點 P 是紅色,叔叔節點 U 是黑色,爺爺節點 G 必定是黑色。

紅色節點的孩子不能是紅色,可是直接把父親節點 P 塗成黑色也不行,這條路徑多了個黑色節點。怎麼辦呢?

既然改變不了你,那咱們就此別過吧,我換一個更適合個人!

咱們怎麼把 P 弄走呢?看來看去,仍是右旋最合適,經過把 爺爺節點 G 右旋,P 變成了這個子樹的根節點,G 變成了 P 的右子樹。

右旋後 G 跑到了右子樹上,這時把 P 變成黑的,多了一個黑節點,再把 G 變成紅的,就平衡了!

上面講的是插入節點 N 在父親節點 P 的左孩子位置,若是 N 是 P 的右孩子,就須要多進行一次左旋,把狀況化解成上述狀況。

shixinzhang

N 位於 P 的右孩子位置,將 P 左旋,就化解成上述狀況了。

根據 TreeMap 的代碼來驗證這個過程:

下面是 TreeMap 在插入後進行調整的代碼,能夠看出來跟咱們分析的一致。

private void fixAfterInsertion(Entry x) {
    x.color = RED;  //直接染成紅色,少點麻煩
 
    //這裏分析的都是父親節點爲紅色的狀況,不是紅色就不用調整了
    while (x != null && x != root && x.parent.color == RED) {
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { // 插入節點 x 的父親節點位於左孩子    
            Entry y = rightOf(parentOf(parentOf(x)));  // y 是 x 的叔叔節點
            if (colorOf(y) == RED) {    //若是 y 也是紅色,只要把父親節點和 y 都變成黑色,爺爺節點變成紅的,就 Ok 了
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {    //若是叔叔節點 y 不是紅色,就須要右旋,讓父親節點變成根節點,爺爺節點去右子樹去,而後把父親節點變成黑色、爺爺節點變成紅色
                    //特殊狀況:x 是父親節點的右孩子,須要對父親節點進行左旋,把 x 移動到左子樹
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateLeft(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateRight(parentOf(parentOf(x)));
            }
        } else {    //和上面對稱的操做
            Entry 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)));
            }
        }
    }
    root.color = BLACK;
}

紅黑樹的平衡刪除

紅黑樹的插入平衡須要好好理解下,若是前面沒有理解,刪除後的調整平衡更加難懂,前方高能,請注意!

紅黑樹的刪除也是分兩步:

  1. 二叉查找樹的刪除
  2. 結構調整

二叉查找樹的刪除

上篇文章 介紹了,二叉查找樹的刪除分三種狀況:

  1. 要刪除的節點正好是葉子節點,直接刪除就 OK 了;
  2. 只有左孩子或者右孩子,直接把這個孩子上移放到要刪除的位置就行了;
  3. 有兩個孩子,就須要選一個合適的孩子節點做爲新的根節點,該節點稱爲 繼承節點。

三種狀況的圖片示意 
(圖來自:shmilyaw-hotmail-com.iteye.com/blog/183643…):

1.要刪除的節點正好是葉子節點,直接刪除就 OK 了(右圖有錯誤,應該是 z 不是 r)

shixinzhang

2.有左孩子或者右孩子,直接把這個孩子上移放到要刪除的位置就行了

shixinzhang

3.有兩個孩子,就須要選一個合適的孩子節點做爲新的根節點,該節點稱爲 繼承節點

shixinzhang

刪除後的結構調整

根據紅黑樹的第 5 個特性:

若是當前待刪除節點是紅色的,它被刪除以後對當前樹的特性不會形成任何破壞影響。 
而若是被刪除的節點是黑色的,這就須要進行進一步的調整來保證後續的樹結構知足要求。

這裏研究的是刪除黑色節點的狀況。

調整思想

爲了保證刪除節點父親節點左右兩邊黑色節點數一致,須要重點關注父親節點沒刪除的那一邊節點是否是黑色。若是刪除後父親節點另外一邊比刪除的一邊黑色節點多,就要想辦法搞到平衡,具體的平衡方法有以下幾種方法:

  1. 把父親節點另外一邊(即刪除節點的兄弟樹)其中一個節點弄成紅色,也少一個黑色
  2. 或者把另外一邊多的黑色節點轉過來一個

刪除節點在父親節點的左子樹仍是右子樹,調整方式都是對稱的,這裏以當前節點爲父節點的左孩子爲例進行分析。

【刪除後的調整主要分三步】:

第一步:

  • 兄弟若是是紅的,說明孩子都是黑的 【旋轉的狀況 1 】 
    • 把兄弟搞成黑的
    • 父親搞成紅的
    • 左旋轉父親(嘿嘿,兄弟給我分一個黑孩子)
    • 接下來對比旋轉後的兄弟

第一步解釋:

這一步的目的是將兄弟節點變成黑的,轉變成第二步兩種情形中的某一種狀況。

在作後續變化前,這棵樹仍是保持着原來的平衡。

第二步,有兩種狀況:

狀況1 :兄弟節點的孩子都是黑色

  • 把兄弟搞成紅的
  • continue 下一波(這個子樹搞完了,研究父親節點,去搞上一級樹,進入第三步)

第二步狀況 1 解釋:

這裏將兄弟節點變成紅色後,從它的父節點到下面的全部路徑就都統一少了 1 個,同時也不影響別的特徵,可是把兄弟節點變紅後,若是有父親節點也是紅的,就可能違反紅黑樹的特徵 4,所以須要到更高一級樹進行鑑別、調整。

shixinzhang

狀況2 :兄弟節點的孩子至多有一個是黑的

  • 把不是黑的那個孩子搞黑 【旋轉的狀況 2 】 
    • 兄弟搞紅
    • 兄弟右旋轉
    • 之後對比旋轉後的兄弟
  • 把兄弟塗成跟父親同樣的顏色 【旋轉的狀況 3 】
  • 而後把父親搞黑
  • 把兄弟的右孩子搞黑
  • 父親節點左旋
  • 研究根節點,進入第三步

第二步狀況 2 解釋:

旋轉的狀況 2 是將兄弟節點的左右孩子都移動到右邊,方便後續操做,以下圖所示:

shixinzhang

旋轉的狀況 3 將兄弟的孩子移到左邊來,同時黑色的父親變到了左邊(總之就是讓左邊多些黑色節點),以下圖所示:

shixinzhang

第三步:

  • 若是研究的不是根節點而且是黑的,從新進入第一步,研究上一級樹;
  • 若是研究的是根節點或者這個節點不是黑的,就退出 
    • 把研究的這個節點塗成黑的。

第三步解釋:

第三步中選擇根節點爲結束標誌,是由於在第二步中,有可能出現咱們正好給刪除黑色節點的子樹補上了一個黑色節點,同時不影響其餘子樹,這時咱們的調整已經完成,能夠直接設置調整節點 x = root,等於宣告調整結束。

由於咱們當前調整的可能只是一棵樹中間的子樹,這裏頭的節點可能還有父節點,這麼一直往上到根節點。當前子樹少了一個黑色節點,要保證總體合格仍是不夠的。

這裏須要在代碼裏有一個保證。假設這裏 B 已是紅色的了。那麼調整結束,最後對 B 節點,也就是調整目標 x 所指向的這個節點塗成黑色。這樣保證前面虧的那一個黑色節點就補回來了。

前面討論的這4種狀況是在當前節點是父節點的左子節點的條件下進行的。若是當前節點是父節點的右子節點,則能夠對應的作對稱的操做處理,過程也是同樣的。

其中具體旋轉方向根據調整節點在父節點的左/右位置決定。

根據 TreeMap 的代碼來驗證這個過程:

private void fixAfterDeletion(Entry x) {
    while (x != root && colorOf(x) == BLACK) {
        if (x == leftOf(parentOf(x))) {
            Entry sib = rightOf(parentOf(x));
 
            //左旋,把黑色節點移到左邊一個
            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rotateLeft(parentOf(x));
                sib = rightOf(parentOf(x));
            }
 
            if (colorOf(leftOf(sib))  == BLACK &&
                colorOf(rightOf(sib)) == BLACK) {
                setColor(sib, RED);
                x = parentOf(x);
            } else {
                if (colorOf(rightOf(sib)) == BLACK) {
                    setColor(leftOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateRight(sib);
                    sib = rightOf(parentOf(x));
                }
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(rightOf(sib), BLACK);
                rotateLeft(parentOf(x));
                x = root;
            }
        } else { //處理的節點在 右邊,相同邏輯,只不過旋轉的方向相反
            Entry 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;
            }
        }
    }
 
    setColor(x, BLACK);
}

當調整的節點屬於父親節點的左子樹時,調整方法對應的流程圖以下:

shixinzhang

當調整的節點屬於父親節點的右子樹時,調整方法也相似,旋轉的方向相對稱。

這裏列出刪除後調整的所有邏輯流程圖(右鍵新窗口打開圖片更清晰):

shixinzhang

總結

紅黑樹並非真正的平衡二叉樹,但在實際應用中,紅黑樹的統計性能要高於平衡二叉樹,但極端性能略差。

紅黑樹的插入、刪除調整邏輯比較複雜,但最終目的是知足紅黑樹的 5 個特性,尤爲是 4 和 5。

在插入調整時爲了簡化操做咱們直接把插入的節點塗成紅色,這樣只要保證插入節點的父節點不是紅色就能夠了。

而在刪除後的調整中,針對刪除黑色節點,所在子樹缺乏一個節點,須要進行彌補或者對別人形成一個黑色節點的傷害。具體調整方法取決於兄弟節點所在子樹的狀況。

紅黑樹的插入、刪除在樹形數據結構中算比較複雜的,理解起來比較難,但只要記住,紅黑樹有其特殊的平衡規則,而咱們爲了維持平衡,根據鄰樹的情況進行旋轉或者塗色。

紅黑樹這麼難理解,一定有其過人之處。它的有序、快速特性在不少場景下都有用到,好比 Java 集合框架的 TreeMap, TreeSet 等。

Thanks

coolingxyz 前輩寫過數據結構相關的課件,flash 動態演示數據結構算法,能夠去看看: 
xu-laoshi.cn/shujujiegou…

一個不錯的在線演示添加、刪除紅黑樹: 
sandbox.runjs.cn/show/2nngvn…

《算法導論》

en.wikipedia.org/wiki/Red–black_tree

www.cnblogs.com/skywang1234…

shmilyaw-hotmail-com.iteye.com/blog/183643…

blog.csdn.net/speedme/art…

blog.csdn.net/eson_15/art…

blog.csdn.net/v_july_v/ar…

dongxicheng.org/structure/r…

相關文章
相關標籤/搜索