1. 程式人生 > >紅黑樹(Red Black Tree)刪除詳解與實現(Java)

紅黑樹(Red Black Tree)刪除詳解與實現(Java)

  本篇要講的就是紅黑樹的刪除操作

  紅黑樹的刪除是紅黑樹操作中比較麻煩且比較有意思的一部分。

  在此之前,重申一遍紅黑樹的五個定義:

1. 紅黑樹的節點要不是黑色的要不是紅色的

    2. 紅黑樹的根節點一定是黑色的

    3. 紅黑樹的所有葉子節點都是黑色的(注意:紅黑樹的葉子節點指Nil節點)

    4. 紅黑樹任何路徑上不允許出現相鄰兩個紅色節點

    5. 從紅黑樹的任一節點開始向下到任意葉子節點所經過的黑色節點數目相同

  接著,請大家謹記你操作的物件都是一顆標準的紅黑樹,所以不要腦補過多不能存在的情況,如果你考慮的情況不在本文的討論範圍之內,可以往上看看是不是你的情況違反了五條規則其中某一條,若還有疑問,歡迎留言討論。

  

  D(Delete)表示待刪除節點

  P(Parent)表示待刪除節點的父節點

  S(Sibling)表示待刪除節點的兄弟節點

  U(Uncle)表示帶刪除節點的叔叔節點

  GP(Grandparent)表示待刪除節點的祖父節點

  XL(Left child of X)表示節點X的左子樹節點

  XR(Right child of X)表示節點X的右子樹節點

  刪除一個新的節點有以下四種情況:

    1. 刪除的節點是葉子節點(非Nil)

    2. 刪除的節點只有左子樹

    3. 刪除的節點只有右子樹

    *4. 刪除的節點同時擁有左子樹和右子樹

  其實只有上面前三種情況,對於第四種情況,可以找到待刪除節點的直接後繼節點

,用這個節點的值代替待刪除節點,接著情況轉變為刪除這個直接後繼節點,情況也變為前三種之一。

  因為有很多情況是不存在,待刪除節點是葉子節點(非Nil)的情況稍微複雜一些,

  我們下面先考慮待刪除的節點只有左子樹或只有右子樹的情況。

  不存在的情況包括

  ① 

  

  ②

  

  ☂ (san)

  

  ④

  

  ⑤

  

  ⑥

  

   請讀者分析一下上面不可能情況的原因,不復雜但一定要知道為什麼。兩個節點的顏色紅黑情況加上左右子樹情況,總共八種情況,上面已經排除了六種,剩下以下兩種可能的的情況。

  ①

  DL表示DL節點原本的值

  ②

  DR表示DR節點原本的值

  這兩種情況的調整操作比較簡單,直接用DL/DR的元素值代替D的元素,再把DL/DR直接刪去就好,操作過後不違反紅黑樹定義,刪除結束。

  刪除節點的四種情況已經解決了三種,剩下最後一種了。

  待刪除的節點是葉子節點的情況:

因為待刪除的節點有可能是紅色也可能是黑色。

  如果待刪除節點是紅色的,那直接刪去這個節點,刪除結束。

  如果待刪除節點是黑色的,根據父節點P和兄弟節點S的情況,可分為以下五種情況。

  情況1:父節點P是紅色節點

      或者    

  這兩種情況是一樣的,我們討論第一個圖就好,當把D刪去後,從P的左子樹下來的黑色節點數目少了一,對應的調整做法為,把P染成黑色,此時P左子樹的黑色結點數目恢復,但此時右子樹黑色結點數目多了一,再把S對應染成紅色即可

  圖例:

  

  情況2:兄弟節點S是紅色節點

       或者     

  只能是這兩種情形,做法是把P染成紅色,S染成黑色,然後以P為軸做相應的旋轉操作(如果D為P的左子樹節點則以P為軸做左旋操作,如果D為P的右子樹節點則以P為軸做右旋操作)

  圖例(以第一種情形為例):

  

到這裡就把情況二變成了情況一(父節點為紅色)的情況,接著按照情況一的處理方式進行操作。

   情況3:結點D的遠親侄子為紅色節點的情況

  

  此時父節點P的顏色可紅可黑,這種情況的調整做法是,交換P和S的顏色,然後把遠侄子節點SR/SL設定為黑色,再以P為軸做相應的旋轉操作(如果D為P的左子樹則左旋,如果D為P的右子樹則右旋)

  圖例(以第一種情形為例):

  

  調整前後從P點下來的所有路徑黑色節點數目沒有發生變化,刪除節點D後結束。(注意此處S的左子樹SL可以為Nil節點或者紅色節點,但依然是按照上面的規則進行調整,對結果沒有影響)

  情況4:節點D的近親侄子為紅色節點的情況

  

  注意此處節點D的遠侄子節點必須為Nil節點,否則就變成情況3了。這種情況的調整方式是,把S染成紅色,把近侄子節點SR/SL染成黑色,然後以節點S為軸做相應的旋轉操作(如果D為P的左子樹則以S為軸做右旋操作,如果D為P的右子樹則以S為軸做左旋操作)。

  圖例(以第一種情形為例)

  

  然後就真的變成情況3了......接著按照情況3的處理方式進行處理。

  情況5:節點D,P,S均為黑色節點

  

  以第一種情形為例,這種情況刪除D之後,從P的左子樹下來的黑色節點數目少了一,且沒有周圍也沒有紅節點來補全這個黑節點,做法就是把D刪去,然後把節點S染成紅色,這樣一來節點P的左右子樹路徑的黑色節點路徑就一樣了,但導致節點P整棵子樹的任意路徑的黑色節點數比其他路徑少了一,此時我們再從P開始(即把P當成D),但不再刪除P,向上繼續調整,直到根節點(一直是情況5)或者遇到情況1~4並調整後結束。

