紅黑樹詳解及其模板類實現

一、歷史

1972年,Rudolf Bayer發明了一種數據結構,這是一種特殊的4階B樹。這些樹維護從根到葉的所有路徑保持相同數量的節點,從而創建完美平衡的樹。但是,它們不是二叉搜索樹。Bayer在他的論文中將它們稱爲“對稱二叉B樹”(symmetric binary B-tree),後來它們作爲2-4樹(也稱2-3-4樹)變得流行。

在1978年的一篇論文“平衡樹的二色框架”中,Leonidas J. Guibas和Robert Sedgewick從對稱二叉B樹中得到了紅黑樹。選擇顏色“紅色”是因爲紅色是在施樂PARC工作時作者可用的彩色激光打印機產生的最佳顏色。 Guibas的另一個說法是,由於可以使用紅色和黑色的筆來繪製樹。

1993年,Arne Andersson引入了右傾樹 right leaning tree)的概念(AA樹),以簡化插入和刪除操作。

1999年,Chris Okasaki展示瞭如何使插入操作純粹起作用。它的平衡功能只需要處理4個不平衡情況和一個默認平衡情況。

最初的算法使用了8個不平衡的情況,但Cormen等人(2001)減少到6個不平衡的情況。Sedgewick展示了僅在46行Java代碼中實現插入操作。2008年,Sedgewick提出了左傾紅黑樹(left-leaning red–black tree),利用了Arne Andersson的簡化算法。Sedgewick最初允許樹節點的兩個孩子都是紅色的節點,使該樹更像2-3-4樹,但後來增加了限制,使新樹更像2-3樹。Sedgewick僅在33行中實現了插入算法,大大縮短了他原來的46行代碼。

二、概述

平衡二叉搜索樹的形式多樣,且各具特色。比如,伸展樹實現簡便、無需修改節點結構、分攤複雜度低,但可惜最壞情況下的單次操作需要\Omega(n)時間,故難以適用於核電站、醫院等對可靠性和穩定性要求極高的場合。反之,AVL樹儘管可以保證最壞情況下的單次操作速度,但需在節點中嵌入平衡因子等標識;更重要的是,刪除操作之後的重平衡可能需做多達\Omega(logn)次旋轉,從而頻繁地導致全樹整體拓撲結構的大幅度變化。

紅黑樹即是針對後一不足的改進。通過爲節點指定顏色,並巧妙地動態調整,紅黑樹可保證:在每次插入或刪除操作之後的重平衡過程中,全樹拓撲結構的更新僅涉及常數個節點。儘管最壞情況下需對多達\Omega(logn)個節點重染色,但就分攤意義而言僅爲O(1)個。當然,爲此首先需要在AVL樹“適度平衡”標準的基礎上,進一步放寬條件。實際上,紅黑樹所採用的“適度平衡”標準,可大致表述爲:任一節點左、右子樹的高度,相差不得超過兩倍

1.定義

爲便於對紅黑樹的理解、實現與分析,統一地引入n+1個外部節點,以保證原樹中每一節點的左、右孩子均非空----儘管有可能其中之一甚至二者同時是外部節點。當然,這些外部節點的引入只是假想式的,在具體實現時並不需要兌現爲真實的節點。【但是如果外部節點確實是顯式節點,它簡化了一些在紅黑樹上操作的算法。爲了節省執行時間,有時指向單個哨兵節點(sentinel node)(而不是空指針)的指針執行所有葉節點的角色; 然後從內部節點到外部節點的所有引用指向該哨兵節點。】如此擴展之後的便利之處在於,我們的考察範圍只需覆蓋真二叉樹。

由紅、黑兩色節點組成的二叉搜索樹若滿足以下條件,即爲紅黑樹(red-black tree):

  1. 樹根始終爲黑色
  2. 外部節點均爲黑色
  3. 其餘節點若爲紅色,則其孩子節點必爲黑色
  4. 從任一外部節點到根節點的沿途,黑節點的數目相等

