數據結構之紅黑樹詳解
紅黑樹原理
什麼是紅黑樹
紅黑樹是一種特殊的二叉查找樹,用來實現對大批量有序數據的管理操作。
二叉查找樹,即是特殊的二叉樹,樹中每個節點的左子樹的所有元素都不大於該節點的元素,右子樹的所有元素都不小於該節點。
相比普通的二叉查找樹,紅黑樹中的每個節點增加了色域(color field)這一數據項,色域取值可以是紅色或者黑色。通過巧妙的設計,紅黑樹可以保證在任何情況下,對數據的有序插入,查找和刪除操作,都能在O(logn)的時間複雜度內完成。
爲什麼要用紅黑樹
紅黑樹是一種性能優異的數據結構,經常用作其他數據結構如map等的底層實現。二叉查找樹,包括隨機二叉查找樹,依然會在某些極端情況下,表現出O(n)的時間複雜度,並不能保證總是高效的處理數據。而紅黑樹,作爲一種平衡查找樹,可以保證整棵樹的高度不超過2log(n+1),從而保證了時間複雜度限制在O(logn),保證了算法在最壞情況下表現依然能滿足我們所期望的效率要求。
紅黑樹的性質(Red-Black properties)
紅黑樹的優秀性能,依靠如下性質來保證,滿足如下性質的二叉查找樹,即是紅黑樹。
- 每個節點都是紅色或者黑色。
- 根節點和葉子節點都是黑色。
- 紅色節點的子節點或者父節點都必須是黑色。
- 每個節點到其任一後代葉子節點的所有簡單路徑上的黑色節點數目相同。
對上述性質進行如下說明:
- 紅黑樹在二叉樹的基礎上,將每個葉子節點的空指針替換爲nil節點作爲新的葉子節點,這些nil節點稱爲外部節點(我們只關心外部節點的顏色,對他們的其他性質沒有要求,因此可以用一個哨兵節點統一代替所有的外部節點)而原有節點稱爲內部節點。
- 性質3要求紅色節點的父節點或者子節點爲黑色,即任何一條從根到葉子的簡單路徑上不能出現兩個連續的節點是紅色。
- 根節點和葉子節點要求爲黑色,而不能爲紅色,是爲了避免給內部節點添加不必要的限制。
- 任一節點到其後代葉子節點的簡單路徑上的黑色節點數稱爲黑高(black height),葉子節點的黑高爲0,黑高不計入該節點本身的顏色。
通過上述的性質規約,可以證明紅黑樹的操作時間複雜度爲O(logn),n爲內部節點的數量(數據的總量)。
首先通過歸納法證明,對任意一節點x,以其爲根的子樹中至少含有2bh(x)-1個內部節點。bh(x)爲黑高。
- 對於葉子節點,bh(x) = 0,2bh(x)-1=0, 條件成立。
- 對任意內部節點x,其子節點的黑高爲bh(x)(當子節點是紅色節點)或者bh(x)-1(當子節點是黑色節點),則x節點的子節點至少含有2bh(x)-1-1個內部節點,故x爲根的子樹含有1+2bh(x)-1-1+2bh(x)-1-1=2bh(x)-1個內部節點。
- 設h爲樹的高度,根據紅黑樹的性質2,任何一條根到葉子的簡單路徑上的黑色節點至少有一半,即根的黑高至少爲h/2,故有n>=2h/2-1,即h<=2log(n+1)。
而二叉查找樹的增刪查改時間複雜度均爲O(h),從而我們可以保證紅黑樹在任意情況下都有O(logn)的時間複雜度。
C語言實現
紅黑樹優良的效率性能。是犧牲了算法上的複雜度。在進行增刪操作時,需要對紅黑樹加以修正來滿足紅黑樹的四條性質。主要通過二叉樹的旋轉和節點的變色來實現。
同時要注意,在插入新節點的時候,我們默認新節點的顏色爲紅色,這是因爲,插入黑色節點,必然會改變某些簡單路徑上的黑色節點數目,很有可能使得所有的簡單路徑黑色節點數目不等,帶來額外的操作調整。
在本實現中,採用了哨兵節點來取代所有的NULL指針,簡化邊界條件。
數據結構定義部分
//頭文件部分
#define RED 0 // 紅色節點
#define BLACK 1 // 黑色節點
typedef int Type; //支持後續數據類型擴展
// 紅黑樹的節點
typedef struct RBTreeNode{
unsigned int color; // 顏色(RED 或 BLACK)
Type key; // 關鍵字(鍵值)
struct RBTreeNode *left; // 左孩子
struct RBTreeNode *right; // 右孩子
struct RBTreeNode *parent; // 父結點
}Node;
typedef struct {
Node *root; //根節點
Node nil; //哨兵元素
}RBTree;
創建紅黑樹
RBTree * init_tree()
{
RBTree *tree = (RBTree*)malloc(sizeof(RBTree));
tree->nil.color = BLACK;
tree->root = &tree->nil;
tree->root->parent = &(tree->nil);
return tree;
}
初始化一個空節點
Node* init_node(Type key)
{
Node *bt =(Node*)calloc(1,sizeof(Node));
bt->color = RED;
bt->right = NULL;
bt->left = NULL;
bt->key = key;
bt->parent = NULL;
return bt;
}
節點插入
- 插入過程:
- 節點插從樹根開始查找,遇到葉子節點nil後插入‘
- 如果該葉子節點的父節點爲nil,說明樹爲空,將插入的節點設爲根節點;
- 通過變色和旋轉操作修正紅黑樹使其滿足紅黑性質。
插入函數
void insert_node(RBTree *tree, Node *node)
{
Node *p,*n;
p = tree->root->parent; //指示父節點
n = tree->root; //尋找插入位置
while(*n != tree->nil)
{
p = n;
if(node->key < n->key)
{
n = n->left;
}
else
{
n = n->right;
}
}
//循環終止時,n爲nil節點,p爲n的父節點,用node取代n的位置
node->parent = p;
if(&p == tree->nil)
{
tree->root = node;
}
else
{
if(node->key < p->key)
{
p->left = node;
}
else
{
p->right = node;
}
}
node->color = RED;
node->left = &tree->nil;
node->right = &tree->nil;
rb_fix(tree, node)
}
紅黑修正函數
增加新的紅色節點時,可能導致如下情況破壞紅黑性質:
- 根節點變爲紅色
- 插入節點的父節點也是紅色
情況1發送在紅黑樹原本爲空的情況下,只需要對根節點進行變色處理即可。
情況2需要考慮到新增節點的叔父節點的情況,因此大體分爲三種情形:
- 叔父節點和父節點均爲紅色,則將叔父節點和父節點均變爲黑色,同時爲了保證性質三,需要調整祖父節點爲紅色。然後將祖父節點作爲新增節點進行迭代;
- 叔父節點是黑色,新增節點,父節點,祖父節點形成"<“或”>“形狀,即新增節點是左孩子,父節點是右孩子,或者新增節點是右孩子,父節點是左孩子,則對父節點進行左旋或者右旋,變爲”/“或”\",即變成第三種情況,在這種情況下,父子節點角色互換。
- 叔父節點是黑色,新增節點,父節點,祖父節點爲"/“或”\"構型,只需要對祖父節點進行右旋或左旋操作,將父節點作爲祖父節點塗黑,孩子節點和祖父節點作爲兩個兄弟節點塗紅,從而保證了性質4不被破壞
左旋和右旋操作,是二叉搜索樹的基本操作之一
/*
左旋示意圖(對節點x進行左旋):
px px
/ /
x y
/ \ --(左旋)--> / \ #
lx y x ry
/ \ / \
ly ry lx ly
*/
void rotate_left(Node * x, RBTree *tree)
{
Node *y = x->right, *lx = x->left, *ly = y->left, *ry = y->right;
Node t;
//將x,lx節點信息暫存
t.color = x->color;
t.key = x->key;
//用y節點信息取代x
x->color = y->color;
x->key = y->key;
x->right = y->right;
if ( &tree->nil!= y->right)
{
y->right->parent = x;
}
x->left = y;
//x節點信息轉移到y節點
y->color = t.color;
y->key = t.key;
y->left = lx;
if (&tree->nil != lx)
{
lx->parent = y;
}
y->right = ly;
}
/*
右旋示意圖(對節點y進行左旋):
py py
/ /
y x
/ \ --(右旋)--> / \
x ry lx y
/ \ / \
lx rx rx ry
*/
void rotate_right(Node *y, RBTree *tree)
{
Node *x = y->left, *lx = x->left, *ry = y->right, *rx = x->right;
Node t;
//y存於t
t.color = y->color;
t.key = y->key;
//x取代y
y->color = x->color;
y->key = x->key;
y->left = lx;
if (&tree->nil != lx)
{
lx->parent = y;
}
y->right = x;
//y取代x
x->color = t.color;
x->key = t.key;
x->left = rx;
x->right = ry;
if (&tree->nil != ry)
{
ry->parent = x;
}
}
//返回祖父節點,採用宏定義的形式更高效
Node * grand(Node *n)
{
return n->parent->parent;
}
//返回叔父節點
Node * uncle(Node *n)
{
if (n->parent == grand(n)->left)
return grand(n)->right;
else
return grand(n)->left;
}
void rb_fix(RBTree *tree, Node *node)
{
//根節點爲紅,變色
if(tree->root->color == RED)
{
tree->root->color = BLACK;
return;
}
//父節點爲黑色。無需修正
if(node->parent->color == BLACK)
{
return;
}
//情形1
if(uncle(node)->color == RED)
{
uncle(node)->color = BLACK;
node->parent->color = BLACK;
grand(node)->color = RED;
rb_fix(tree,grand(node));
return;
}
//情形2.父子節點互換,node爲父節點
if(node==node->parent->left&&uncle(node)==grand(node)->left ||
node==node->parent->right&&uncle(node)==grand(node)->right)
{
if(node == node->parent->left)
{
rotate_right(node->parent, tree);
rb_fix(node->right); // 原父節點現在是右孩子
}
else
{
rotate_left(node->parent, tree);
rb_fix(node->left);
}
return;
}
//情形3
if(node==node->parent->left&&node->parent==grand(node)->left ||
node==node->parent->right&&node->parent==grand(node)->right)
{
node->parent->color = BLACK;
grand(n)->color = RED;
if(node == node->parent->left)
{
rotate_right(node->parent->parent, tree);
}
else
{
rotate_left(node->parent->parent, tree);
}
return;
}
}
節點的刪除
對於紅黑樹刪除節點,刪除過程和二叉查找樹刪除過程大致相同,最大的區別就在於刪除後同樣需要對紅黑樹進行修正來滿足紅黑性質。
- 首先,如果待刪除節點有兩個非nil的子節點,那麼需要轉化成至多隻有一個非nil子節點的刪除問題。方法就是找到該節點的前驅或者後繼節點,刪除它的前驅或者後繼節點,並把其值放入該節點中。
- 對於至多隻有一個非nil子節點的待刪除節點,如果該節點是紅色,只需要用它的一個黑色兒子進行替補,無需修正。
- 如果是黑色的待刪除節點,則在用其子節點替補後,需要進行紅黑修正
尋找後繼節點
後繼節點即右子樹的最小元素
Node *find_next(Node *node, RBTree *tree)
{
Node *n = node->right;
while(n->left != &tree->nil)
{
n = n->left;
}
return n;
}
節點取代函數
//d爲被取代的節點,n爲取代節點
void transplant(Node *d, Node *n, RBTree *tree)
{
//被取代節點是根節點
if(d->parent == &tree->nil)
{
tree->root = n;
return;
}
if(d->parent->left == d)
{
d->parent->left = n;
}
else
{
d->parent->right = n;
}
}
刪除節點函數
void delete_node(RBTree *tree, Node *n)
{
//保存被刪除節點的顏色,用來作爲是否需要修正的條件
unsigned int flag = n->color;
//保存取代節點
Node *r;
//兩個子節點都是內部節點,用後繼節點代替該節點,刪除後繼節點
if(n->left != &tree->nil && n->right != &tree->nil)
{
n->key = find_next(n,tree)->key;
delete_node(tree, find_next(n,tree));
return;
}
//左孩子是nil
if(n->left == tree->nil)
{
r = n->right;
transplant(n,r,tree);
}
//右孩子是nil
else
{
r = n->left;
transplant(n,r,tree);
}
if(flag == BLACK)
{
rb_delete_fix(tree, r);
}
}
紅黑樹修正函數
紅黑樹修正函數接受新的替代節點作爲參數,而上述分析可知,該節點是被刪除節點的子節點。僅當被刪除節點是黑色節點時纔會調用修正函數,故分以下情況進行討論。
需要修正的原因是經過新節點的路徑比其他路徑少了一個黑色節點
- 修正節點是紅色節點,塗黑即可替代原來的黑色節點保持紅黑性質;
- 修正節點是黑色節點,且是根節點,則無需修正;
- 修正節點是黑色節點,兄弟節點、兄弟節點的子節點都是黑色,父節點也是黑色,則重繪兄弟節點爲紅色,此時相當於經過父節點的路徑都少了一個黑色節點,把父節點作爲修正節點進行迭代;
- 修正節點是黑色節點,兄弟節點、兄弟節點的子節點都是黑色,父節點是紅色,則調換兄弟節點和父節點的顏色,相當於通過修正節點路徑的黑色節點數目增加1,從而完成修正
- 修正節點是黑色節點,且是左(右)節點,兄弟節點是黑色,兄弟的右(左)孩子是紅色,左(右)孩子無要求:對父節點左(右)旋,使得兄弟節點成爲新的父節點,原來的父節點取代修正節點N,紅色的孩子節點取代兄弟節點的位置。交換父節點和兄弟節點的顏色,並重繪紅色孩子爲黑色節點。則相當於在原來修正節點的路徑上增加了父節點作爲黑色節點,原來的紅色孩子節點變黑,填補了上位的兄弟節點留下的空位。
- 修正節點是黑色節點,且是左(右)節點,兄弟節點是黑色,兄弟的左(右)孩子是紅色,右(左)孩子是黑色,對兄弟節點進行一次右(左)旋,使得黑色子節點取代兄弟節點的位置,紅色子節點取代原來黑色子節點的位置,交換原兄弟節點和紅色子節點的顏色,構成情況5,迭代調用。
- 修正節點是黑色節點,兄弟節點是紅色節點,對父節點做旋轉操作,將兄弟節點轉到祖父節點位置,兄弟節點的一個兒子節點成爲修正節點的新的兄弟節點,然後調換原來的兄弟節點和父節點的顏色,此時修正節點有了新的紅色父節點和黑色兄弟節點,可用情況4,5,6迭代。
代碼如下:
//求兄弟節點
Node * sib(Node *n)
{
if(n == n->parent->left)
{
return n->parent->right;
}
else
{
return n->parent->left;
}
}
//修正函數
void rb_delete_fix(RBTree *tree, Node *r)
{
//情況1,修正節點是紅色
if(r->color == RED)
{
r->color == BLACK;
return;
}
//情況2
if(r == tree->root)
{
return;
}
//情況7,兄弟節點爲紅色
if(sib(r)->color == RED)
{
sib(r)->color = BLACK;
r->parent->color = RED;
if(r == r->parent->left)
{
rotate_left(r->parent, tree);
}
else
{
rotate_right(r->parent, tree);
}
rb_delete_fix(tree, r);
return;
]
//情況3,兄弟節點的子節點都是黑色,父節點也是黑色
if(sib(r)->left->color == BLACK && sib(r)->right->color == BLACK && r->parent->color == BLACK)
{
sib(r)->color = RED;
rb_delete_fix(tree, r->parent);
return;
}
//情況4,兄弟節點的子節點都是黑色,父節點是紅色
if(sib(r)->left->color == BLACK && sib(r)->right->color == BLACK && r->parent->color == RED)
{
sib(r)->color = RED;
r->parent->color = BLACK;
return;
}
//情況5,兄弟有一個節點是紅色,且紅色節點的左右位置和修正節點的左右位置相對
if(r->parent->left == r && sib(r)->right->color == RED || r->parent->right == r && sib(r)->left->color == RED)
{
sib(r)->color = r->parent->color;
r->parent->color = BLACK;
if(r==r->parent->left)
{
sib(r)->right->color = BLACK;
rotate_left(r->parent,tree);
}
else
{
sib(r)->left->color = BLACK;
rotate_right(r->parent,tree);
}
return;
}
//情況6,可以不寫判斷條件,因爲其上的分支已經把其他所有情況進行了處理
if(sib(r)->left->color == RED)
{
sib(r)->color = RED;
sib(r)->left->color = BLACK;
rotate_right(sib(r), tree);
}
else
{
sib(r)->color = RED;
sib(r)->right->color = BLACK;
rotate_left(sib(r), tree);
}
rb_delete_fix(r,tree);
}
}