Java集合詳解6:這次,從頭到尾帶你解讀Java中的紅黑樹

Java技術江湖發表於2019-11-07

《Java集合詳解系列》是我在完成夯實Java基礎篇的系列部落格後準備開始寫的新系列。

這些文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裡檢視

https://github.com/h2pl/Java-Tutorial

喜歡的話麻煩點下Star、fork哈

文章首發於我的個人部落格:

www.how2playlife.com

什麼是紅黑樹

首先,什麼是紅黑樹呢? 紅黑樹是一種“平衡的”二叉查詢樹,它是一種經典高效的演算法,能夠保證在最壞的情況下動態集合操作的時間為O(lgn)。紅黑樹每個節點包含5個域,分別為color,key,left,right和p。 color是在每個節點上增加的一個儲存位表示節點的顏色,可以是RED或者BLACK。key為結點中的value值,left,right為該結點的左右孩子指標,沒有的話為NIL,p是一個指標,是指向該節的父節點。如下圖(來自維基百科)表示就是一顆紅黑樹,NIL為指向外結點的指標。(外結點視為沒有key的結點)

   紅黑樹有什麼性質呢?一般稱為紅黑性質,有以下五點:
 1)每個結點或者是紅的或者是黑的;
 2)根結點是黑的;
 3)每個葉結點(NIL)是黑的;
 4)如果一個結點是紅的,則它的兩個孩子都是黑的;
 5)對每個結點,從該結點到其他其子孫結點的所有路徑上包含相同數目的黑結點。
   為了後面的分析,我們還得知道以下知識點。
(1)黑高度:從某個結點x出發(不包括該結點)到達一個葉結點的任意一條路徑上,黑色結點的個數稱為該結點x的黑高度。
(2)一顆有n個內結點的紅黑樹的高度至多為2lg(n+1)。   (內結點視為紅黑樹中帶關鍵字的結點)
(3)包含n個內部節點的紅黑樹的高度是 O(log(n))。

定義

紅黑樹是特殊的二叉查詢樹,又名R-B樹(RED-BLACK-TREE),由於紅黑樹是特殊的二叉查詢樹,即紅黑樹具有了二叉查詢樹的特性,而且紅黑樹還具有以下特性:

  • 1.每個節點要麼是黑色要麼是紅色

  • 2.根節點是黑色

  • 3.每個葉子節點是黑色,並且為空節點(還有另外一種說法就是,每個葉子結點都帶有兩個空的黑色結點(被稱為黑哨兵),如果一個結點n的只有一個左孩子,那麼n的右孩子是一個黑哨兵;如果結點n只有一個右孩子,那麼n的左孩子是一個黑哨兵。)

  • 4.如果一個節點是紅色,則它的子節點必須是黑色

  • 5.從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點。

有幾點需要注意的是:

1.特性3中指定紅黑樹的每個葉子節點都是空節點,但是在Java實現中紅黑樹將使用null代表空節點,因此遍歷紅黑樹時看不到黑色的葉子節點,反而見到的葉子節點是紅色的

2.特性4保證了從根節點到葉子節點的最長路徑的長度不會超過任何其他路徑的兩倍,例如黑色高度為3的紅黑樹,其最短路徑(路徑指的是根節點到葉子節點)是2(黑節點-黑節點-黑節點),其最長路徑為4(黑節點-紅節點-黑節點-紅節點-黑節點)。

實踐

紅黑樹操作

插入操作

首先紅黑樹在插入節點的時,我們設定插入節點的顏色為 紅色,如果插入的是黑色節點,必然會違背特性5,即改變了紅黑樹的黑高度,如下插入紅色結點又存在著幾種情況:

1. 黑父

如圖所示,這種情況不會破壞紅黑樹的特性,即不需要任何處理

2. 紅父

當其父親為紅色時又會存在以下的情況

  • 紅叔

紅叔的情況,其實相對來說比較簡單的,如下圖所示,只需要通過修改父、叔的顏色為黑色,祖的顏色為紅色,而且回去遞迴的檢查祖節點即可

  • 黑叔

黑叔的情況有如下幾種,這幾種情況下是不能夠通過修改顏色達到平衡的效果,因此會通過旋轉的操作,紅黑樹種有兩種旋轉操作,左旋和右旋(現在存在的疑問,什麼時候使用到左旋,什麼時候使用到右旋)

  • Case 1:[先右旋,在改變顏色(根節點必須為黑色,其兩個子節點為紅色,叔節點不用改變)],如下圖所示,注意省略黑哨兵節點

  • Case 2:[先左旋變成Case1中的情況,再右旋,最後改變顏色(根節點必須為黑色,其兩個子節點為紅色,叔節點不用改變)],如下圖所示,注意省略黑哨兵節點

  • Case 3:[先左旋,最後改變顏色(根節點必須為黑色,其兩個子節點為紅色,叔節點不用改變)],如下圖所示,注意省略黑哨兵節點

  • Case 4:[先右旋變成Case 3的情況,再左旋,最後改變顏色(根節點必須為黑色,其兩個子節點為紅色,叔節點不用改變)],如下圖所示,注意省略黑哨兵節點

以上就是紅黑樹新增節點所有可能的操作,下面會介紹紅黑樹中的刪除操作

刪除操作

刪除操作相比於插入操作情況更加複雜,刪除一個節點可以大致分為三種情況:

  • 1.刪除的節點沒有孩子節點,即當前節點為葉子節點,這種可以直接刪除

  • 2.刪除的節點有一個孩子節點,這種需要刪除當前節點,並使用其孩子節點頂替上來

  • 3.刪除的節點有兩個孩子節點,這種需要先找到其後繼節點(樹中大於節點的最小的元素);然後將其後繼節點的內容複製到該節點上,其後繼節點就相當於該節點的替身, 需要注意的是其後繼節點一定不會有兩個孩子節點(這點應該很好理解,如果後繼節點有左孩子節點,那麼當前的後繼節點肯定不是最小的,說明後繼節點只能存在沒有孩子節點或者只有一個右孩子節點),即這樣就將問題轉換成為1,2中的方式。

在講述修復操作之前,首先需要明白幾點,

1.對於紅黑樹而言,單支節點的情況只有如下圖所示的一種情況,即為當前節點為黑色,其孩子節點為紅色,(1.假設當前節點為紅色,其兩個孩子節點必須為黑色,2.若有孫子節點,則必為黑色,導致黑子數量不等,而紅黑樹不平衡)

2.由於紅黑樹是特殊的二叉查詢樹,它的刪除和二叉查詢樹型別,真正的刪除點即為刪除點A的中序遍歷的後繼(前繼也可以),通過紅黑樹的特性可知這個後繼必然最多隻能有一個孩子,其這個孩子節點必然是右孩子節點,從而為單支情況(即這個後繼節點只能有一個紅色孩子或沒有孩子)

下面將詳細介紹,在執行刪除節點操作之後,將通過修復操作使得紅黑樹達到平衡的情況。

  • Case 1:被刪除的節點為紅色,則這節點必定為葉子節點(首先這裡的被刪除的節點指的是真正刪除的節點,通過上文得知的真正刪除的節點要麼是節點本身,要麼是其後繼節點,若是節點本身則必須為葉子節點,不為葉子節點的話其會有左右孩子,則真正刪除的是其右孩子樹上的最小值,若是後繼節點,也必須為葉子節點,若不是則其也會有左右孩子,從而和2中相違背),這種情況下刪除紅色葉節點就可以了,不用進行其他的操作了。

  • Case 2:被刪除的節點是黑色,其子節點是紅色,將其子節點頂替上來並改變其顏色為黑色,如下圖所示

  • Case 3:被刪除的節點是黑色,其子節點也是黑色,將其子節點頂替上來,變成了雙黑的問題,此時有以下情況

    • Case 1:新節點的兄弟節點為 紅色,此時若新節點在左邊則做左旋操作,否則做右旋操作,之後再將其父節點顏色改變為紅色,兄弟節點

從圖中可以看出,操作之後紅黑樹並未達到平衡狀態,而是變成的 黑兄的情況

  • Case 2:新節點的兄弟節點為 黑色,此時可能有如下情況

    • 紅父二黑侄:將父節點變成黑色,兄弟節點變成紅色,新節點變成黑色即可,如下圖所示

  • 黑父二黑侄:將父節點變成新節點的顏色,新節點變成黑色,兄弟節點染成紅色,還需要繼續以父節點為判定點繼續判斷,如下圖所示

  • 紅侄:

情況一:新節點在右子樹,紅侄在兄弟節點左子樹,此時的操作為右旋,並將兄弟節點變為父親的顏色,父親節點變為黑色,侄節點變為黑色,如下圖所示

情況二:新節點在右子樹,紅侄在兄弟節點右子樹,此時的操作為先左旋,後右旋並將侄節點變為父親的顏色,父節點變為黑色,如下圖所示

情況三:新節點在左子樹,紅侄在兄弟節點左子樹,此時的操作為先右旋在左旋並將侄節點變為父親的顏色,父親節點變為黑色,如下圖所示

情況四:新節點在右子樹,紅侄在兄弟節點右子樹,此時的操作為左旋,並將兄弟節點變為父節點的顏色,父親節點變為黑色,侄節點變為黑色,如下圖所示

紅黑樹實現

