大戰紅黑樹


1.概念

紅黑樹(R-B Tree), 全稱Red-Block Tree. 它是一種特殊的二叉樹, 樹中的每個節點都有顏色, 可以是紅可以是黑.

注意: 非紅色節點就是黑色節點, 即NULL節點是黑色節點

學習時可以先按照既定的規則進行調整, 學會后, 再去思考為什么有這些情況, 然后再考慮為什么這種情況需要這么處理.

2.性質

  • 性質1: 節點是紅色或黑色.
  • 性質2: 根節點是黑色.
  • 性質3: 每個NULL節點是黑色.
  • 性質4: 每個紅色節點的兩個孩子節點一定是黑色.
  • 性質5: 從任意節點到其NULL節點的所有路徑中都包含相同數目的黑色節點.

3.預備知識-旋轉

當紅黑樹的結構發生改變時(添加/刪除元素), 紅黑樹的性質可能會被破壞, 需要通過調整使樹重新成為紅黑樹, 調整可以分為兩類:

  1. 顏色調整: 改變節點的顏色
  2. 結構調整: 左旋 + 右旋

3-1.左旋

左旋要確定對誰(旋轉節點)進行左旋.

簡單說: 左旋就是把旋轉節點變為其右孩子的左節點(右孩子變為旋轉節點的父節點).

3-1-1.左旋步驟

  1. 將旋轉節點的右節點的左節點指向旋轉節點的右節點上(雙向關聯).
  2. 將旋轉節點的右節點的父節點指向旋轉節點的父節點(雙向關聯).
  3. 將旋轉節點的父節點指向旋轉節點的右節點(雙向關聯).

3-1-2.左旋示例圖

假設旋轉節點為: 節點20, 對旋轉節點進行左旋. 如下圖

3-1-3.參考TreeMap的左旋代碼

/** From CLR */
private void rotateLeft(Entry<K,V> p) {

	if (p != null) {

		// 獲取p的右節點r, 臨時存儲
		Entry<K,V> r = p.right;

		// --將旋轉節點的右節點的左節點指向旋轉節點的右節點上(雙向關聯).
		// 將p的右節點的左節點連接到p的右節點上
		p.right = r.left;
		// 將p的右節點的左節點的父節點指向為p
		if (r.left != null)
			r.left.parent = p;

		// 將旋轉節點的右節點的父節點指向旋轉節點的父節點(雙向關聯).
		// 將p的父節點賦值給r, r的父節點指向為p的父節點
		r.parent = p.parent;

		if (p.parent == null) // 父節點為空, 根節點即為 r
			root = r;
		else if (p.parent.left == p) // p是父節點的左節點
			p.parent.left = r;
		else  // p是父節點的右節點
			p.parent.right = r;

		// 將旋轉節點的父節點指向旋轉節點的右節點(雙向關聯).
		r.left = p;
		p.parent = r;
	}
}

3-2.右旋

右旋要確定對誰(旋轉節點)進行右旋.

簡單說: 右旋就是把旋轉節點變為其左孩子的右節點(左孩子變為旋轉節點的父節點).

3-2-1.右旋步驟

  1. 將旋轉節點的左節點的右節點指向旋轉節點的左節點上(雙向關聯).
  2. 將旋轉節點的左節點的父節點指向旋轉節點的父節點(雙向關聯).
  3. 將旋轉節點的父節點指向旋轉節點的左節點(雙向關聯).

3-2-2.右旋示例圖

假設旋轉節點為: 節點30, 對旋轉節點進行右旋. 如下圖

3-2-3.參考TreeMap的右旋代碼

/** From CLR */
private void rotateRight(Entry<K,V> p) {

	if (p != null) {

		// 臨時存儲p的左節點
		Entry<K,V> 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;
	}
}

4.預備知識-尋找節點的后繼

當節點元素被刪除時, 如果待刪除節點有兩個孩子, 則不能刪除該節點, 應該尋找到待刪除節點的前驅或后繼節點, 然后使用前驅或后繼節點中值覆蓋待刪除節點的值, 最后把前驅或后繼節點刪除.

實際上節點的后繼節點就是紅黑樹按照中序遍歷結果, 節點元素的后一個元素, 前驅節點同理.

理解了二叉樹的中序遍歷, 這里邊很容易理解了.

參考TreeMap的尋找后繼代碼:

/**
 * Returns the successor of the specified Entry, or null if no such.
 */
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {

	if (t == null) // null is null
		return null;
	else if (t.right != null) { // 右節點非空

		// 循環尋找右節點的左節點的左節點..., 直到左節點的左節點為null, 返回.
		Entry<K,V> p = t.right;
		while (p.left != null)
			p = p.left;
		return p;
	} else { // 右節點為null

		// t是父節點的右節點: 一直獲取父節點, 直到獲取到根節點, 返回
		// t是父節點的左節點: 后繼節點就是父節點, 返回
		Entry<K,V> p = t.parent;
		Entry<K,V> ch = t;
		while (p != null && ch == p.right) {
			ch = p;
			p = p.parent;
		}

		return p;
	}
}

當然TreeMap中還有尋找節點的前驅的方法: Entry<K,V> predecessor(Entry<K,V> t).

5.插入調整

紅黑樹的插入操作如同二叉排序樹的插入操作一樣, 不同的時, 在新元素插入之后, 需要對數進行調整使其重新成為一顆紅黑樹, 這里就研究如何進行調整.

新插入的元素一定是葉節點, 那么父節點為黑色就不需要進行處理, 因為新插入的元素默認染為紅色, 如果父節點是紅色, 就違反了性質4, 此時需要進行調整.

5-1.插入新元素時會出現的情況

  • 情況1: 紅黑樹是空樹
  • 情況2: 父節點為黑色
  • 情況3: 父節點為紅色 & 叔叔節點為紅色
  • 情況4: 父節點為紅色 & 叔叔節點為黑色

5-2.情況1: 紅黑樹是空樹

處理步驟:

  1. 將新節點染為紅色
  2. 將新節點染為黑色

調整完成.

示例圖

5-3.情況2: 父節點為黑色

父節點是黑色, 添加一個紅色孩子節點並不會影響紅黑樹的性質, 不需要調整.

示例圖

在只有根節點(20)的紅黑樹中插入一個新節點(10), 如下圖

大家可以嘗試一下在復雜的樹中插入, 也不會影響紅黑樹的性質的.

5-4.情況3: 父節點為紅色 & 叔叔節點為紅色

處理步驟:

  1. 將父節點和叔叔節點染為黑色
  2. 將祖父節點染為紅色

按照上述步驟調整之后, 祖父節點的顏色由黑色變為紅色, 這時需要對祖父節點進行調整.

示例圖

在現有的紅黑樹中插入新節點(35), 則調整過程如下

圖中祖父節點即為根節點, 直接染為黑色即可, 如果祖父節點非根節點, 此時需要將當前節點指向祖父節點, 對祖父節點進行進一步的調整.

5-5.情況4: 父節點為紅色 & 叔叔節點為黑色

處理步驟(父節點是祖父節點的左節點):

  1. 將新節點調整為父節點的左孩子節點(如果是父節點的右孩子的話)
    1. 將父節點作為新節點
    2. 對新節點進行左旋
  2. 將父節點染為黑色
  3. 將祖父節點染為紅色
  4. 對祖父節點進行右旋

處理步驟(父節點是祖父節點的右節點):

  1. 將新節點調整為父節點的右孩子節點(如果是父節點的左孩子的話)
    1. 將父節點作為新節點
    2. 對新節點進行右旋
  2. 將父節點染為黑色
  3. 將祖父節點染為紅色
  4. 對祖父節點進行左旋

發現, 節點的調整只在以祖父節點為根的樹中進行調整, 調整前后祖父節點的顏色不變, 只要把以祖父節點為根的樹調整為紅黑樹即可. 但是要保證調整前與調整后, 從祖父節點從葉節點的路徑要包含相同數目的黑節點.

所以, 經過該步驟調整之后, 樹一定為紅黑樹.

示例圖

在現有的紅黑樹中插入新節點(45), 則調整過程如下

調整完成.

5-6.插入調整總結

插入調整總體看起來比較簡單, 閉眼冥想一下各種情況, 然后接下來看代碼.

5-7.參考TreeMap的插入調整代碼

