十五、數據結構---紅黑樹


 二叉查找樹
    二叉查找樹,也稱有序二叉樹(ordered binary tree),或已排序二叉樹(sorted binary tree),是指一棵空樹或者具有下列性質的二叉樹:
    若任意結點的左子樹不空,則左子樹上的所有結點的值均小於它的根結點的值;
    若任意結點的右子樹不空,則右子樹上的所有結點的值均大於它的根結點的值;
    任意結點的左,右子樹也分別爲二叉查找樹。
    沒有鍵值相等的節點。
 注意:具有n個結點的二叉樹的深度至少是log2(N)+1,至多是n,所以二叉查找樹的查找性能(時間複雜度)在O(log2(N))和O(n)之間。
 一、紅黑樹的介紹
   紅黑樹(Red-Black Tree,簡稱R-B Tree),它是一種特殊的二叉查找樹。
   意味着它滿足二叉查找樹的特徵:任意一個結點所包含的鍵值,大於等於左孩子的鍵值,小於等於右孩子的鍵值。
   紅黑樹的每個結點上都有存儲位表示結點的顏色,顏色是紅(Red)或黑(Black)。
   紅黑樹的特性:
   (1)每個結點或者是黑色,或者是紅色。
   (2)根結點是黑色。
   (3)每個葉子結點是黑色。[注意:這裏的葉子結點,是指爲空的葉子結點!]
   (4)如果一個結點是紅色的,則它的子節點必須是黑色的。
   (5)從根結點到所有葉子結點上的黑色結點數量是相同的。
   上述的性質約束了紅黑樹的關鍵:從根到葉子的最長可能路徑不多於最短可能路徑的兩倍長。
   得到這個結論的理由是:
    1.紅黑樹中最短的可能路徑是全部爲黑色結點的路徑
    2.紅黑樹中最長的可能路徑是紅黑相同的路徑


    可以看到根結點到所有NULL LEAF節點(即葉子結點)所經過的黑色節點都是2個。
    另外從這張圖上我們還能得到一個結論:紅黑樹並不是高度的平衡樹。所謂平衡樹指的是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,但是我們看:
    .最左邊的路徑0026-->0017-->0012-->0010-->0003-->NULL LEAF,它的高度爲5
    .最後邊的路徑0026-->0041-->0047-->NULL LEAF,它的高度爲3
    左右子樹的高度差爲2,因此紅黑樹並不是高度平衡的,它放棄了高度平衡的特性而只追求部分平衡,這種特性降低了插入,刪除時對樹旋轉的要求,從而提升了樹的整體性能。


 二、紅黑樹的Java實現(代碼說明)
    紅黑樹的基本操作是添加,刪除和旋轉。在對紅黑樹進行添加或刪除後,會用到旋轉方法。爲什麼呢?道理很簡單,添加或刪除紅黑樹中的結點之後,紅黑樹就發生了變化,可能不滿足紅黑樹的5條性質,也就不再是一棵紅黑樹了,而是一棵普通的樹。而通過旋轉,可以使這課樹重新成爲紅黑樹。簡單點說,旋轉的目的是讓樹保持紅黑樹的特性。
    旋轉包括兩種:左旋 和 右旋。
    下面分別對紅黑樹的基本操作進行介紹。


 (1)基本定義


    public class RBTree<T extends Comparable<T>> {


    private RBTNode<T> mRoot;    // 根結點


    private static final boolean RED   = false;
    private static final boolean BLACK = true;


    public class RBTNode<T extends Comparable<T>> {
        boolean color;        // 顏色
        T key;                // 關鍵字(鍵值)
        RBTNode<T> left;    // 左孩子
        RBTNode<T> right;    // 右孩子
        RBTNode<T> parent;    // 父結點


        public RBTNode(T key, boolean color, RBTNode<T> parent, RBTNode<T> left, RBTNode<T> right) {
            this.key = key;
            this.color = color;
            this.parent = parent;
            this.left = left;
            this.right = right;
        }


    }


    ...
}


    RBTree是紅黑樹對應的類,RBTNode是紅黑樹的節點類。在RBTree中包含了根結點mRoot和紅黑樹的相關API。


 (2)左旋
    對X進行左旋,意味着"將x變成一個左結點"。

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

    左旋的實現代碼(Java)
    /* 
     * 對紅黑樹的節點(x)進行左旋轉
     *
     * 左旋示意圖(對節點x進行左旋):
     *      px                              px
     *     /                               /
     *    x                               y                
     *   /  \      --(左旋)-.           / \                #
     *  lx   y                          x  ry     
     *     /   \                       /  \
     *    ly   ry                     lx  ly  
     *
     *
     */
    private void leftRotate(RBTNode<T> x) {
        // 設置x的右孩子爲y
        RBTNode<T> y = x.right;


        // 將 “y的左孩子” 設爲 “x的右孩子”;
        // 如果y的左孩子非空,將 “x” 設爲 “y的左孩子的父親”
        x.right = y.left;
        if (y.left != null)
            y.left.parent = x;


        // 將 “x的父親” 設爲 “y的父親”
        y.parent = x.parent;


        if (x.parent == null) {
            this.mRoot = y;            // 如果 “x的父親” 是空節點,則將y設爲根節點
        } else {
            if (x.parent.left == x)
                x.parent.left = y;    // 如果 x是它父節點的左孩子,則將y設爲“x的父節點的左孩子”
            else
                x.parent.right = y;    // 如果 x是它父節點的左孩子,則將y設爲“x的父節點的左孩子”
        }
        
        // 將 “x” 設爲 “y的左孩子”
        y.left = x;
        // 將 “x的父節點” 設爲 “y”
        x.parent = y;
    }


 (3)右旋
    對y進行右旋意味着"將y變成一個右節點"。

    右旋的過程是將y的左子樹繞y順時針旋轉,使得y的左子樹成爲y的父親,同時修改相關結點的引用。


    代碼:
    /* 
 * 對紅黑樹的節點(y)進行右旋轉
 *
 * 右旋示意圖(對節點y進行左旋):
 *            py                               py
 *           /                                /
 *          y                                x                  
 *         /  \      --(右旋)-.            /  \                     #
 *        x   ry                           lx   y  
 *       / \                                   / \                   #
 *      lx  rx                                rx  ry
 * 
 */
private void rightRotate(RBTNode<T> y) {
    // 設置x是當前節點的左孩子。
    RBTNode<T> x = y.left;


    // 將 “x的右孩子” 設爲 “y的左孩子”;
    // 如果"x的右孩子"不爲空的話,將 “y” 設爲 “x的右孩子的父親”
    y.left = x.right;
    if (x.right != null)
        x.right.parent = y;


    // 將 “y的父親” 設爲 “x的父親”
    x.parent = y.parent;


    if (y.parent == null) {
        this.mRoot = x;            // 如果 “y的父親” 是空節點,則將x設爲根節點
    } else {
        if (y == y.parent.right)
            y.parent.right = x;    // 如果 y是它父節點的右孩子,則將x設爲“y的父節點的右孩子”
        else
            y.parent.left = x;    // (y是它父節點的左孩子) 將x設爲“y的父節點的左孩子”
    }


    // 將 “y” 設爲 “x的右孩子”
    x.right = y;


    // 將 “y的父節點” 設爲 “x”
    y.parent = x;
}
 (4)插入


/*********************** 向紅黑樹中插入節點 **********************/
public void insert(T key) {
    RBNode<T> node = new RBNode<T>(key, RED, null, null, null);
    if(node != null) 
        insert(node);
}


//將節點插入到紅黑樹中,這個過程與二叉搜索樹是一樣的
private void insert(RBNode<T> node) {
    RBNode<T> current = null; //表示最後node的父節點
    RBNode<T> x = this.root; //用來向下搜索用的
    
    //1. 找到插入的位置
    while(x != null) {
        current = x;
        int cmp = node.key.compareTo(x.key);
        if(cmp < 0) 
            x = x.left;
        else
            x = x.right;
    }
    node.parent = current; //找到了位置,將當前current作爲node的父節點
    
    //2. 接下來判斷node是插在左子節點還是右子節點
    if(current != null) {
        int cmp = node.key.compareTo(current.key);
        if(cmp < 0)
            current.left = node;
        else
            current.right = node;
    } else {
        this.root = node;
    }
    
    //3. 將它重新修整爲一顆紅黑樹
    insertFixUp(node);
}
    這與二叉搜索樹中實現的思路一模一樣,這裏不再贅述,主要看看方法裏面最後一步insertFixUp操作。
    因爲插入後可能會導致樹的不平衡,insertFixUp方法裏主要是分情況討論,分析何時變色,何時左旋,何時右旋。
    我們先從理論上分析具體的情況,然後再看insertFixUp方法的具體實現。
    如果是第一次插入,由於原樹爲空,所以只會違反紅-黑樹的規則2,所以只要把根結點塗黑即可;如果插入節點的父節點是黑色的,那不會違背紅-黑樹的規則,什麼也不需要做;但是遇到如下三種情況時,我們就要開始變色和旋轉了:
        1.插入結點的父節點和其叔叔結點(祖父結點的另一個子結點)均爲紅色的;
        2.插入結點的父結點是紅色,叔叔結點是黑色,且插入結點是其父結點的右子結點;
        3.插入結點的父結點是紅色,叔叔結點是黑色,且插入結點是其父結點的左子結點。
    對於情況1:插入結點的父結點和其叔叔結點(祖父結點的另一個子結點)均爲紅色的。此時,肯定存在祖父結點,但是不知道父結點是其左子結點還是右子結點,但是由於對稱性,我們只要討論出一邊的情況,另一邊情況自然也與之對應。這裏考慮父結點是祖父結點的左子結點的情況。
    如下圖1所示:

圖1


圖2


    對於這種情況,我們要做的操作有:將當前結點(4)的父結點(5)和叔叔結點(8)塗黑,將祖父結點(7)塗紅,變成如圖2所示的情況。再將當前結點指向其祖父結點,再次從新的當前結點開始算法(具體看下面程序)。這樣圖2就變成了情況2了。


    對於情況2:插入結點的父結點是紅色,叔叔結點是黑色,且插入結點是其父結點的右子結點。我們要做的操作有:將當前結點(7)的父結點(2)作爲新的結點,以新的當前結點爲支點做左旋操作。完成後如圖3所示,這樣圖3就變成情況3了。

圖3




    對於情況3:插入結點的父結點是紅色,叔叔結點是黑色,且插入結點是其父結點的左子結點。我們要做的操作有:將當前結點的父結點(7)塗黑,將祖父結點(11)塗紅在祖父結點爲支點做右旋操作。最後把根結點塗黑,整個紅-黑樹重新恢復了平衡,如圖4所示。

圖4

    至此,插入操作完成!
    我們可以看出,如果是從情況1開始發生的,必然會走完情況2和3,也就是說這是一整個流程,當然了,實際中不一定會從情況1發生,如果從情況2開始發生,那再走個情況3即可完成調整,如果直接只要調整情況3,那麼前兩種情況均不需要調整了。
    故變色和旋轉之間先後關係可以表示爲 變色->左旋->右旋。


    至此,我們完成了全部的插入操作。下面我們看看insertFixUp方法中的具體實現:


private void insertFixUp(RBNode<T> node) {
    RBNode<T> parent, gparent; //定義父節點和祖父節點
    
    //需要修整的條件:父節點存在,且父節點的顏色是紅色
    while(((parent = parentOf(node)) != null) && isRed(parent)) {
        gparent = parentOf(parent);//獲得祖父節點
        
        //若父節點是祖父節點的左子節點,下面else與其相反
        if(parent == gparent.left) {                
            RBNode<T> uncle = gparent.right; //獲得叔叔節點
            
            //case1: 叔叔節點也是紅色
            if(uncle != null && isRed(uncle)) {
                setBlack(parent); //把父節點和叔叔節點塗黑
                setBlack(uncle);
                setRed(gparent); //把祖父節點塗紅
                node = gparent; //將位置放到祖父節點處
                continue; //繼續while,重新判斷
            }
            
            //case2: 叔叔節點是黑色,且當前節點是右子節點
            if(node == parent.right) {
                leftRotate(parent); //從父節點處左旋
                RBNode<T> tmp = parent; //然後將父節點和自己調換一下,爲下面右旋做準備
                parent = node;
                node = tmp;
            }
            
            //case3: 叔叔節點是黑色,且當前節點是左子節點
            setBlack(parent);
            setRed(gparent);
            rightRotate(gparent);
        } else { //若父節點是祖父節點的右子節點,與上面的完全相反,本質一樣的
            RBNode<T> uncle = gparent.left;
            
            //case1: 叔叔節點也是紅色
            if(uncle != null & isRed(uncle)) {
                setBlack(parent);
                setBlack(uncle);
                setRed(gparent);
                node = gparent;
                continue;
            }
            
            //case2: 叔叔節點是黑色的,且當前節點是左子節點
            if(node == parent.left) {
                rightRotate(parent);
                RBNode<T> tmp = parent;
                parent = node;
                node = tmp;
            }
            
            //case3: 叔叔節點是黑色的,且當前節點是右子節點
            setBlack(parent);
            setRed(gparent);
            leftRotate(gparent);
        }
    }
    
    //將根節點設置爲黑色
    setBlack(this.root);
}
 (5)刪除
    上面探討完了紅-黑樹的插入操作,接下來討論刪除,紅黑樹的刪除和二叉查找樹的刪除是一樣的,只不過刪除後多了個平衡的修復而已。
    我們先來回憶一下二叉搜索樹的刪除:
        1.如果待刪除結點沒有子結點,那麼直接刪除掉即可;
        2.如果待刪除結點只有一個子結點,那麼直接刪掉,並用其子結點去頂替它;
        3.如果待刪除結點有兩個子結點,這種情況比較複雜:
        首先找出它的"後繼結點" 和 "被刪除結點的父結點" 之間的關係,最後處理 "後繼結點的子結點" 和 "被刪除結點的子結點" 之間的關係。每一步中也會有不同的情況。


        我們來看一下刪除操作的代碼及註釋:


/*********************** 刪除紅黑樹中的節點 **********************/
public void remove(T key) {
    RBNode<T> node;
    if((node = search(root, key)) != null)
        remove(node);
}


private void remove(RBNode<T> node) {
    RBNode<T> child, parent;
    boolean color;
    
    //1. 被刪除的節點“左右子節點都不爲空”的情況
    if((node.left != null) && (node.right != null)) {
        //先找到被刪除節點的後繼節點,用它來取代被刪除節點的位置
        RBNode<T> replace = node;
        //  1). 獲取後繼節點
        replace = replace.right;
        while(replace.left != null) 
            replace = replace.left;
        
        //  2). 處理“後繼節點”和“被刪除節點的父節點”之間的關係
        if(parentOf(node) != null) { //要刪除的節點不是根節點
            if(node == parentOf(node).left) 
                parentOf(node).left = replace;
            else
                parentOf(node).right = replace;
        } else { //否則
            this.root = replace;
        }
        
        //  3). 處理“後繼節點的子節點”和“被刪除節點的子節點”之間的關係
        child = replace.right; //後繼節點肯定不存在左子節點!
        parent = parentOf(replace);
        color = colorOf(replace);//保存後繼節點的顏色
        if(parent == node) { //後繼節點是被刪除節點的子節點
            parent = replace;
        } else { //否則
            if(child != null) 
                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 == BLACK) { //4. 如果移走的後繼節點顏色是黑色,重新修整紅黑樹
            removeFixUp(child, parent);//將後繼節點的child和parent傳進去
        }
        node = null;
        return;
    }
}
    下面我們主要看看方法裏面最後的removerFixUp操作。
    因爲remove後可能會導致樹的不平衡,removeFixUp方法裏主要是分情況討論,分析何時變色,何時左旋,何時右旋。我們同樣先從理論上分析具體的情況,然後在看removeFixUp方法的具體實現。
    從上面的代碼中可以看出,刪除某個結點後,會用它的後繼結點來填上,並且後繼結點會設置爲和刪除結點同樣的顏色,所以刪除結點的那個位置是不會破壞平衡的,可能破壞平衡的是後繼結點原來的位置,因爲後繼結點拿走了,原來的位置結構改變了,這就會導致不平衡的出現。所以removeFixUp方法中傳入的參數也是後繼結點的子結點和父結點。
    爲了方便下文的敘述,我們現在約定:後繼結點的子結點稱爲"當前結點"。
    刪除操作後,如果當前結點是黑色的根結點,那麼不用任何操作,因爲並沒有破壞樹的平衡性,既沒有違背紅-黑樹的規則,這很好理解。如果當前結點是紅色的,說明剛剛移走的後繼結點是黑色的,那麼不管後繼結點的父節點是啥顏色,我們只要將當前結點塗黑就可以了,紅-黑樹的平衡性就可以恢復。但是遇到以下四種情況,我們就需要通過變色或旋轉來恢復紅-黑樹的平衡了。
        1.當前結點是黑色的,且兄弟結點是紅色的(那麼父結點和兄弟結點的子結點肯定是黑色的);
        2.當前結點是黑色的,且兄弟結點是黑色的,且兄弟結點的兩個子結點均爲黑色的;
        3.當前結點是黑色的,且兄弟結點是黑色的,且兄弟結點左子結點是紅色,右子結點是黑色的;
        4.當前結點是黑色的,且兄弟結點是黑色的,且兄弟結點的右子結點是紅色,左子結點任意顏色。
     以上四種情況中,我們可以看出2,3,4其實是 "當前結點是黑色的,且兄弟結點是黑色的" 三種子集,等會在程序中可以體現出來。現在我們假設當前結點是左結點(當然也有可能是右結點,跟左結點相反即可,我們討論一邊就可以了),分別解決上面四種情況:
     對於情況1:當前結點是黑色的,且兄弟結點是紅色的(那麼父結點和兄弟結點的子結點肯定是黑色的)。
     如圖5所示:A結點表示當前結點。針對這種情況,我們要做的操作有:將父結點(B)塗紅,將兄弟結點(D)塗黑,然後將當前結點(A)的父結點(B)作爲支點左旋,然後當前結點的兄弟結點就變成黑色的情況(自然就轉換成情況2,3,4的公有特徵了),如圖6所示:

圖5


圖6


     對於情況2:當前結點是黑色的,且兄弟結點是黑色的,且兄弟結點的兩個子結點均爲黑色的。如圖7所示,A表示當前結點。針對這種情況,我們要做的操作有:將兄弟結點(D)塗紅,將當前結點指向其父結點(B),將其父結點指向當前結點的祖父結點,繼續新的算法(具體見下面的程序),不需要旋轉。這樣變成了圖8所示的情況:

圖7


圖8

     對於情況3:當前結點是黑色的,且兄弟結點是黑色的,且兄弟結點的左結點是紅色的,右結點是黑色的。如圖9所示,A是當前結點。針對這種情況,我們要做的操作有:把當前結點的兄弟結點(D)塗紅,把兄弟結點的左子節點(C)塗黑,然後以兄弟結點作爲支點做右旋操作。然後兄弟結點就會變成黑色的,且兄弟結點的右子結點變成紅色的情況(情況4)了。如圖10:

圖9


圖10


     對於情況4:當前結點是黑色的,且兄弟結點是黑色的,且兄弟結點的右子結點是紅色,左子結點任意顏色。如圖11所示:A爲當前結點,針對這種情況,我們要做的操作有:把兄弟結點(D)塗成父結點的顏色,再把父結點(B)塗黑,把兄弟結點的右子結點(E)塗黑,然後以當前結點的父結點爲支點做左旋操作,如圖12所示。至此,刪除修復算法就結束了,最後將根結點塗黑即可。

圖11圖12
     我們可以看出,如果是從情況1開始發生的,可能情況2,3,4中的一種:如果是情況2,就不可能在出現3和4;如果是情況3,必然會導致情況4的出現;如果2和3都不是,那必然是4.當然了,實際中可能不一定會從情況1發生,這要看具體情況了。
     至此,我們完成了全部的刪除操作。下面我們看看removeFixUp方法中的具體實現(可以結合上面的分析圖,更加利與理解):
     
//node表示待修正的節點,即後繼節點的子節點(因爲後繼節點被挪到刪除節點的位置去了)
private void removeFixUp(RBNode<T> node, RBNode<T> parent) {
    RBNode<T> other;
    
    while((node == null || isBlack(node)) && (node != this.root)) {
        if(parent.left == node) { //node是左子節點,下面else與這裏的剛好相反
            other = parent.right; //node的兄弟節點
            if(isRed(other)) { //case1: node的兄弟節點other是紅色的
                setBlack(other);
                setRed(parent);
                leftRotate(parent);
                other = parent.right;
            }
            
            //case2: node的兄弟節點other是黑色的,且other的兩個子節點也都是黑色的
            if((other.left == null || isBlack(other.left)) && 
                    (other.right == null || isBlack(other.right))) {
                setRed(other);
                node = parent;
                parent = parentOf(node);
            } else {
                //case3: node的兄弟節點other是黑色的,且other的左子節點是紅色,右子節點是黑色
                if(other.right == null || isBlack(other.right)) {
                    setBlack(other.left);
                    setRed(other);
                    rightRotate(other);
                    other = parent.right;
                }
                
                //case4: node的兄弟節點other是黑色的,且other的右子節點是紅色,左子節點任意顏色
                setColor(other, colorOf(parent));
                setBlack(parent);
                setBlack(other.right);
                leftRotate(parent);
                node = this.root;
                break;
            }
        } else { //與上面的對稱
            other = parent.left;
            
            if (isRed(other)) {
                // Case 1: node的兄弟other是紅色的  
                setBlack(other);
                setRed(parent);
                rightRotate(parent);
                other = parent.left;
            }


            if ((other.left==null || isBlack(other.left)) &&
                (other.right==null || isBlack(other.right))) {
                // Case 2: node的兄弟other是黑色,且other的倆個子節點都是黑色的  
                setRed(other);
                node = parent;
                parent = parentOf(node);
            } else {


                if (other.left==null || isBlack(other.left)) {
                    // Case 3: node的兄弟other是黑色的,並且other的左子節點是紅色,右子節點爲黑色。  
                    setBlack(other.right);
                    setRed(other);
                    leftRotate(other);
                    other = parent.left;
                }


                // Case 4: node的兄弟other是黑色的;並且other的左子節點是紅色的,右子節點任意顏色
                setColor(other, colorOf(parent));
                setBlack(parent);
                setBlack(other.left);
                rightRotate(parent);
                node = this.root;
                break;
            }
        }
    }
    if (node!=null)
        setBlack(node);
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章