其中,條件1和2意味着紅節點均爲內部節點,且其父節點及左、右孩子必然存在。條件3意味着紅節點之父必爲黑色,因此樹中任一通路都不包含相鄰的紅節點。除去根節點本身,沿途所經黑節點的總數稱作該節點的黑深度(black depth)----根節點的黑深度爲0。故條件4亦可等效地理解和描述爲“所有外部節點的黑深度統一”。由條件4可進一步推知,在從任一節點通往其任一後代外部節點的沿途,黑節點的總數亦必相等。除去(黑色)外部節點,沿途所經黑節點的總數稱作該節點的黑高度(black height)。所有外部節點的黑高度均爲0。

可總結爲:樹根和外部節點爲黑,紅孩兒黑,所有外部節點的黑深度統一。

2.類比4階B樹

紅黑樹的上述定義,不免令人困惑和費解。幸運的是,藉助B樹的概念,我們完全可以清晰地理解和把握紅黑樹的定義,及其運轉過程。爲此,需要注意到:在紅黑樹4階B樹之間,存在及其密切的聯繫;經適當轉換之後,二者相互等價!

具體地,自頂而下逐層考查紅黑樹各節點。每遇到一個紅節點,都將對應的子樹整體提升一層,從而與其父節點(必黑)水平對齊,二者之間的聯邊則相應地調整爲橫向。

如此轉換之後,橫向邊或向左或向右,但由紅黑樹的條件3,同向者彼此不會相鄰;即便不考慮聯邊的左右方向,沿水平方向相鄰的邊至多兩條(向左、右各一條),涉及的節點至多三個(一個黑節點加上零到兩個紅節點)。此時,若將原紅黑樹的節點視作關鍵碼,沿水平方向相鄰的每一組(父子至多三個)節點即恰好構成4階B樹的一個節點。圖2.1針對所有可能的四種情況,分別給出了具體的轉換過程。

圖2.1 紅黑樹到4階B樹的等價變換

以下將不再嚴格區分紅黑樹中的節點及其在(2-4)樹中對應的關鍵碼。當然,此時的關鍵碼也被賦予了對應的顏色。對照紅黑樹的條件,(2,4)樹中的每個節點應包含且僅包含一個黑關鍵碼,同時紅關鍵碼不得超過兩個。而且,若某個節點果真包含兩個紅關鍵碼,則黑關鍵碼的位置必然居中。

3.平衡性

與所有二叉搜索樹一樣,紅黑樹的性能首先取決於其平衡性。實際上,即便計入擴充的外部節點,包含n個內部節點的紅黑樹T的高度h也不致超過O(logn)。更嚴格地有:

log_{2}(n+1)\leq h\leq 2*log_{2}(n+1)

也就是說,儘管紅黑樹不能如完全樹那樣可做到理想平衡,也不如AVL樹那樣可做到較嚴格的適度平衡,但其高度仍控制在最小高度的兩倍以內,從漸進的角度看仍是O(logn),依然保證了適度平衡----這正是紅黑樹可高效率支持各種操作的基礎。

三、實現

1.預定義宏

#define BinNodePosi(T) BinNode<T>*	//節點位置
#define stature(p) ((p)?(p)->height:-1)	//節點高度(與“空樹高度爲-1”的約定相統一)
typedef enum{RB_RED,RB_BLACK} RBColor;	//節點顏色
/*************************************************************************************
*BinNode狀態與性質的判斷
**************************************************************************************/
#define IsRoot(x) (!((x).parent))
#define IsLChild(x) (!IsRoot(x) && (&(x)==(x).parent->lc))
#define IsRChild(x) (!IsRoot(x) && (&(x)==(x).parent->rc))
#define HasParent(x) (!IsRoot(x))
#define HasLChild(x) ((x).lc)
#define HasRChild(x) ((x).rc)
#define HasChild(x) (HasLChild(x) || HasRChild(x))	//至少擁有一個孩子
#define HasBothChild(x) (HasLChild(x) && HasRChild(x))	//同時擁有兩個孩子
#define IsLeaf(x) (!HasChild(x))
/************************************************************************************
*與BinNode具有特定關係的節點及指針
*************************************************************************************/
#define sibling(p) (IsLChild(*(p))?(p)->parent->rc:(p)->parent->lc)	//兄弟
#define uncle(x) sibling((x)->parent)	//叔叔
#define FromParentTo(x) /*來自父親的引用*/\
	(IsRoot(x)?_root:(IsLChild(x)?(x).parent->lc:(x).parent->rc))

#define IsBlack(p) (!(p) || (RB_BLACK == (p)->color))	//外部節點也視作黑節點
#define IsRed(p) (!IsBlack(p))	//非黑即紅
#define BlackHeightUpdated(x) (/*RedBlack高度更新條件*/ \
	(stature((x).lc)==stature((x).rc)) && \
	((x).height == (IsRed(&x)?stature((x).lc):stature((x).lc)+1)) \
)

2.紅黑樹節點

template<typename T>
struct BinNode {	//二叉樹節點模板類
	//成員
	T data;	//數值
	BinNodePosi(T) parent;	//父節點
	BinNodePosi(T) lc;	//左孩子
	BinNodePosi(T) rc;	//右孩子
	int height;	//黑高度
	RBColor color;	//顏色
	//構造函數
	BinNode() :parent(NULL),lc(NULL),rc(NULL),height(0),color(RB_RED){}
	BinNode(T e,BinNodePosi(T) p = NULL, BinNodePosi(T) lc = NULL, BinNodePosi(T) rc = NULL,
			int h = 0, RBColor c = RB_RED):
		data(e),parent(p),lc(lc),rc(rc),height(h),color(c){}
	//操作接口
	int size();	//統計當前節點後代總數,亦即以其爲根的子樹的規模
	BinNodePosi(T) succ();	//取當前節點的直接後繼

	//比較器、判等器
	bool operator<(const BinNode& bn) { return data < bn.data; }	//小於
	bool operator==(const BinNode& bn) { return data == bn.data; }	//等於
	bool operator!=(const BinNode& bn) { return data != bn.data; }	//不等於
};

3.紅黑樹模板類聲明

template<typename T>
class RedBlack {
protected:
	int _size;	//規模
	BinNodePosi(T) _root;	//根節點
	BinNodePosi(T) _hot;	//"命中"節點的父親
	void release(BinNodePosi(T) x);	//釋放樹中所有節點
	BinNodePosi(T) connect34(	//按照“3+4”結構,聯結3個節點及四棵子樹
		BinNodePosi(T) a, BinNodePosi(T) b, BinNodePosi(T) c,
		BinNodePosi(T) T0, BinNodePosi(T) T1, BinNodePosi(T) T2, BinNodePosi(T) T3);
	BinNodePosi(T) rotateAt(BinNodePosi(T) x);	//對x及其父親、祖父做統一旋轉調整
	void solveDoubleRed(BinNodePosi(T) x);	//雙紅修正
	void solveDoubleBlack(BinNodePosi(T) r);	//雙黑修正
	int updateHeight(BinNodePosi(T) x);	//更新節點x的高度
public:
	RedBlack():_size(0),_root(NULL){}	//構造函數
	~RedBlack() { release(_root); }	//析構函數
	int size() const { return _size; }	//規模
	bool empty() const { return !_root; }	//判空
	BinNodePosi(T)& search(const T& e);	//查找
	BinNodePosi(T) insert(const T& e);	//插入
	bool remove(const T& e);	//刪除
};

template<typename T>
void RedBlack<T>::release(BinNodePosi(T) x)
{
	if (!x) return;	//遞歸基:空樹
	release(x->lc);
	release(x->rc);
	delete x;
}

4.查找算法

template<typename T>	//在以v爲根的子樹中查找關鍵碼e
static BinNodePosi(T)& searchIn(BinNodePosi(T)& v, const T& e, BinNodePosi(T)& hot)
{
	if (!v || (e == v->data))	//遞歸基:在節點v(或假想的通配節點)處命中
		return v;
	hot = v;	//一般情況:先記下當前節點,然後再
	return searchIn(((e < v->data) ? v->lc : v->rc), e, hot);	//深入一層,遞歸查找
}	//返回時,返回值指向命中節點(或假想的通配哨兵),hot指向其父親(退化時爲初始值NULL)
template<typename T>	//在紅黑樹中查找關鍵碼e
BinNodePosi(T)& RedBlack<T>::search(const T& e)
{
	return searchIn(_root, e, _hot = NULL);	//返回目標節點位置的引用,以便後續插入、刪除操作
}

5.節點插入算法

1.節點插入與雙紅現象

實現代碼: 

template<typename T>
BinNodePosi(T) RedBlack<T>::insert(const T& e)	//將e插入紅黑樹
{
	BinNodePosi(T)& x = search(e);	if (x) return x;	//確認目標不存在
	x = new BinNode<T>(e, _hot, NULL, NULL, -1);	_size++;	//創建紅節點x:以_hot爲父,黑高度-1
	solveDoubleRed(x);	//經雙紅修正後,即可返回
	return x ? x : _hot->parent;	//無論e是否存在於原樹中,返回時總有x->data==e
}

 不妨假定經調用接口search(e)做查找之後,確認目標節點尚不存在。於是,在查找終止的位置x處創建節點,並隨即將其染成紅色(除非此時全樹僅含一個節點)。現在,對照紅黑樹的四個條件,唯有3未必滿足----亦即,此時x的父親也可能是紅色。

因新節點的引入,而導致父子節點同爲紅色的此類情況,稱作“雙紅”(double red)。當前節點x的兄弟及兩個孩子(初始時都是外部節點),始終均爲黑色。將x的父親與祖父分別記作p和g。既然此前的紅黑樹合法,故作爲紅節點p的父親,g必然存在且爲黑色。g作爲內部節點,其另一孩子(即p的兄弟、x的叔父)也必然存在,將其記作u。視節點u的顏色不同,雙紅修正分兩類情況分別處置。

2.雙紅修正

>>>>>>RR-1

首先,考查u爲黑色的情況。此時,x的兄弟、兩個孩子的黑高度,如圖3.1即爲此類情況的所有可能的情況(四種)。

圖3.1 雙紅修正第一種情況(RR-1)及其調整方法(上方、下方分別爲紅黑樹及其對應B-樹的局部)

從B樹角度等效地看,即同一節點不應包含緊鄰的紅色關鍵碼。故只需令黑色關鍵碼與緊鄰的紅色關鍵碼互換顏色。從紅黑樹的角度看,這等效於按中序遍歷次序,對節點x、p和g及其四棵子樹,做一次局部“3+4”重構。如此調整之後,局部子樹的黑高度將復原,這意味着全樹的平衡也必然得以恢復。同時,新子樹的根節點b爲黑色,也不致引發新的雙紅現象。至此,整個插入操作遂告完成。  

>>>>>>RR-2

 再考查節點u爲紅色的情況。此時,u的左、右孩子非空且均爲黑色,其黑高度必與x的兄弟以及兩個孩子相等。圖3.2給出了兩種可能的此類情況(另兩種情況對稱)。

圖3.2 雙紅修正第二種情況(RR-2)及其調整方法(帶問號的關鍵碼可能存在,且顏色不定)

此時紅黑樹條件3的違反,從B樹角度等效地看,即該節點因超過4度而發生上溢。從紅黑樹的角度來看,只需將紅節點p和u轉爲黑色,黑節點g轉爲紅色,x保持紅色。從B樹的角度來看,等效於上溢節點的一次分裂。

如此調整之後局部子樹的黑高度復原。然而,子樹根節點g轉爲紅色之後,有可能在更高層再次引發雙紅現象。從B樹的角度來看,對應於在關鍵碼g被移出並歸入上層節點之後,進而導致上層節點的上溢----即上溢的向上傳播。若果真如此,可以等效地將g視作新插入節點,同樣地分以上兩類情況如法處置。若最後一步迭代之後導致原樹根的分裂,並由g獨立地構成新的樹根節點,則應遵照紅黑樹條件1的要求,強行將其轉爲黑色----如此全樹黑高度隨即增加一層。 

>>>>>>實現代碼:

/**************************************************************************************************
**RedBlack雙紅調整算法:解決節點x與其父均爲紅色的問題。分爲兩大類情況:
**	RR-1 : 2次顏色翻轉,2次黑高度更新,1~2次旋轉,不再遞歸
**	RR-2 : 3次顏色翻轉,3次黑高度更新,0次旋轉,需要遞歸
***************************************************************************************************/
template<typename T>
void RedBlack<T>::solveDoubleRed(BinNodePosi(T) x)	//x當前必爲紅
{
	if (IsRoot(*x)) {	//若已(遞歸)轉至樹根,則將其轉黑,整樹黑高度也隨之遞增
		_root->color = RB_BLACK;
		_root->height++;
		return;
	}	//否則,x的父親p必存在
	BinNodePosi(T) p = x->parent;	if (IsBlack(p)) return;	//若p爲黑,則可終止調整。否則
	BinNodePosi(T) g = p->parent;	//既然p爲紅,則x的祖父必存在,且必爲黑色
	BinNodePosi(T) u = uncle(x);	//以下,視x叔父u的顏色分別處理
	if (IsBlack(u)) {	//u爲黑色(含NULL)時
		if (IsLChild(*x) == IsLChild(*p))	//若x與p同側,則
			p->color = RB_BLACK;	//p由紅轉黑,x保持紅
		else    //若x與p異側,則
			x->color = RB_BLACK;	//x由紅轉黑,p保持紅
		g->color = RB_RED;	//g必定由黑轉紅
							//////以上雖保證總共兩次染色,但因增加了判斷而得不償失
							//////在旋轉後將根置黑、孩子置紅,雖需三次染色但效率更高
		BinNodePosi(T) gg = g->parent;	//曾祖父(great-grand parent)
		BinNodePosi(T) r = FromParentTo(*g) = rotateAt(x);	//調整後的子樹根節點
		r->parent = gg;	//與原曾祖父連接
	}
	else {	//若u爲紅色
		p->color = RB_BLACK;	p->height++;	//p由紅轉黑
		u->color = RB_BLACK;	u->height++;	//u由紅轉黑
		if (!IsRoot(*g))
			g->color = RB_RED;	//g若非根,則轉紅
		solveDoubleRed(g);	//繼續調整g
	}
}

3.小結

由上面分析可知,只有在RR-1修復時才需做1~2次旋轉;而且一旦旋轉後,修復過程必然隨即完成。故就全樹拓撲結構而言,每次插入後僅涉及常數次調整;而且稍後將會看到,紅黑樹的節點刪除操作亦是如此----AVL樹卻只能保證前一點。

6.節點刪除算法

1.節點刪除與雙黑現象

實現代碼:

/*************************************************************************************************
**刪除位置x所指的節點
**目標x在此前經查找定位,並確認非NULL,故必刪除成功
**返回值指向實際被刪除節點的接替者,hot指向實際被刪除節點的父親----二者均有可能是NULL
**************************************************************************************************/
template<typename T>
static BinNodePosi(T) removeAt(BinNodePosi(T)& x, BinNodePosi(T)& hot)
{
	BinNodePosi(T) w = x;	//實際被摘除的節點,初值同x
	BinNodePosi(T) succ = NULL;	//實際被刪除節點的接替者
	if (!HasLChild(*x))	//若*x的左子樹爲空,則可
		succ = x = x->rc;	//直接將*x替換爲其右子樹
	else if (!HasRChild(*x))	//若右子樹爲空,則可
		succ = x = x->lc;	//對稱地處理----注意:此時succ != NULL
	else {	//若左右子樹均存在,則選擇x的直接後繼作爲實際被摘除節點,爲此需要
		w = w->succ();	//(在右子樹中)找到*x的直接後繼*w
		std::swap(x->data, w->data);	//交換*x和*w數據元素
		BinNodePosi(T) u = w->parent;
		((u == x) ? u->rc : u->lc) = succ = w->rc;	//隔離節點*w
	}
	hot = w->parent;	//記錄實際被刪除節點的父親
	if (succ) succ->parent = hot;	//並將被刪除節點的接替者與hot相連
	delete w;	//釋放被摘除節點
	return succ;	//返回接替者
}
template<typename T>
bool RedBlack<T>::remove(const T& e)	//從紅黑樹中刪除關鍵碼e
{
	BinNodePosi(T)& x = search(e);	if (!x) return false;
	BinNodePosi(T) r = removeAt(x, _hot); if (!(--_size)) return true;	//實施刪除
//assert:_hot某一孩子剛被刪除,且被r所指節點(可能是NULL)接替。以下檢查是否失衡,並做必要調整
	if (!_hot) {	//若剛被刪除的是根節點,則將其置黑,並更新黑高度
		_root->color = RB_BLACK;
		updateHeight(_root);
		return true;
	}
	//assert:以下,原x(現r)必非根,_hot必非空
	if (BlackHeightUpdated(*_hot))	return true;	//若所有祖先的黑深度依然平衡,則無需調整
	if (IsRed(r)) {	//否則,若r爲紅,則只需令其轉黑
		r->color = RB_BLACK;
		r->height++;
		return true;
	}
	//assert:以下,原x(現r)均爲黑色
	solveDoubleBlack(r);
	return true;	//經雙黑調整後返回
}	//若目標節點存在且被刪除,返回true;否則返回false

 爲刪除關鍵碼e,首先調用search(e)進行查找。若查找成功,則調用接口removeAt(x)實施刪除。x爲實際被摘除者,其父親爲p=_hot,其接替者爲r,而r的兄弟爲外部接節點w=NULL。【因隨後的復衡調整位置可能逐層上升,故不妨等效地理解爲:w是與r黑高度相等的子紅黑樹,且隨其父親x一併被摘除。如此,可將x統一視作雙分支節點,從而更爲通用地描述以下算法。】 此時紅黑樹的前兩個條件繼續滿足,但後兩個條件卻未必。當然,若x原爲樹根,則無論r顏色如何,只需將其置爲黑色並更新黑高度即可。因此不妨假定,x的父親p存在。