我看過幾篇文章,最後一種情況基本講到我這裡就已經結束了,所以我在這種情況上也因此多話了一點時間去理解。若此處有更詳細的例子,會更能幫助理解,所以我決定舉兩個例子,來說明什麼叫從P節點開始向上調整,哪種情況就是要直到根節點, 哪種情況就是遇到情況1~4,然後調整後結束

  從節點P往上依然是全黑的情況(父節點,兄弟節點均為黑色)

  

  從節點P往上是其他情況

  

  這裡只是舉個例子,無論是變成情況1~4的哪種,經過調整之後都無需再繼續上溯,因為此時黑色節點數目已經恢復,且例子裡面GP不是根節點,因為根節點不可能為紅色。

  下面倒序總結一下

  待刪除的節點是黑色葉子(非Nil)節點的情況

  

 

   

  

   

  待刪除的節點是紅色葉子節點的情況

  情況6 直接刪除該節點

  待刪除的節點只擁有左子樹或只擁有右子樹的情況

  

  待刪除的節點同時擁有左子樹和右子樹的情況

  情況9 找出直接後繼節點並轉變為情況1~8

  至此,關於紅黑樹刪除的所有情況均討論完畢,以上的每個字以及每個圖都是自己寫自己畫的,花了不少時間,希望大家多看看,結合圖理解比較形象,徹底搞懂紅黑樹的操作,程式碼卻是次要的,因為同一種思路也有不同的程式碼風格和實現方式。同時也希望這篇文章能對大家有幫助。

  下面是刪除的程式碼:

  總的公共方法是這樣的,找到該元素對應的節點,然後刪除該節點:

    public boolean delete(int elem) {
        if (null == this.root) {
            return false;
        } else {
            TreeNode node = this.root;
            // find out the node need to be deleted
            while (null != node) {
                if (node.getElem() == elem) {
                    deleteNode(node);
                    return true;
                } else if (node.getElem() > elem) {
                    node = node.getLeft();
                } else {
                    node = node.getRight();
                }
            }
            return false;
        }
    }
delete(int elem)

  刪除節點的方法為私有方法,包含了同時擁有左右子樹,只擁有左子樹以及只擁有右子樹的操作

    private void deleteNode(TreeNode node) {
        if(null  == node.getLeft() && null == node.getRight()) {
            if (node.getColor() == NodeColor.RED) {
                delete_red_leaf(node, true);
            } else {
                delete_black_leaf(node, true);
            }
        } else if (null == node.getLeft()) {
            // the node color must be black and the right child must be red node
            // replace the element of node with its right child's
            // cut off the the link between node and its right child
            node.setElem(node.getRight().getElem());
            node.setRight(null);
        } else if (null == node.getRight()) {
            node.setElem(node.getLeft().getElem());
            node.setLeft(null);
        } else {
            // both children are not null
            TreeNode next = node.getRight();
            while (null != next.getLeft()) {
                next = next.getLeft();
            }
            TreeUtils.swapTreeElem(node, next);
            deleteNode(next);
        }
    }
private void deleteNode(TreeNode node)

  由大及小,刪除的節點是紅色葉子節點的情況,注意此處待刪除的節點肯定不是根節點,所以不需要考慮該節點為根節點的情況

    private void delete_red_leaf(TreeNode node, boolean needDel) {
        TreeNode parent = node.getParent();
        if (node == parent.getLeft()) {
            parent.setLeft(null);
        } else {
            parent.setRight(null);
        }
    }
private void delete_red_leaf(TreeNode node, boolean needDel)

  最後就是最麻煩的刪除的刪除黑色葉子(非Nil)節點的情況,找出兄弟節點,找出遠侄子節點,找出近侄子節點。

    private void delete_black_leaf(TreeNode node, boolean needDel) {
        TreeNode parent = node.getParent();
        if (null != parent) {
            boolean nodeInLeft = parent.getLeft() == node;
            TreeNode sibling = nodeInLeft ? parent.getRight() : parent.getLeft();
            TreeNode remoteNephew = null == sibling ? null : (nodeInLeft ? sibling.getRight() : sibling.getLeft());
            TreeNode nearNephew = null == sibling ? null : (nodeInLeft ? sibling.getLeft() : sibling.getRight());
            if (sibling.getColor() == NodeColor.RED) {
                delete_sibling_red(node);
            } else if (null != remoteNephew && remoteNephew.getColor() == NodeColor.RED) {
                delete_remote_nephew_red(node);
            } else if (null != nearNephew && remoteNephew.getColor() == NodeColor.RED) {
                delete_near_nephew_red(node);
            } else {
                // the sibling is also a leaf
                if (parent.getColor() == NodeColor.RED) {
                    delete_parent_red(node);
                } else {
                    sibling.setColor(NodeColor.RED);
                    delete_black_leaf(parent, false);
                }
            }
        }
        if (needDel) {
            if (null == parent) {
              this.root = null;
            } else if (node.getParent().getLeft() == node) {
                parent.setLeft(null);
            } else {
                parent.setRight(null);
            }
        }
    }
private void delete_black_leaf(TreeNode node, boolean needDel)

  刪除葉子節點包含了另外一個引數 boolean needDel ,因為上面提到的有些情況需要繼續上溯,所以有些節點不能被刪除。

  紅黑樹所有操作大功告成,希望對大家的學習有所幫助。

   PS:請問大家有可以畫好看的二叉樹的軟體推薦嗎

  請尊重智慧財產權,引用轉載請通知作者!