/** From CLR */
private void fixAfterInsertion(Entry<K,V> x) {

	// 默認新節點的顏色為紅色, 默認紅色處理起來比較簡單, WHY?
	x.color = RED;

	// 父節點為紅色時, 增加一個新節點, 會違反性質4
	while (x != null && x != root && x.parent.color == RED) {

		if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { // 父節點為祖父節點的左節點

			// 獲取叔叔節點
			Entry<K,V> y = rightOf(parentOf(parentOf(x)));

			if (colorOf(y) == RED) { // 叔叔節點為紅色時

				// 父節點和兄弟節點染為黑色
				setColor(parentOf(x), BLACK);
				setColor(y, BLACK);

				// 祖父節點染為紅色
				setColor(parentOf(parentOf(x)), RED);

				// 當前節點指向為祖父節點
				// 如果此時x=root了, 那么方法的最后一行代碼便很有必要了.
				x = parentOf(parentOf(x));
			} else { // 叔叔節點為黑色時

				// 將新節點調整為父節點的左孩子節點
				if (x == rightOf(parentOf(x))) {
					x = parentOf(x);
					rotateLeft(x);
				}

				// 父節點染為黑色
				setColor(parentOf(x), BLACK);

				// 祖父節點染為紅色
				setColor(parentOf(parentOf(x)), RED);

				// 對祖父節點進行右旋
				rotateRight(parentOf(parentOf(x)));

				// 此時, x的父節點已經被染為黑色了, 退出while循環
			}
		} else { // 與上面對應
			Entry<K,V> 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;
}

6.刪除調整

刪除, 對於紅黑樹來說是最復雜的, 也比較難理解, 分情況進行分析, 就簡單了.

Let's go!

6-1.刪除節點時會出現的情況

  • 情況1: 節點既有左子樹又有右子樹
  • 情況2: 節點只有左子樹或只有右子樹
  • 情況3: 節點既沒有左子樹又沒有右子樹(節點是葉節點)

對於情況1, 我們首先要找到該節點的前驅或后繼節點, 使用前驅或后繼節點的值覆蓋待刪除節點的值, 然后將前驅或后繼節點按照情況2或情況3進行刪除即可, 此時前驅或者后繼節點頂多有一個子節點. 本文中使用后繼.

所以, 對於紅黑樹來說, 實際刪除節點的情況只有兩種(情況2和情況3), 但是這兩種情況又分為多種情況, 下面一一列舉.

下文中, 待刪除節點用節點D(Delete)表示.

6-2.情況2出現的情況

  • 情況2-1: 節點D是紅色 & 其右節點(R)是黑色 -- 不存在
  • 情況2-2: 節點D是紅色 & 其右節點(R)是紅色 -- 不存在
  • 情況2-3: 節點D是紅色 & 其左節點(L)是黑色 -- 不存在
  • 情況2-4: 節點D是紅色 & 其左節點(L)是紅色 -- 不存在
  • 情況2-5: 節點D是黑色 & 其右節點(R)是黑色 -- 不存在
  • 情況2-6: 節點D是黑色 & 其右節點(R)是紅色
  • 情況2-7: 節點D是黑色 & 其左節點(L)是黑色 -- 不存在
  • 情況2-8: 節點D是黑色 & 其左節點(L)是紅色

分析情況2, 只會存在情況2-6和情況2-8的刪除, 其它情況並不符合紅黑樹的特性, 所以根本不會存在其它情況的刪除, 再看情況2-6和情況2-8, 由於節點D頂多有一個孩子, 所以兩種情況的處理方式是一樣的.

6-2-1.情況2-6: 節點D是黑色 & 其右節點(R)是紅色

處理步驟:

  1. 將其右節點鏈接到其父節點上.
  2. 將其右節點染為黑色即可.

等同於刪除了一個紅色節點, 並不影響紅黑樹的性質.

示例圖

在現有的紅黑樹中刪除節點30, 過程如下

此時, 只需把節點刪除, 然后把其后繼節點染為黑色即可.

6-2-2.情況2-8: 節點D是黑色 & 其左節點(L)是紅色

處理步驟:

  1. 將其左節點鏈接到其父節點上.
  2. 將其左節點染為黑色即可.

等同於刪除了一個紅色節點, 並不影響紅黑樹的性質.

示例圖

如同情況2-6的示例圖, 只不過孩子節點在左邊而已.

6-3.情況3出現的情況

  • 情況3-1: 節點D是紅色
  • 情況3-2: 節點D是黑色 & 兄弟節點是紅色
  • 情況3-3: 節點D是黑色 & 兄弟節點是黑色 & 兄弟節點有孩子節點
  • 情況3-3: 節點D是黑色 & 兄弟節點是黑色 & 兄弟節點無孩子節點

6-3-1.情況3-1: 節點D是紅色

此時父節點一定為黑色, 如果有兄弟節點, 兄弟節點一定為紅色.

示例圖

在現有的紅黑樹中刪除節點35, 過程如下

刪除一個紅色節點並不會影響紅黑樹的性質, 無須調整.

6-3-2.情況3-2: 節點D是黑色 & 兄弟節點是紅色

此時, 兄弟節點一定有兩個黑色子節點, 因為節點D是葉節點.

處理步驟(節點D是父節點的左節點):

  1. 父節點染為紅色
  2. 兄弟節點染為黑色
  3. 對父節點進行左旋
  4. 重新計算兄弟節點

處理步驟(節點D是父節點的右節點):

  1. 父節點染為紅色
  2. 兄弟節點染為黑色
  3. 對父節點進行右旋
  4. 重新計算兄弟節點

示例圖

在現有的紅黑樹中刪除節點10, 過程如下

虛線表示節點被刪除了, 經過該步驟之后, 樹還不是紅黑樹, 需要進一步調整.

6-3-3.情況3-3: 節點D是黑色 & 兄弟節點是黑色 & 兄弟節點的子節點至少一個為紅色

兄弟節點的子節點包括孩子節點和NULL節點, 因為它們都是黑色.

處理步驟(節點D是父節點的左節點):

  1. 將兄弟節點的紅色子節點調整為兄弟節點的右節點(如果兄弟節點的右節點是黑色)
    1. 將兄弟節點的左節點染為黑色
    2. 將兄弟節點染為紅色
    3. 對兄弟節點進行右旋
    4. 重新計算兄弟節點
  2. 將兄弟節點的顏色染為父節點的顏色
  3. 將父節點染為黑色
  4. 將兄弟節點的右節點染為黑色
  5. 對父節點進行左旋

處理步驟(節點D是父節點的右節點):

  1. 將兄弟節點的紅色子節點調整為兄弟節點的左節點(如果兄弟節點的左節點是黑色)
    1. 將兄弟節點的右節點染為黑色
    2. 將兄弟節點染為紅色
    3. 對兄弟節點進行左旋
    4. 重新計算兄弟節點
  2. 將兄弟節點的顏色染為父節點的顏色
  3. 將父節點染為黑色
  4. 將兄弟節點的右節點染為黑色
  5. 對父節點進行右旋

如果兄弟節點有兩個紅色子節點, 直接從第2步開始調整, 如果兄弟節點有一個紅色子節點, 需要先將紅色子節點調整為與兄弟節點方向一致的位置.

發現, 節點的調整只在以父節點為根的樹中進行調整, 調整前后父節點的顏色不變, 只要把以父節點為根的樹調整為紅黑樹即可. 但是要保證調整前與調整后, 從父節點從葉節點的路徑要包含相同數目的黑節點.

所以, 經過該步驟調整之后, 樹一定為紅黑樹.

示例圖

在現有的紅黑樹中刪除節點25, 過程如下

調整完之后便是紅黑樹.

6-3-4.情況3-4: 節點D是黑色 & 兄弟節點是黑色 & 兄弟節點的子節點都為黑色

兄弟節點的子節點包括孩子節點和NULL節點, 因為它們都是黑色.

處理步驟:

  1. 將兄弟節點染為紅色
  2. 將待調整的節點指向父節點

這種情況刪除節點D之后, 如果父節點是紅色, 直接把父節點染為黑色, 兄弟節點染為紅色即可. 如果父節點是黑色, 刪除后經過父節點的路徑少了一個黑節點, 需要對父節點進行調整.

示例圖

在現有的紅黑樹中刪除節點10, 過程如下

圖中的情況是父節點是紅色的情況, 如果父節點是黑色呢? 看下圖

注: D表示待刪除節點; X表示當前節點.

圖中也只是演示了一種情況, 可以會出現其它情況, 但是任何情況也會坐落於刪除的這幾種情況之中.

6-4.刪除調整總結

刪除時, 先看待刪除節點的顏色, 再看其兄弟節點的顏色, 最后看兄弟節點是否有子節點, 根據具體的情況進行調整.

6-5.參考TreeMap的刪除調整代碼

/** From CLR */
private void fixAfterDeletion(Entry<K,V> x) {

	// 刪除的節點為黑色時, 需要進行調整
	while (x != root && colorOf(x) == BLACK) {

		// 當前節點是左節點
		if (x == leftOf(parentOf(x))) {

			// 獲取右節點(兄弟節點)
			Entry<K,V> 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) {  // 兄弟節點的兩個子節點都是黑色, 其實是NULL節點

				// 兄弟節點染為紅色
				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 { // symmetric
			Entry<K,V> 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;
			}
		}
	}

	// 將x染為黑色
	setColor(x, BLACK);
}

7.總結

紅黑樹是一個比較重要的算法, 我覺得作為一個程序員應該需要了解它.

紅黑樹的核心在於元素變動之后, 如何進行調整使其重新成為一顆紅黑樹.

通過學習紅黑樹, 深刻體會到大問題並不可怕, 一點點拆分為小問題, 一定會解決的.

如有發現錯誤, 煩請指出.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM