2. 紅黑樹

定義:紅黑樹(Red-Black Tree,簡稱R-B Tree),它一種特殊的二叉查找樹(Binary Search Tree)。java

要理解紅黑樹,先要了解什麼是二叉查找樹。在上一章中,咱們學習了什麼是二叉樹,以及二叉樹的遍歷,以上一章的知識做爲基礎,如今學校什麼是二叉查找樹。二叉查找樹是一棵知足如下特徵的二叉樹:node

  1. 左子樹上的全部節點的值均小於或等於它的根節點的值
  2. 右子樹上的全部節點的值均大於或等於它的根節點的值
  3. 左、右子樹也分別爲二叉排序樹

    如下二叉樹就是一棵標準的二叉樹:算法

一棵典型的二叉查找樹數據結構

二叉查找樹的查詢效率如何呢?仍是以上面這棵樹爲例,如今咱們查找值爲8的節點,函數

1. 根節點9 >8,因此比較左孩子節點,學習

2.因爲5<8,因此查看右孩子節點,this

3.因爲7<8,全部查看右孩子節點spa

4.此時發現8=8 ,查找成功。code

以上查找方法,正是二分查找思想。查找所需的最大次數,正好是樹的高度。二叉查找樹這種數據結構,在查找上是有它的優點的,但是它也並非完美的,哪裏不完美呢?如今,若是對一棵二叉查找樹,插入數據(或節點),二叉查找樹該怎麼作呢?對象

假設初始的二叉查找樹只有三個節點,根節點值爲9,左孩子值爲8,右孩子值爲12:

接下來咱們依次插入以下五個節點:7,6,5,4,3。依照二叉查找樹的特性,結果會變成什麼樣呢?

經過觀察發現,原本效率很高二分查找,再添加節點後,查找時間複雜度幾乎變成了線性。那麼如何解決二查找樹屢次插入新節點而致使的不平衡呢?這時候,紅黑樹就應運而生了。

紅黑樹(Red-Black Tree)是一種自平衡的二叉查找樹,它除了符合二叉查找樹的全部特性外,還要符合如下附加特性:

  1. 節點是紅色或黑色
  2. 根節點是黑色
  3. 每一個葉子節點都是黑色的空姐點(NIL節點)
  4. 每一個紅色節點的兩個子節點都是黑色。( 從每一個葉子到根的全部路徑上不能有兩個連續的紅色節點
  5. 從任一節點到其每一個葉子節點的全部路徑都包含相同數目的黑色節點。

下圖中這棵樹,就是一顆典型的紅黑樹:

紅黑樹從根節點到葉子節點,最長路徑不會超過最短路徑的2倍。當有節點插入或刪除的時候,當前紅黑樹的規則就會被打破了,此時須要把節點作出調整,以維持紅黑樹的規則。

什麼狀況下會破壞紅黑樹的規則,什麼狀況下不會破壞規則呢?咱們舉兩個簡單的栗子:

1.向原紅黑樹插入值爲14的新節點:

因爲父節點15是黑色節點,所以這種狀況並不會破壞紅黑樹的規則,無需作任何調整。

2. 向原紅黑樹中增長值爲21的新節點:

此時,能夠觀察到這棵樹再也不符合紅黑樹的規則,須要作出調整,怎麼調整呢?有兩種方法,「變色」和「旋轉」,而旋轉又分紅兩種「左旋轉」和「右旋轉」。

變色爲了從新符合紅黑樹的規則,嘗試把紅色節點變爲黑色,或者把黑色節點變爲紅色。

     下圖所表示的是紅黑樹的一部分,須要注意節點25並不是根節點。由於節點21和節點22連續出現了紅色,不符合規則4,因此把節點22從紅色變成黑色:

但這樣並不算完,由於憑空多出的黑色節點打破了規則5,因此發生連鎖反應,須要繼續把節點25從黑色變成紅色:

此時仍然沒有結束,由於節點25和節點27又造成了兩個連續的紅色節點,須要繼續把節點27從紅色變成黑色:

左旋轉: 逆時針旋轉紅黑樹的兩個節點,使得父節點被本身的右孩子取代,而本身成爲本身的左孩子。提及來很怪異,你們看下圖:

圖中,身爲右孩子的Y取代了X的位置,而X變成了本身的左孩子。此爲左旋轉。

右旋轉:

順時針旋轉紅黑樹的兩個節點,使得父節點被本身的左孩子取代,而本身成爲本身的右孩子。你們看下圖:

圖中,身爲左孩子的Y取代了X的位置,而X變成了本身的右孩子。此爲右旋轉。

咱們以剛纔插入節點21的狀況爲例:

首先,咱們須要作的是變色,把節點25及其下方的節點變色:

此時節點17和節點25是連續的兩個紅色節點,那麼把節點17變成黑色節點?恐怕不合適。這樣一來不但打破了規則4,並且根據規則2(根節點是黑色),也不可能把節點13變成紅色節點。

 

變色已沒法解決問題,咱們把節點13看作X,把節點17看作Y,像剛纔的示意圖那樣進行左旋轉

因爲根節點必須是黑色節點,因此須要變色,變色結果以下:

這樣就結束了嗎?並無。由於其中兩條路徑(17 -> 8 -> 6 -> NIL)的黑色節點個數是4,其餘路徑的黑色節點個數是3,不符合規則5。

這時候咱們須要把節點13看作X,節點8看作Y,像剛纔的示意圖那樣進行右旋轉

 

最後根據規則來進行變色

如此一來,咱們的紅黑樹變得從新符合規則。這一個例子的調整過程比較複雜,經歷了以下步驟:

變色 -> 左旋轉 -> 變色 -> 右旋轉 -> 變色

能夠看出,紅黑樹在有新節點加入時,操做仍是很複雜的。那有哪些應用場景呢?

JDK的集合類TreeMap和TreeSet第層就是紅黑樹實現的。在JDK1.8中,HashMap也用到了紅黑樹。

紅黑樹的JAVA代碼實現

1.基本定義

/**
 * 紅黑樹
 *
 * @param <T>
 */
public class RBTree<T extends Comparable<T>> {
	/**
	 * 根節點
	 */
	private              RBTNode<T> root;
	private static final boolean    RED   = false;
	private static final boolean    BLACK = true;

	/**
	 * 節點對象
	 *
	 * @param <T>
	 */
	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;
		}

	}
}

2.左旋

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) {
			/**
			 * 若是 「x的父親」 是空節點,則將y設爲根節點
			 */
			this.root = y;
		} else {
			if (x.parent.left == x){
				// 若是 x是它父節點的左孩子,則將y設爲「x的父節點的左孩子」
				x.parent.left = y;
			}
			else{
				// 若是 x是它父節點的左孩子,則將y設爲「x的父節點的左孩子」
				x.parent.right = y;
			}
		}
		// 將 「x」 設爲 「y的左孩子」
		y.left = x;
		// 將 「x的父節點」 設爲 「y」
		x.parent = y;
	}

3.右旋

/* 
 * 對紅黑樹的節點(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設爲「x的父節點的左孩子」
    }

    // 將 「y」 設爲 「x的右孩子」
    x.right = y;

    // 將 「y的父節點」 設爲 「x」
    y.parent = x;
}

3. 添加節點

將一個節點插入到紅黑樹中,須要執行哪些步驟呢?首先,將紅黑樹看成一顆二叉查找樹,將節點插入;而後,將節點着色爲紅色;最後,經過"旋轉和從新着色"等一系列操做來修正該樹,使之從新成爲一顆紅黑樹。詳細描述以下:
第一步: 將紅黑樹看成一顆二叉查找樹,將節點插入。
       紅黑樹自己就是一顆二叉查找樹,將節點插入後,該樹仍然是一顆二叉查找樹。也就意味着,樹的鍵值仍然是有序的。此外,不管是左旋仍是右旋,若旋轉以前這棵樹是二叉查找樹,旋轉以後它必定仍是二叉查找樹。這也就意味着,任何的旋轉和從新着色操做,都不會改變它仍然是一顆二叉查找樹的事實。
好吧?那接下來,咱們就來千方百計的旋轉以及從新着色,使這顆樹從新成爲紅黑樹!

第二步:將插入的節點着色爲"紅色"。
       爲何着色成紅色,而不是黑色呢?爲何呢?在回答以前,咱們須要從新溫習一下紅黑樹的特性:
(1) 每一個節點或者是黑色,或者是紅色。
(2) 根節點是黑色。
(3) 每一個葉子節點是黑色。 [注意:這裏葉子節點,是指爲空的葉子節點!]
(4) 若是一個節點是紅色的,則它的子節點必須是黑色的。
(5) 從一個節點到該節點的子孫節點的全部路徑上包含相同數目的黑節點。
      將插入的節點着色爲紅色,不會違背"特性(5)"!少違背一條特性,就意味着咱們須要處理的狀況越少。接下來,就要努力的讓這棵樹知足其它性質便可;知足了的話,它就又是一顆紅黑樹了。o(∩∩)o...哈哈

第三步: 經過一系列的旋轉或着色等操做,使之從新成爲一顆紅黑樹。
       第二步中,將插入節點着色爲"紅色"以後,不會違背"特性(5)"。那它到底會違背哪些特性呢?
       對於"特性(1)",顯然不會違背了。由於咱們已經將它塗成紅色了。
       對於"特性(2)",顯然也不會違背。在第一步中,咱們是將紅黑樹看成二叉查找樹,而後執行的插入操做。而根據二叉查找數的特色,插入操做不會改變根節點。因此,根節點仍然是黑色。
       對於"特性(3)",顯然不會違背了。這裏的葉子節點是指的空葉子節點,插入非空節點並不會對它們形成影響。
       對於"特性(4)",是有可能違背的!
       那接下來,想辦法使之"知足特性(4)",就能夠將樹從新構形成紅黑樹了。

/* 
 * 將結點插入到紅黑樹中
 *
 * 參數說明:
 *     node 插入的結點        // 對應《算法導論》中的node
 */
private void insert(RBTNode<T> node) {
    int cmp;
    RBTNode<T> y = null;
    RBTNode<T> x = this.mRoot;

    // 1. 將紅黑樹看成一顆二叉查找樹,將節點添加到二叉查找樹中。
    while (x != null) {
        y = x;
        cmp = node.key.compareTo(x.key);
        if (cmp < 0)
            x = x.left;
        else
            x = x.right;
    }

    node.parent = y;
    if (y!=null) {
        cmp = node.key.compareTo(y.key);
        if (cmp < 0)
            y.left = node;
        else
            y.right = node;
    } else {
        this.mRoot = node;
    }

    // 2. 設置節點的顏色爲紅色
    node.color = RED;

    // 3. 將它從新修正爲一顆二叉查找樹
    insertFixUp(node);
}

/* 
 * 新建結點(key),並將其插入到紅黑樹中
 *
 * 參數說明:
 *     key 插入結點的鍵值
 */
public void insert(T key) {
    RBTNode<T> node=new RBTNode<T>(key,BLACK,null,null,null);

    // 若是新建結點失敗,則返回。
    if (node != null)
        insert(node);
}

內部接口 -- insert(node)的做用是將"node"節點插入到紅黑樹中。
外部接口 -- insert(key)的做用是將"key"添加到紅黑樹中。


添加修正操做的實現代碼(Java語言)

/*
 * 紅黑樹插入修正函數
 *
 * 在向紅黑樹中插入節點以後(失去平衡),再調用該函數;
 * 目的是將它從新塑形成一顆紅黑樹。
 *
 * 參數說明:
 *     node 插入的結點        // 對應《算法導論》中的z
 */
private void insertFixUp(RBTNode<T> node) {
    RBTNode<T> parent, gparent;

    // 若「父節點存在,而且父節點的顏色是紅色」
    while (((parent = parentOf(node))!=null) && isRed(parent)) {
        gparent = parentOf(parent);

        //若「父節點」是「祖父節點的左孩子」
        if (parent == gparent.left) {
            // Case 1條件:叔叔節點是紅色
            RBTNode<T> uncle = gparent.right;
            if ((uncle!=null) && isRed(uncle)) {
                setBlack(uncle);
                setBlack(parent);
                setRed(gparent);
                node = gparent;
                continue;
            }

            // Case 2條件:叔叔是黑色,且當前節點是右孩子
            if (parent.right == node) {
                RBTNode<T> tmp;
                leftRotate(parent);
                tmp = parent;
                parent = node;
                node = tmp;
            }

            // Case 3條件:叔叔是黑色,且當前節點是左孩子。
            setBlack(parent);
            setRed(gparent);
            rightRotate(gparent);
        } else {    //若「z的父節點」是「z的祖父節點的右孩子」
            // Case 1條件:叔叔節點是紅色
            RBTNode<T> uncle = gparent.left;
            if ((uncle!=null) && isRed(uncle)) {
                setBlack(uncle);
                setBlack(parent);
                setRed(gparent);
                node = gparent;
                continue;
            }

            // Case 2條件:叔叔是黑色,且當前節點是左孩子
            if (parent.left == node) {
                RBTNode<T> tmp;
                rightRotate(parent);
                tmp = parent;
                parent = node;
                node = tmp;
            }

            // Case 3條件:叔叔是黑色,且當前節點是右孩子。
            setBlack(parent);
            setRed(gparent);
            leftRotate(gparent);
        }
    }

    // 將根節點設爲黑色
    setBlack(this.mRoot);
}

insertFixUp(node)的做用是對應"上面所講的第三步"。它是一個內部接口。

4.刪除操做

將紅黑樹內的某一個節點刪除。須要執行的操做依次是:首先,將紅黑樹看成一顆二叉查找樹,將該節點從二叉查找樹中刪除;而後,經過"旋轉和從新着色"等一系列來修正該樹,使之從新成爲一棵紅黑樹。詳細描述以下:
第一步:將紅黑樹看成一顆二叉查找樹,將節點刪除。
       這和"刪除常規二叉查找樹中刪除節點的方法是同樣的"。分3種狀況:
① 被刪除節點沒有兒子,即爲葉節點。那麼,直接將該節點刪除就OK了。
② 被刪除節點只有一個兒子。那麼,直接刪除該節點,並用該節點的惟一子節點頂替它的位置。
③ 被刪除節點有兩個兒子。那麼,先找出它的後繼節點;而後把「它的後繼節點的內容」複製給「該節點的內容」;以後,刪除「它的後繼節點」。在這裏,後繼節點至關於替身,在將後繼節點的內容複製給"被刪除節點"以後,再將後繼節點刪除。這樣就巧妙的將問題轉換爲"刪除後繼節點"的狀況了,下面就考慮後繼節點。 在"被刪除節點"有兩個非空子節點的狀況下,它的後繼節點不多是雙子非空。既然"的後繼節點"不可能雙子都非空,就意味着"該節點的後繼節點"要麼沒有兒子,要麼只有一個兒子。若沒有兒子,則按"狀況① "進行處理;若只有一個兒子,則按"狀況② "進行處理。

第二步:經過"旋轉和從新着色"等一系列來修正該樹,使之從新成爲一棵紅黑樹。
        由於"第一步"中刪除節點以後,可能會違背紅黑樹的特性。因此須要經過"旋轉和從新着色"來修正該樹,使之從新成爲一棵紅黑樹。

/* 
 * 刪除結點(node),並返回被刪除的結點
 *
 * 參數說明:
 *     node 刪除的結點
 */
private void remove(RBTNode<T> node) {
    RBTNode<T> child, parent;
    boolean color;

    // 被刪除節點的"左右孩子都不爲空"的狀況。
    if ( (node.left!=null) && (node.right!=null) ) {
        // 被刪節點的後繼節點。(稱爲"取代節點")
        // 用它來取代"被刪節點"的位置,而後再將"被刪節點"去掉。
        RBTNode<T> replace = node;

        // 獲取後繼節點
        replace = replace.right;
        while (replace.left != null)
            replace = replace.left;

        // "node節點"不是根節點(只有根節點不存在父節點)
        if (parentOf(node)!=null) {
            if (parentOf(node).left == node)
                parentOf(node).left = replace;
            else
                parentOf(node).right = replace;
        } else {
            // "node節點"是根節點,更新根節點。
            this.mRoot = replace;
        }

        // child是"取代節點"的右孩子,也是須要"調整的節點"。
        // "取代節點"確定不存在左孩子!由於它是一個後繼節點。
        child = replace.right;
        parent = parentOf(replace);
        // 保存"取代節點"的顏色
        color = colorOf(replace);

        // "被刪除節點"是"它的後繼節點的父節點"
        if (parent == node) {
            parent = replace;
        } else {
            // child不爲空
            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)
            removeFixUp(child, parent);

        node = null;
        return ;
    }

    if (node.left !=null) {
        child = node.left;
    } else {
        child = node.right;
    }

    parent = node.parent;
    // 保存"取代節點"的顏色
    color = node.color;

    if (child!=null)
        child.parent = parent;

    // "node節點"不是根節點
    if (parent!=null) {
        if (parent.left == node)
            parent.left = child;
        else
            parent.right = child;
    } else {
        this.mRoot = child;
    }

    if (color == BLACK)
        removeFixUp(child, parent);
    node = null;
}

/* 
 * 刪除結點(z),並返回被刪除的結點
 *
 * 參數說明:
 *     tree 紅黑樹的根結點
 *     z 刪除的結點
 */
public void remove(T key) {
    RBTNode<T> node; 

    if ((node = search(mRoot, key)) != null)
        remove(node);
}

內部接口 -- remove(node)的做用是將"node"節點插入到紅黑樹中。
外部接口 -- remove(key)刪除紅黑樹中鍵值爲key的節點。


刪除修正操做的實現代碼(Java語言)

/*
 * 紅黑樹刪除修正函數
 *
 * 在從紅黑樹中刪除插入節點以後(紅黑樹失去平衡),再調用該函數;
 * 目的是將它從新塑形成一顆紅黑樹。
 *
 * 參數說明:
 *     node 待修正的節點
 */
private void removeFixUp(RBTNode<T> node, RBTNode<T> parent) {
    RBTNode<T> other;

    while ((node==null || isBlack(node)) && (node != this.mRoot)) {
        if (parent.left == node) {
            other = parent.right;
            if (isRed(other)) {
                // Case 1: x的兄弟w是紅色的  
                setBlack(other);
                setRed(parent);
                leftRotate(parent);
                other = parent.right;
            }

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

                if (other.right==null || isBlack(other.right)) {
                    // Case 3: x的兄弟w是黑色的,而且w的左孩子是紅色,右孩子爲黑色。  
                    setBlack(other.left);
                    setRed(other);
                    rightRotate(other);
                    other = parent.right;
                }
                // Case 4: x的兄弟w是黑色的;而且w的右孩子是紅色的,左孩子任意顏色。
                setColor(other, colorOf(parent));
                setBlack(parent);
                setBlack(other.right);
                leftRotate(parent);
                node = this.mRoot;
                break;
            }
        } else {

            other = parent.left;
            if (isRed(other)) {
                // Case 1: x的兄弟w是紅色的  
                setBlack(other);
                setRed(parent);
                rightRotate(parent);
                other = parent.left;
            }

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

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

                // Case 4: x的兄弟w是黑色的;而且w的右孩子是紅色的,左孩子任意顏色。
                setColor(other, colorOf(parent));
                setBlack(parent);
                setBlack(other.left);
                rightRotate(parent);
                node = this.mRoot;
                break;
            }
        }
    }

    if (node!=null)
        setBlack(node);
}

removeFixup(node, parent)是對應"上面所講的第三步"。它是一個內部接口。

相關文章
相關標籤/搜索