红黑树浅析
概要
目录
1 红黑树的介绍2 红黑树的应用
3 红黑树的操作流程
4 红黑树的基本操作(一)左旋和右旋
5 红黑树的基本操作(二)添加
6 红黑树的基本操作(三)删除
R-B Tree简介
R-B Tree,全称是Red-Black Tree,又称为“红黑树”,它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。
红黑树的特性:
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
注意:
(01) 特性(3)中的叶子节点,是只为空(NIL或null)的节点。
(02) 特性(5),确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。
红黑树示意图如下:
红黑树的应用比较广泛,主要是用它来存储有序的数据,它的时间复杂度是O(lgn),效率非常之高。
例如,Java集合中的TreeSet和TreeMap,C++ STL中的set、map,以及Linux虚拟内存的管理,都是通过红黑树去实现的。
红黑树的操作流程
红黑树的结点
#ifndef _RB_TREE_NODE_H_
#define _RB_TREE_NODE_H_
//#ifndef bool
//typedef enum bool{false ,true}bool;
//#endif
#ifndef RB_TREE_COLOR
typedef enum RB_TREE_COLOR{
RB_RED = 0,
RB_BLACK = 1
};
#endif
/************************************************************************/
/* 名称:红黑树的结点
时间:2016-08-19
*/
/************************************************************************/
template <class T> //结点元素类型
struct rb_tree_node
{
T key; //关键字(键值)
RB_TREE_COLOR color;
rb_tree_node<T> *left;
rb_tree_node<T> *right;
rb_tree_node<T> *parent;
rb_tree_node(const T &val = 0 , const RB_TREE_COLOR &col = RB_RED
/*, const rb_tree_node *p = NULL*/);
~rb_tree_node(void);
};
#include "rb_tree_node.cpp"
#endif
#ifndef _RB_TREE_NODE_CPP_
#define _RB_TREE_NODE_CPP_
#include "rb_tree_node.h"
template <class T>
rb_tree_node<T>::rb_tree_node(const T &val , const RB_TREE_COLOR &col
/*, const rb_tree_node *p = NULL*/):
left(NULL) , right(NULL) ,parent(NULL) ,key(val) , color(col){
}
template <class T>
rb_tree_node<T>::~rb_tree_node(void)
{
}
#endif
红黑树的基本操作(一)左旋和右旋
红黑树的基本操作是添加、删除。在对红黑树进行添加或删除之后,都会用到旋转方法。为什么呢?道理很简单,添加或删除红黑树中的节点之后,红黑树就发生了变化,可能不满足红黑树的5条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转,可以使这颗树重新成为红黑树。简单点说,旋转的目的是让树保持红黑树的特性。
旋转包括两种:左旋 和 右旋。下面分别对它们进行介绍。
/*************对红黑树节点x进行左旋操作 ******************/
/*
* 左旋示意图:对节点x进行左旋
* p p
* / /
* x y
* / \ / \
* lx y -----> x ry
* / \ / \
* ly ry lx ly
* 左旋做了三件事:
* 1. 将y的左子节点赋给x的右子节点,并将x赋给y左子节点的父节点(y左子节点非空时)
* 2. 将x的父节点p(非空时)赋给y的父节点,同时更新p的子节点为y(左或右)
* 3. 将y的左子节点设为x,将x的父节点设为y
*/
template <class T>
void rb_tree<T>::left_rotate(rb_tree_node<T> *x){
rb_tree_node<T> *y = x->right ; // 前提:这里假设x的右孩子为y。下面开始正式操作
/* xy之间断开重连 */
y->parent = x->parent; // 将 “x的父亲” 设为 “y的父亲”
x->right = y->left; // 将 “y的左孩子” 设为 “x的右孩子”,即 将ly设为x的右孩子
/* x p 之间断开重连 */
if( !x->parent ) // 情况1:如果 “x的父亲” 是空节点,则将y设为根节点
root = y;
else if( x->parent->left == x ) // 情况2:如果 x是它父节点的左孩子,则将y设为“x的父节点的左孩子”
x->parent->left = y;
else // 情况3:(x是它父节点的右孩子) 将y设为“x的父节点的右孩子”
x->parent->right = y;
x->parent = y; // 将 “x的父节点” 设为 “y”
/* y ly 之间断开重连 */
if(y->left)
y->left->parent = x; // 将 “x” 设为 “y的左孩子的父亲”,即 将ly的父亲设为x
y->left = x; // 将 “x” 设为 “y的左孩子”
}
/*************对红黑树节点y进行右旋操作 ******************/
/*
* 左旋示意图:对节点y进行右旋
* p p
* / /
* y x
* / \ / \
* x ry -----> lx y
* / \ / \
* lx rx rx ry
* 右旋做了三件事:
* 1. 将x的右子节点赋给y的左子节点,并将y赋给x右子节点的父节点(x右子节点非空时)
* 2. 将y的父节点p(非空时)赋给x的父节点,同时更新p的子节点为x(左或右)
* 3. 将x的右子节点设为y,将y的父节点设为x
*/
template <class T>
void rb_tree<T>::right_rotate(rb_tree_node<T> *y){
rb_tree_node<T> *x = y->left ; // 前提:这里假设y的左孩子为x。下面开始正式操作
/* xy之间断开重连 */
x->parent = y->parent; // 将 “y的父亲” 设为 “x的父亲”
y->left = x->right; // 将 “x的右孩子” 设为 “y的左孩子”,即 将rx设为y的左孩子
/* x p 之间断开重连 */
if( !y->parent ) // 情况1:如果 “y的父亲” 是空节点,则将x设为根节点
root = x;
else if( y->parent->left == y ) // 情况2:如果 y是它父节点的左孩子,则将x设为“y的父节点的左孩子”
x->parent->left = x;
else // 情况3:(y是它父节点的右孩子) 将x设为“y的父节点的右孩子”
x->parent->right = x;
y->parent = x; // 将 “y的父节点” 设为 “x”
/* y ly 之间断开重连 */
if(x->right)
x->right->parent = y ; // 将 “y” 设为 “x的右孩子的父亲”,即 将rx的父亲设为y
x->right = y; // 将 “y” 设为 “x的右孩子”
}
红黑树的基本操作(二)添加
将一个节点插入到红黑树中,需要执行哪些步骤呢?首先,将红黑树当作一颗二叉查找树,将节点插入;然后,将节点着色为红色;最后,通过旋转和重新着色等方法来修正该树,使之重新成为一颗红黑树。详细描述如下:
第一步: 将红黑树当作一颗二叉查找树,将节点插入。
红黑树本身就是一颗二叉查找树,将节点插入后,该树仍然是一颗二叉查找树。也就意味着,树的键值仍然是有序的。此外,无论是左旋还是右旋,若旋转之前这棵树是二叉查找树,旋转之后它一定还是二叉查找树。这也就意味着,任何的旋转和重新着色操作,都不会改变它仍然是一颗二叉查找树的事实。
好吧?那接下来,我们就来想方设法的旋转以及重新着色,使这颗树重新成为红黑树!
template <class T>
bool rb_tree<T>::insert(const T &val){
/* 创建新结点 */
rb_tree_node<T> *new_node = new rb_tree_node<T>(val);
/* 调用重载版本*/
return this->insert(new_node);
}
template <class T>
bool rb_tree<T>::insert(rb_tree_node<T> *new_node){
rb_tree_node<T> *p ,*q;
//p = NULL; q = NULL;
/* start to search */
q = p = this->get_root();
while(p){
q = p;
if(p->key < new_node->key )
p = p->right;
else if( p->key > new_node->key )
p = p->left;
else
return false; //在tree找到相同key的结点, 返回false
}
/* judge left or right */
if(!q)
this->set_root(new_node);
else if( q->key > new_node->key )
q->left = new_node;
else
q->right = new_node;
/* set new_node's parent */
new_node->parent = q;
/* fixup */
insert_fixup(new_node);
return true;
}
第二步:将插入的节点着色为"红色"。
为什么着色成红色,而不是黑色呢?为什么呢?在回答之前,我们需要重新温习一下红黑树的特性:
(1) 每个节点或者是黑色,或者是红色。
(2) 根节点是黑色。
(3) 每个叶子节点是黑色。 [注意:这里叶子节点,是指为空的叶子节点!]
(4) 如果一个节点是红色的,则它的子节点必须是黑色的。
(5) 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
将插入的节点着色为红色,不会违背"特性(5)"!少违背一条特性,就意味着我们需要处理的情况越少。接下来,就要努力的让这棵树满足其它性质即可;满足了的话,它就又是一颗红黑树了。
第三步: 通过一系列的旋转或着色等操作,使之重新成为一颗红黑树。
第二步中,将插入节点着色为"红色"之后,不会违背"特性(5)"。那它到底会违背哪些特性呢?
对于"特性(1)",显然不会违背了。因为我们已经将它涂成红色了。
对于"特性(2)",显然也不会违背。在第一步中,我们是将红黑树当作二叉查找树,然后执行的插入操作。而根据二叉查找数的特点,插入操作不会改变根节点。所以,根节点仍然是黑色。
对于"特性(3)",显然不会违背了。这里的叶子节点是指的空叶子节点,插入非空节点并不会对它们造成影响。
对于"特性(4)",是有可能违背的!
那接下来,想办法使之"满足特性(4)",就可以将树重新构造成红黑树了。
下面看看流程到底是怎样实现这选择和重新着色的。
根据被插入节点的父节点的情况,可以将"当节点z被着色为红色节点,并插入二叉树"划分为三种情况来处理。
① 情况说明:被插入的节点是根节点。
处理方法:直接把此节点涂为黑色。
② 情况说明:被插入的节点的父节点是黑色。
处理方法:什么也不需要做。节点被插入后,仍然是红黑树。
③ 情况说明:被插入的节点的父节点是红色。
处理方法:那么,该情况与红黑树的“特性(5)”相冲突。这种情况下,被插入节点是一定存在非空祖父节点的;进一步的讲,被插入节点也一定存在叔叔节点(即使叔叔节点为空,我们也视之为存在,空节点本身就是黑色节点)。理解这点之后,我们依据"叔叔节点的情况",将这种情况进一步划分为3种情况(Case)。
现象说明 | 处理策略 | |
Case 1 | 当前节点的父节点是红色,且当前节点的祖父节点的另一个子节点(叔叔节点)也是红色。 | (01) 将“父节点”设为黑色。 |
Case 2 | 当前节点的父节点是红色,叔叔节点是黑色,且当前节点是其父节点的右孩子 | (01) 将“父节点”作为“新的当前节点”。 |
Case 3 | 当前节点的父节点是红色,叔叔节点是黑色,且当前节点是其父节点的左孩子 | (01) 将“父节点”设为“黑色”。 |
template <class T>
void rb_tree<T>::insert_fixup(rb_tree_node<T> *new_node){
rb_tree_node<T> *P , *G , *S;
/*
* 注 :这里虽然多了内存空间, 但节省了寻找结点的父结点 ,
* 祖父结点,兄弟结点的时间 ,因为需要指针一个个找
*/
/* 循环调整 */
while (true){
/* init P */
P = new_node->parent;
/* 情况1:插入的点为根结点 */
if (!P){
new_node->color = RB_BLACK;
break;
}
/* 情况2:插入的点的父结点为黑色, 什么都不做, 结束调整 */
else if( P->color == RB_BLACK )
break;
/* 情况2:插入的点的父结点为红色 , 又有4种情况 */
else{
/* 若父节点是祖父节点的左子节点,下面else与其相反 */
/* 当父结点P为红色时, 祖父结点G 是肯定存在的, 否则p就是root ,产生矛盾*/
G = P->parent;
if ( P == G->left){
/* 叔叔节点是红色,就是情况3的 c , d */
S = G->right;
if( S && S->color == RB_RED ){
/* 情况3的 d */
if ( new_node == P->right ){
new_node = P;
left_rotate(new_node);
/* 完成旋转后 , 重新设定PGS */
P = new_node->parent;
G = P->parent;
S = G->right;
}
/* 情况3的 c */
P->color = RB_BLACK;
G->color = RB_RED;
S->color = RB_BLACK; //可以看出, 这是情况cd与ab处理的区别
new_node = G;
}
/* 叔叔节点是黑色,就是情况3的 a , b */
else{
/* 情况3的 b */
if ( new_node == P->right ){
new_node = P;
left_rotate(new_node);
/* 完成旋转后 , 重新设定PGS */
P = new_node->parent;
G = P->parent;
//S = G->right;
}
/* 情况3的 a */
P->color = RB_BLACK;
G->color = RB_RED;
//new_node = G;
/* 以“祖父节点”为支点进行右旋。 */
right_rotate(G);
}
}
/* 若父节点是祖父节点的右子节点,与上面的完全相反,镜像处理 */
else{
/* 情况3的 c , d */
S = G->left;
if( S && S->color == RB_RED ){
/* 情况3的 d */
if ( new_node == P->left ){
new_node = P;
right_rotate(new_node);
/* 完成旋转后 , 重新设定PGS */
P = new_node->parent;
G = P->parent;
S = G->left;
}
/* 情况3的 c */
P->color = RB_BLACK;
G->color = RB_RED;
S->color = RB_BLACK; //可以看出, 这是情况cd与ab处理的区别
new_node = G;
}
/* 情况3的 a , b */
else{
/* 情况3的 b */
if ( new_node == P->left ){
new_node = P;
right_rotate(new_node);
///* 完成旋转后 , 重新设定PGS */
P = new_node->parent;
G = P->parent;
//S = G->left;
}
/* 情况3的 a */
P->color = RB_BLACK;
G->color = RB_RED;
//new_node = G;
/* 以“祖父节点”为支点进行左旋。 */
left_rotate(G);
}
}
}
}
}
红黑树的基本操作(三)删除
将红黑树内的某一个节点删除。需要执行的操作依次是:首先,将红黑树当作一颗二叉查找树,将该节点从二叉查找树中删除;然后,通过"旋转和重新着色"等一系列来修正该树,使之重新成为一棵红黑树。详细描述如下:
第一步:将红黑树当作一颗二叉查找树,将节点删除。
这和"删除常规二叉查找树中删除节点的方法是一样的"。分3种情况:
① 被删除节点没有儿子,即为叶节点。那么,直接将该节点删除就OK了。
② 被删除节点只有一个儿子。那么,直接删除该节点,并用该节点的唯一子节点顶替它的位置。
③ 被删除节点有两个儿子。那么,先找出它的后继节点;然后把“它的后继节点的内容”复制给“该节点的内容”;之后,删除“它的后继节点”。在这里,后继节点相当于替身,在将后继节点的内容复制给"被删除节点"之后,再将后继节点删除。这样就巧妙的将问题转换为"删除后继节点"的情况了,下面就考虑后继节点。 在"被删除节点"有两个非空子节点的情况下,它的后继节点不可能是双子非空。既然"的后继节点"不可能双子都非空,就意味着"该节点的后继节点"要么没有儿子,要么只有一个儿子。若没有儿子,则按"情况① "进行处理;若只有一个儿子,则按"情况② "进行处理。
/*********************** 得到当前结点的后继结点,即大于节点x的最小节点 **********************/
template <class T>
rb_tree_node<T> *rb_tree<T>::successor(rb_tree_node<T> *p_cur){
rb_tree_node<T> *p_ret , *p_par ;
/* 判断条件 */
if( p_cur ){
if ( p_cur->right ){
/* 一直左下,找到叶结点 */
p_ret = p_cur->right;
while(p_ret->left)
p_ret = p_ret->left;
}
else{
/* 没有右结点,则查看父结点与当前结点的关系 */
//如果x没有右子节点,会出现以下两种情况:
//1. x是其父节点的左子节点,则x的后继节点为它的父节点
//2. x是其父节点的右子节点,则先查找x的父节点p,然后对p
//再次进行这两个条件的判断
p_par = p_ret->parent;
while( !p_par&&p_par->right == p_ret ){
p_ret = p_par;
p_par = p_ret->parent;
}
p_ret = p_par;
}
}
return p_ret;
}
/*********************** 删除红黑树中的节点 **********************/
template <class T>
bool rb_tree<T>::remove(const T &val){
rb_tree_node<T> *rm_node ;
bool b_ret;
b_ret = false;
if( !( rm_node = this->search(val)) )
b_ret = this->remove(rm_node);
return b_ret;
}
/*********************** 删除红黑树中的节点,重载版本 **********************/
template <class T>
bool rb_tree<T>::remove(rb_tree_node<T> *rm_node){
rb_tree_node<T> *Y ,*X;
RB_TREE_COLOR Y_color;
// 若“z的左孩子” 或 “z的右孩子”为空,则将“z”赋值给 “y”;
// 否则,将“z的后继节点”赋值给 “y”。
if( !rm_node->left || !rm_node->right )
/* case 1 2 3 */
Y = rm_node;
else
/* case 4 */
Y = this->successor(rm_node);
// 若“y的左孩子” 不为空,则将“y的左孩子” 赋值给 “x”;
// 否则,“y的右孩子” 赋值给 “x”。 注意右孩子有可能为空
if( Y->left )
X = Y->left;
else
X = Y->right;
/* 开始处理 P与X的连接 */
// 将“y的父节点” 设置为 “x的父节点”
if(X)
X->parent = Y;
if(!Y->parent)
root = X; //删除的结点y为根结点
else if (Y->parent->left == Y)
// 情况2:若“y是它父节点的左孩子”,则设置“x” 为 “y的父节点的左孩子”
Y->parent->left = X;
else
// 情况3:若“y是它父节点的右孩子”,则设置“x” 为 “y的父节点的右孩子”
Y->parent->right = X;
/* 若Y是后继结点 */
if ( Y != rm_node ){
// 若“y的值” 赋值给 “z”。注意:这里只拷贝z的值给y,而没有拷贝z的颜色!!!
rm_node->key = Y->key;
}
Y_color = Y->color;
/* 删除结点Y */
delete Y; Y = NULL;
/*因为删除了一个黑色结点, 就要开始调整*/
if( Y_color == RB_BLACK )
this->remove_fixup(X);
return true;
}
第二步:通过"旋转和重新着色"等一系列来修正该树,使之重新成为一棵红黑树。
因为"第一步"中删除节点之后,可能会违背红黑树的特性。所以需要通过"旋转和重新着色"来修正该树,使之重新成为一棵红黑树。
删除某个节点后,会用它的后继节点来填上,并且后继节点会设置为和删除节点同样的颜色,所以删除节点的那个位置是不会破坏平衡的。可能破坏平衡的是后继节点原来的位置,因为后继节点拿走了,原来的位置结构改变了,这就会导致不平衡的出现。所以后面的remove_fixup方法中传入的参数也是后继节点的子节点.
为了方便下文的叙述,我们现在约定:后继节点的子节点称为“当前节点”。
删除操作后,如果当前节点是黑色的根节点,那么不用任何操作,因为并没有破坏树的平衡性,即没有违背红-黑树的规则,这很好理解。如果当前节点是红色的,说明刚刚移走的后继节点是黑色的,那么不管后继节点的父节点是啥颜色,我们只要将当前节点涂黑就可以了,红-黑树的平衡性就可以恢复。但是如果遇到以下四种情况,我们就需要通过变色或旋转来恢复红-黑树的平衡了。
现象说明 | 处理策略 | |
Case 1 | x是"黑+黑"节点,x的兄弟节点是红色。(此时x的父节点和x的兄弟节点的子节点都是黑节点)。 | (01) 将x的兄弟节点设为“黑色”。 |
Case 2 | x是“黑+黑”节点,x的兄弟节点是黑色,x的兄弟节点的两个孩子都是黑色。 | (01) 将x的兄弟节点设为“红色”。 |
Case 3 | x是“黑+黑”节点,x的兄弟节点是黑色;x的兄弟节点的左孩子是红色,右孩子是黑色的。 | (01) 将x兄弟节点的左孩子设为“黑色”。 |
Case 4 | x是“黑+黑”节点,x的兄弟节点是黑色;x的兄弟节点的右孩子是红色的,x的兄弟节点的左孩子任意颜色。 | (01) 将x父节点颜色 赋值给 x的兄弟节点。 |
template <class T>
void rb_tree<T>::remove_fixup(rb_tree_node<T> *rep_node){
rb_tree_node<T> *p ,*s , *ls , *rs;
/* 先处理情况3 ,情况1 ,2 在循环结束可以直接将rep_node 设置为black即可 */
while(rep_node != this-get_root() && rep_node->color == RB_BLACK){
/* X不可能为空,在上面的函数可以证实 */
p = rep_node->parent;
if ( p->left == rep_node ){
/*rep_node是左子节点,下面else与这里的刚好相反 */
s = p->right;
if ( s->color == RB_RED ){
/* case 1 ,因为兄弟结点为red , 其子女必定为black, 所以不用判断*/
p->color = RB_RED;
s->color = RB_BLACK;
this->left_rotate(p);
s = p->right;
}
/* 这里不加else的原因 , 看流程图 */
ls = s->left;
rs = s->right;
if( ( ls && ls->color == RB_BLACK ) && ( rs && rs->color == RB_BLACK ) ){
/* case 2 */
s->color = RB_RED ;
rep_node = p;
}
else{
/* case 3 , 4 */
if (ls && ls->color == RB_RED && rs && rs->color == RB_BLACK ){
/* case 3 */
ls->color = RB_BLACK ;
s->color = RB_RED ;
this->right_rotate(s);
s = p->right;
ls = s->left;
rs = s->right;
}
/* case 4 */
s->color = p->color ;
p->color = RB_BLACK ;
rs->color = RB_BLACK;
this->left_rotate(p);
break;
}
}
else{
/* rep_node是右子节点 , 与第一个if对应 */
s = p->left;
if ( s->color == RB_RED ){
/* case 1 ,因为兄弟结点为red , 其子女必定为black, 所以不用判断*/
p->color = RB_RED;
s->color = RB_BLACK;
this->right_rotate(p);
s = p->left;
}
/* 这里不加else的原因 , 看流程图 */
ls = s->left;
rs = s->right;
if( ( ls && ls->color == RB_BLACK ) && ( rs && rs->color == RB_BLACK ) ){
/* case 2 */
s->color = RB_RED ;
rep_node = p;
}
else{
/* case 3 , 4 */
if (rs && rs->color == RB_RED && ls && ls->color == RB_BLACK ){
/* case 3 */
rs->color = RB_BLACK ;
s->color = RB_RED ;
this->right_rotate(s);
s = p->left;
ls = s->left;
rs = s->right;
}
/* case 4 */
s->color = p->color ;
p->color = RB_BLACK ;
ls->color = RB_BLACK;
this->left_rotate(p);
break;
}
}
}
root->color = RB_BLACK;
}
最后:有人写了个在线演示版本,挺不错的:http://sandbox.runjs.cn/show/2nngvn8w