圖3.3 刪除節點x之後,紅黑樹條件4:(a)或依然滿足,(b)或經重染色後重新滿足,(c)或不再滿足

若如圖3.3(a)所示x爲紅色,則在摘除子樹w,並將x替換爲r之後,如圖(a')所示局部子樹的黑高度即可復原。 即便x爲黑色,只要如圖(b)所示r爲紅色,則如圖(b')所示,只需在刪除操作之後將r翻轉爲黑色,亦可使局部子樹的黑高度復原。然而如圖(c)所示,若x與r均爲黑色,則在刪除操作之後,如圖(c')所示局部子樹的黑高度將會降低一個單位。

被刪除節點x及其替代者r同爲黑色的此類情況,稱作“雙黑”(double black)。爲此,需考查原黑節點x的兄弟s(必然存在,但可能是外部節點),並視s和p顏色的不同組合,按四種情況分別處置。

2.雙黑修正

>>>>>>BB-1

首先,考慮節點s爲黑,且節點s至少有一個紅孩子。

圖3.4 雙黑修正(情況BB-1)
(若p爲紅,問號之一爲黑關鍵碼;若p爲黑,則自成一個節點)

既然節點x的另一孩子w=NULL,故從B樹角度(圖3.4(a'))看節點x被刪除之後的情況,可以等效地理解爲:關鍵碼x原所屬的節點發生下溢。故可參照B樹的調整方法,下溢節點從父節點借出一個關鍵碼,然後父節點從下溢節點的兄弟節點借出一個關鍵碼,調整後的效果如圖3.4(b')。從紅黑樹的角度(圖3.4(b))來看,上述調整過程等效於對節點t,s和p實施"3+4"重構。此外,若這三個節點按中序遍歷次序重命名爲a、b和c,則還需將a和c染成黑色,b則繼承p此前的顏色。注意,整個過程中節點r保持黑色不變。 由圖3.4(b)(及其對稱情況)不難驗證,經以上處理之後,紅黑樹的所有條件,都在這一局部以及全局得到恢復,故刪除操作遂告完成。

 

 

 

節點s及其兩個孩子均爲黑色時,視節點p顏色的不同,又可進一步分爲兩種情況。

>>>>>>BB-2-R

首先考慮p爲紅色的情況。

圖3.5 雙黑修正(情況BB-2-R)
(帶問號的黑關鍵碼可能存在,但不會同時存在) 

與BB-1類似,在對應的B樹中,關鍵碼x的刪除導致其所屬的節點下溢。但因此時關鍵碼s所在節點只有兩個分支,故下溢節點無法從父節點借出關鍵碼。按照B樹平衡算法,此時應如圖3.5(b’)所示,將關鍵碼p取出並下降一層,然後以之爲“粘合劑”,將原左、右孩子合併爲一個節點。從紅黑樹角度看,這一過程可如圖3.5(b)所示等效地理解爲:s和p顏色互換。

由圖3.5(b)(及其對稱情況)可知,經過以上處理,紅黑樹的所有條件都在此局部得以恢復。另外,由於關鍵碼p原爲紅色,故如圖3.5(a')所示,在關鍵碼p所屬節點中,其左或右必然還有一個黑色關鍵碼(不可能左右兼有)----這意味着,在關鍵碼p從其中取出之後,不致引發新的下溢。至此,紅黑樹條件亦必在全局得以恢復,刪除操作即告完成。 

 

 

 

 

 

>>>>>>BB-2-B

再考慮p爲黑色的情況。

圖3.6 雙黑修正(情況BB-2-B)

 此時與BB-2-R類似,在對應的B樹中,因關鍵碼x的刪除,導致其所屬節點發生下溢。故可如圖3.6(b')所示,將下溢節點與其兄弟合併。從紅黑樹的角度來看,這一過程可如圖3.6(b)所示等效地理解爲:節點s由黑轉紅。由圖3.6(b)(及其對稱情況)可知,經過以上處理之後,紅黑樹的所有條件都將在此局部得到恢復。

然而,既然s和x在此之前均爲黑色,故如圖3.6(a')所示,p原所屬的B樹節點必然僅含p這一個關鍵碼。於是p被借出之後,該節點必將繼而發生下溢,從而有待於後續的進一步修正。從紅黑樹的角度來看,此時的狀態則可等效理解爲:節點p的(黑色)父親剛被刪除。當然,可以視具體情況繼續調整。實際上,這也是雙黑修正過程中,需要再次迭代的唯一可能。

 

 

 

 

>>>>>>BB-3 

最後,考慮節點s爲紅色的情況。

圖3.7 雙黑修正(情況BB-3)

此時,作爲紅節點s的父親,節點p必爲黑色;同時,s的兩個孩子也應均爲黑色。於是從B樹的角度來看,只需如圖(b')所示,令關鍵碼s與p互換顏色,即可得到一棵與之完全等價的B樹。而從紅黑樹的角度來看,這一轉換對應於以節點p爲軸做一次旋轉,並交換節點s與p的顏色。經過如此處理之後,儘管雙黑缺陷依然存在(子樹r的黑高度並未復原),而且缺陷位置的高度也並未上升,但在轉換之後的紅黑樹中,被刪除節點x(及其替代者節點r)有了一個新的兄弟s‘----與此前的兄弟s不同,s'必然是黑的!這就意味着,接下來可以套用此前所介紹其它情況的處置方法,繼續並最終完成雙黑修正。

還需注意:現在的節點p,也已經黑色轉爲紅色。因此,接下來即便需要繼續調整,必然既不可能轉換回此前的情況BB-3,也不可能轉入可能需要反覆迭代的情況BB-2-B。實際上此後只可能轉入更早討論過的兩類情況    ----BB-1或BB-2-R。這就意味着接下來至多再做一步迭代調整,整個雙黑修復即可完成。

 

>>>>>>實現代碼:

/****************************************************************************************
*RedBlack雙黑調整算法:解決節點x與被其替代的節點均爲黑色的問題
*分爲三大類共四種情況:
*	BB-1	:2次顏色翻轉,2次黑高度更新,1-2次旋轉,不再遞歸
*	BB-2R	:2次顏色翻轉,2次黑高度更新,0次旋轉,不在遞歸
*	BB-2B	:1次顏色翻轉,1次黑高度更新,0次旋轉,需要遞歸
*	BB-3	:2次顏色翻轉,2次黑高度更新,1次旋轉,轉爲BB-1或BB-2R
****************************************************************************************/
template<typename T>
void RedBlack<T>::solveDoubleBlack(BinNodePosi(T) r)
{
	BinNodePosi(T) p = r ? r->parent : _hot;	if (!p) return;	//r的父親
	BinNodePosi(T) s = (r == p->lc) ? p->rc : p->lc;	//r的兄弟
	if (IsBlack(s)) {	//兄弟s爲黑
		BinNodePosi(T) t = NULL;	//s的紅孩子(若左、右孩子皆紅,左者優先;皆黑時爲NULL)
		if (IsRed(s->rc))	t = s->rc;	//右子
		if (IsRed(s->lc))	t = s->lc;	//左子
		if (t) {	//黑s有紅孩子:BB-1
			RBColor oldColor = p->color;	//備份原子數根節點p顏色,並對t及其父親、祖父
											//以下,通過旋轉重平衡,並將新子樹的左、右孩子染黑
			BinNodePosi(T) b = FromParentTo(*p) = rotateAt(t);	//旋轉
			if (HasLChild(*b)) {
				b->lc->color = RB_BLACK;
				updateHeight(b->lc);
			}	//左子
			if (HasRChild(*b)) {
				b->rc->color = RB_BLACK;
				updateHeight(b->rc);
			}	//右子
			b->color = oldColor;
			updateHeight(b);	//新子樹根節點繼承原根節點的顏色
		}
		else {	//黑s無紅孩子
			s->color = RB_RED;	//s轉紅
			s->height--;
			if (IsRed(p)) {	//BB-2R
				p->color = RB_BLACK;	//p轉黑,但黑高度不變
			}
			else {
				p->height--;	//p保持黑,但黑高度下降
				solveDoubleBlack(p);	//遞歸上溯
			}
		}
	}
	else {	//兄弟s爲紅:BB-3
		s->color = RB_BLACK;	//s轉黑
		p->color = RB_RED;	//p轉紅
		BinNodePosi(T) t = IsLChild(*s) ? s->lc : s->rc;	//取t與其父s同側
		_hot = p;
		FromParentTo(*p) = rotateAt(t);	//對t及其父親、祖父做平衡調整
		solveDoubleBlack(r);	//繼續修正r處雙黑----此時的p已轉紅,故後續只能是BB-1或BB-2R
	}
}

3.小結

縱覽各種情況,不難確認:一旦在某步迭代中做過節點的旋轉調整,整個修復過程便會隨即完成。因此與雙紅修正一樣,雙黑修正的整個過程,也僅涉及常數次的拓撲結構調整操作。

四、總結

紅黑樹與AVL樹之間最本質的一項差異在於:在每次刪除操作後,拓撲聯接關係有所變化的節點絕不會超過常數個。這使得紅黑樹成爲函數式編程中最常見的持久性數據結構之一。

五、拓展

1.應用

紅黑樹爲插入時間,刪除時間和搜索時間提供最壞情況保證。這不僅使它們在時間敏感的應用程序(如實時應用程序)中具有價值,而且使它們成爲提供最壞情況保證的其他數據結構中的寶貴構建塊; 例如,計算幾何中使用的許多數據結構可以基於紅黑樹,而當前Linux內核epoll系統調用實現中使用的完全公平調度程序使用紅黑樹。

紅黑樹在函數式編程中也特別有價值,它是最常見的持久性數據結構之一,用於構建關聯數組以及可在突變後保留先前版本的集合

tango樹的原始描述是一種爲快速搜索而優化的樹,它特別使用紅黑樹作爲其數據結構的一部分。

在Java8中,已經修改了Collection HashMap,不再使用LinkedList來存儲具有衝突的哈希碼的不同元素,而是使用紅黑樹。這使搜索元素的時間複雜度的提高從O(n)到O(logn)。

2.相關數據結構

AA樹:

AA樹是紅黑樹的變種,設計的目的是減少紅黑樹考慮的不同情況。區別於紅黑樹的是,AA樹的紅節點只能作爲右孩子。另外AA樹爲實現方便,不再使用紅黑兩種顏色,而是用level標記結點,結點中的level相當於紅黑樹中結點的黑高度。AA樹可以在O(log n)的時間內做查找,插入和刪除,這裏的n是樹中元素的數目。

 

箴言錄

子貢問曰:“有一言而可以終身行之者乎?”子曰:“其恕乎!己所不欲,勿施於人。”

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