如下是使用JAVA程式碼實現紅黑樹的過程,主要包括了插入、刪除、左旋、右旋、遍歷等操作

插入
/* 插入一個節點
 * @param node
 */
private void insert(RBTreeNode<T> node){
    int cmp;
    RBTreeNode<T> root = this.rootNode;
    RBTreeNode<T> parent = null;
    //定位節點新增到哪個父節點下
    while(null != root){
        parent = root;
        cmp = node.key.compareTo(root.key);
        if (cmp < 0){
            root = root.left;
        } else {
            root = root.right;
        }
    }
    node.parent = parent;
    //表示當前沒一個節點,那麼就當新增的節點為根節點
    if (null == parent){
        this.rootNode = node;
    } else {
        //找出在當前父節點下新增節點的位置
        cmp = node.key.compareTo(parent.key);
        if (cmp < 0){
            parent.left = node;
        } else {
            parent.right = node;
        }
    }
    //設定插入節點的顏色為紅色
    node.color = COLOR_RED;
    //修正為紅黑樹
    insertFixUp(node);
}
/**
 * 紅黑樹插入修正
 * @param node
 */
private void insertFixUp(RBTreeNode<T> node){
    RBTreeNode<T> parent,gparent;
    //節點的父節點存在並且為紅色
    while( ((parent = getParent(node)) != null) && isRed(parent)){
        gparent = getParent(parent);
        //如果其祖父節點是空怎麼處理
        // 若父節點是祖父節點的左孩子
        if(parent == gparent.left){
            RBTreeNode<T> uncle = gparent.right;
            if ((null != uncle) && isRed(uncle)){
                setColorBlack(uncle);
                setColorBlack(parent);
                setColorRed(gparent);
                node = gparent;
                continue;
            }
            if (parent.right == node){
                RBTreeNode<T> tmp;
                leftRotate(parent);
                tmp = parent;
                parent = node;
                node = tmp;
            }
            setColorBlack(parent);
            setColorRed(gparent);
            rightRotate(gparent);
        } else {
            RBTreeNode<T> uncle = gparent.left;
            if ((null != uncle) && isRed(uncle)){
                setColorBlack(uncle);
                setColorBlack(parent);
                setColorRed(gparent);
                node = gparent;
                continue;
            }
            if (parent.left == node){
                RBTreeNode<T> tmp;
                rightRotate(parent);
                tmp = parent;
                parent = node;
                node = tmp;
            }
            setColorBlack(parent);
            setColorRed(gparent);
            leftRotate(gparent);
        }
    }
    setColorBlack(this.rootNode);
}

插入節點的操作主要分為以下幾步:

  • 1.定位:即遍歷整理紅黑樹,確定新增的位置,如上程式碼中insert方法中就是在找到新增的位置

  • 2.修復:這也就是前面介紹的,新增元素後可能會使得紅黑樹不在滿足其特性,這時候需要通過變色、旋轉來調整紅黑樹,也就是如上程式碼中insertFixUp方法

刪除節點

如下為刪除節點的程式碼

private void remove(RBTreeNode<T> node){
    RBTreeNode<T> child,parent;
    boolean color;
    //被刪除節點左右孩子都不為空的情況
    if ((null != node.left) && (null != node.right)){
        //獲取到被刪除節點的後繼節點
        RBTreeNode<T> replace = node;
        replace = replace.right;
        while(null != replace.left){
            replace = replace.left;
        }
        //node節點不是根節點
        if (null != getParent(node)){
            //node是左節點
            if (getParent(node).left == node){
                getParent(node).left = replace;
            } else {
                getParent(node).right = replace;
            }
        } else {
            this.rootNode = replace;
        }
        child = replace.right;
        parent = getParent(replace);
        color = getColor(replace);
        if (parent == node){
            parent = replace;
        } else {
            if (null != child){
                setParent(child,parent);
            }
            parent.left = child;
            replace.right = node.right;
            setParent(node.right, replace);
        }
        replace.parent = node.parent;
        replace.color = node.color;
        replace.left = node.left;
        node.left.parent = replace;
        if (color == COLOR_BLACK){
            removeFixUp(child,parent);
        }
        node = null;
        return;
    }
    if (null != node.left){
        child = node.left;
    } else {
        child = node.right;
    }
    parent = node.parent;
    color = node.color;
    if (null != child){
        child.parent = parent;
    }
    if (null != parent){
        if (parent.left == node){
            parent.left = child;
        } else {
            parent.right = child;
        }
    } else {
        this.rootNode = child;
    }
    if (color == COLOR_BLACK){
        removeFixUp(child, parent);
    }
    node = null;
}
/**
 * 刪除修復
 * @param node
 * @param parent
 */
private void removeFixUp(RBTreeNode<T> node, RBTreeNode<T> parent){
    RBTreeNode<T> other;
    //node不為空且為黑色,並且不為根節點
    while ((null == node || isBlack(node)) && (node != this.rootNode) ){
        //node是父節點的左孩子
        if (node == parent.left){
            //獲取到其右孩子
            other = parent.right;
            //node節點的兄弟節點是紅色
            if (isRed(other)){
                setColorBlack(other);
                setColorRed(parent);
                leftRotate(parent);
                other = parent.right;
            }
            //node節點的兄弟節點是黑色,且兄弟節點的兩個孩子節點也是黑色
            if ((other.left == null || isBlack(other.left)) &&
                    (other.right == null || isBlack(other.right))){
                setColorRed(other);
                node = parent;
                parent = getParent(node);
            } else {
                //node節點的兄弟節點是黑色,且兄弟節點的右孩子是紅色
                if (null == other.right || isBlack(other.right)){
                    setColorBlack(other.left);
                    setColorRed(other);
                    rightRotate(other);
                    other = parent.right;
                }
                //node節點的兄弟節點是黑色,且兄弟節點的右孩子是紅色,左孩子是任意顏色
                setColor(other, getColor(parent));
                setColorBlack(parent);
                setColorBlack(other.right);
                leftRotate(parent);
                node = this.rootNode;
                break;
            }
        } else {
            other = parent.left;
            if (isRed(other)){
                setColorBlack(other);
                setColorRed(parent);
                rightRotate(parent);
                other = parent.left;
            }
            if ((null == other.left || isBlack(other.left)) &&
                    (null == other.right || isBlack(other.right))){
                setColorRed(other);
                node = parent;
                parent = getParent(node);
            } else {
                if (null == other.left || isBlack(other.left)){
                    setColorBlack(other.right);
                    setColorRed(other);
                    leftRotate(other);
                    other = parent.left;
                }
                setColor(other,getColor(parent));
                setColorBlack(parent);
                setColorBlack(other.left);
                rightRotate(parent);
                node = this.rootNode;
                break;
            }
        }
    }
    if (node!=null)
        setColorBlack(node);
}

刪除節點主要分為幾種情況去做對應的處理:

  • 1.刪除節點,按照如下三種情況去刪除節點
    • 1.真正刪除的節點沒有子節點
    • 2.真正刪除的節點有一個子節點
    • 3.正在刪除的節點有兩個子節點
  • 2.修復紅黑樹的特性,如程式碼中呼叫removeFixUp方法修復紅黑樹的特性。

3.總結

以上主要介紹了紅黑樹的一些特性,包括一些操作詳細的解析了裡面的過程,寫的時間比較長,感覺確實比較難理清楚。後面會持續的理解更深入,若有存在問題的地方,請指正。

參考文章

紅黑樹(五)之 Java的實現

通過分析 JDK 原始碼研究 TreeMap 紅黑樹演算法實現

紅黑樹

(圖解)紅黑樹的插入和刪除

紅黑樹深入剖析及Java實現

微信公眾號

Java技術江湖

如果大家想要實時關注我更新的文章以及分享的乾貨的話,可以關注我的公眾號【Java技術江湖】一位阿里 Java 工程師的技術小站,作者黃小斜,專注 Java 相關技術:SSM、SpringBoot、MySQL、分散式、中介軟體、叢集、Linux、網路、多執行緒,偶爾講點Docker、ELK,同時也分享技術乾貨和學習經驗,致力於Java全棧開發!

Java工程師必備學習資源: 一些Java工程師常用學習資源,關注公眾號後,後臺回覆關鍵字 “Java” 即可免費無套路獲取。

我的公眾號

個人公眾號:黃小斜

黃小斜是跨考軟體工程的 985 碩士,自學 Java 兩年,拿到了 BAT 等近十家大廠 offer,從技術小白成長為阿里工程師。

作者專注於 JAVA 後端技術棧,熱衷於分享程式設計師乾貨、學習經驗、求職心得和程式人生,目前黃小斜的CSDN部落格有百萬+訪問量,知乎粉絲2W+,全網已有10W+讀者。

黃小斜是一個斜槓青年,堅持學習和寫作,相信終身學習的力量,希望和更多的程式設計師交朋友,一起進步和成長!關注公眾號【黃小斜】後回覆【原創電子書】即可領取我原創的電子書《菜鳥程式設計師修煉手冊:從技術小白到阿里巴巴Java工程師》

程式設計師3T技術學習資源: 一些程式設計師學習技術的資源大禮包,關注公眾號後,後臺回覆關鍵字 “資料” 即可免費無套路獲取。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69906029/viewspace-2659773/,如需轉載,請註明出處,否則將追究法律責任。

相關文章