红黑树是一种二叉搜索树,它靠维持黑色节点的个数来保持平衡的。
红黑树的应用
红黑树应用较广,一般有两个方面的应用。一个是以key-val的方式,实现用key值查找value值的功能,如服务器中,通过socket查找与客户端对应的fd,以及操作系统中的内存块的查找等。另一个应用是利用了红黑树是二叉搜索树的性质,通过其中序遍历是有序的这一性质进行排序,例如定时器的实现。
当然,红黑树的应用还有很多,但是大多数都离不开上面提到的两条性质,这里不再列举。
红黑树和节点的实现
这里不再罗列红黑树的五条性质,直接贴出红黑树的定义。这里只实现了key域,val域在定义后面直接添加即可。
class rb_node
{
public:
char color;
rb_node *parent;
rb_node *left;
rb_node *right;
int key;
};
class rb_tree
{
public:
rb_node *nil;
rb_node *root;
};
这里提示一点,在树这个类里,定义了叶子节点(nil)。这里可以让树中所有的空都指向这一节点。另外,类的成员方法没有贴出,除构造函数外,下面将一点一点介绍。
节点的旋转
红黑树要想调整节点,如同其他平衡树一样,有左旋和右旋的操作,只要注意好3个方向上的6个指针即可。这里为了方便,暂不引入颜色的概念。
这里也可以看出,左旋和右旋是一对互逆的操作。
//介绍一个小技巧,右旋,直接在左旋的基础上,把代码中的right和left互换即可
void left_rotate(rb_node *node)
{
rb_node *y=node->right;
rb_node *z=y->left;
//第一个方向
y->parent=node->parent;
if(node->parent==nil)//此时node是根节点
{
root=y;
}else if(node->parent->left==node)//node是左孩子
{
node->parent->left=y;
}else//node是右孩子
{
node->parent->right=y;
}
//第二个方向
node->parent=y;
y->left=node;
//第三个方向
node->right=z;
if(z!=nil)
{
z->parent=node;
}
}
void right_rotate(rb_node *node)
{
rb_node *y=node->left;
rb_node *z=y->right;
//第一个方向
y->parent=node->parent;
if(node->parent==nil)
{
root=y;
}else if(node->parent->left==node)
{
node->parent->left=y;
}else
{
node->parent->right=y;
}
//第二个方向
node->parent=y;
y->right=node;
//第三个方向
node->left=z;
if(z!=nil)
{
z->parent=node;
}
}
节点的插入
还是暂不引入颜色的概念。对于一棵二叉搜索树,插入节点的过程,是从根节点开始,将插入节点的key值与树上节点的key值进行对比,小就继续往左走,大就往右走,直到最后的叶子节点,就插入到那个位置。红黑树也是这样。
void _insert(rb_node *cur,rb_node *node)
{
rb_node *temp=cur;
while(cur!=nil)
{
temp=cur;
if(node->key<cur->key)
{
cur=cur->left;
}else if(node->key>cur->key)
{
cur=cur->right;
}else//添加值在树中已存在
{
return;//这里的做法是直接返回,也可以根据需求换成其他操作,如覆盖原节点等。
}
}
if(temp==nil)
{
node->parent=nil;
root=node;
}else if(node->key<temp->key)
{
temp->left=node;
node->parent=temp;
}else//这里已经排除了相等的情况
{
temp->right=node;
node->parent=temp;
}
insert_fix(node);
}
这里对于要插入节点的key值已经在树中出现的情况,是不做任何操作,直接舍弃这个待插入节点的,当然,对于不同的需求,可以有不同的改动(如覆盖或继续添加这个节点)。
下面我们开始引入颜色的概念。
节点插入后的调整
在插入节点之前,对于整棵树而言,这个新节点是红色的好还是黑色的好?为了尽量不影响红黑树的五条性质,应该插入红色节点,因为如果插入黑色节点就会影响黑高,每次插入都会破坏第5条性质。对于插入节点是红色的情况,可能会破坏第4条性质,这时就需要根据情况调整。
首先说明一下,如果插入节点的父节点是黑色,这样插入后不会破坏红黑树的任何性质,不需要任何调整。所以,这里只有三种情况需要调整,且父节点是红色,祖父节点一定是黑色。
1.父节点红色,叔父节点红色
这种情况较为简单,直接进行颜色翻转就行,只不过要注意继续向上迭代检查一下,因为颜色翻转后,祖父节点有可能违背性质4。
情况二三相近,只不过在遇到情况二时把节点转成情况三。
2.父节点红色,叔父节点黑色,插入节点为右孩子
这种情况只需要进行一次左旋,就转换成了情况三。
3.父节点红色,叔父节点黑色,插入节点为左孩子
此时需要先将父节点和祖父节点的颜色翻转,然后还要对祖父节点进行右旋。
值得说明的是,情况二三只是截取了树的一部分,只看图中的节点是不满足红黑树的性质的。这里举一个例子,把情况二三中的z节点当作是叶子节点nil就能满足红黑树的性质了。
void insert_fix(rb_node *node)
{
while(node->parent->color==RED)
{
rb_node *y=node->parent;
if(y==y->parent->left)//插入节点的父结点是左孩子
{
rb_node *z=y->parent->right;
//第一种情况,叔父节点也是红色
if(z->color==RED)
{
//改变颜色即可
y->color=BLACK;
z->color=BLACK;
y->parent->color=RED;
node=node->parent->parent;//继续向上检查
}else//叔父节点是黑色,这里有两种情况
{
if(node==y->right)//第二种情况,插入节点是右孩子
{
left_rotate(y);
//这里是为了与下面第三种情况对接
node=y;
y=node->parent;
}
//第三种情况,插入节点是左孩子
y->color=BLACK;
y->parent->color=RED;
right_rotate(y->parent);
}//此时,在这棵子树顶端的节点是黑色,一定不会与其祖先颜色冲突
}else//插入节点的父结点是右孩子,与左孩子情况一样,左右互换即可
{
rb_node *z=y->parent->left;
if(z->color==RED)
{
y->color=BLACK;
z->color=BLACK;
y->parent->color=RED;
node=node->parent->parent;
}else
{
if(node==y->left)
{
right_rotate(y);
node=y;
y=node->parent;
}
y->color=BLACK;
y->parent->color=RED;
left_rotate(y->parent);
}
}
}
root->color=BLACK;
}
这里区分父节点是左右孩子的意义,在于方便找到叔父节点,其余都是对称操作。
下面开始进入红黑树的删除部分。
后继节点
和二叉搜索树一样,如果待删除节点的左右孩子都存在,直接删除该节点会对该树造成较大影响。为了方便,一般是把待删除节点的后继节点(与待删除节点的值接近,也可以选择其前驱节点)的值用来覆盖这个待删除节点的值,然后再删除这个后继节点。这个后继节点就是这个待删除节点的右孩子中,最左侧的节点。
//红黑树的后继节点
rb_node *successor(rb_node *node)
{
if(node==nil)
{
return nil;
}
//有右孩子
if(node->right!=nil)
{
rb_node *p=node->right;
while(p->left!=nil)
{
p=p->left;
}
return p;
}
//该节点没有右孩子,向上回溯,直到父结点为根或者是左孩子
//这一段在删除节点中没有调用,可以忽略
while(node->parent!=nil&&node->parent->right==node)
{
node=node->parent;
}
return node;
}
对于删除操作而言,用户给的不会是红黑树节点指针,而是对应的key值,所以我们也要有检索的方法。
rb_node *_search(rb_node *node,int key)
{
if(node==nil)
{
return nil;
}
if(key==node->key)
{
return node;
}
if(key<node->key)
{
return _search(node->left,key);
}
return _search(node->right,key);
}
rb_node *search(int key)
{
if(root==nil)
{
return nil;
}
rb_node *tmp=_search(root,key);
return tmp;
}
这里使用了递归的方法,也可以使用迭代的方法。
节点的删除
经过上面的替换,现在真正要删除节点的左右孩子中,至少有一个nil,所以我们可以直接删去这个节点,然后把那个子节点与其父结点相连。值得注意的是,这里还有一个待调整节点y,用于下一步节点的调整。
rb_node *del(rb_node *node)
{
if(node==nil)
{
cout<<"delete nil"<<endl;
return nil;
}
rb_node *x=nil;//真正要删除的节点
rb_node *y=nil;//旋转节点
if(node->left==nil||node->right==nil)
{
x=node;
}else
{
x=successor(node);
}
//左右孩子,一个为nil,一个实际存在
if(x->left!=nil)
{
y=x->left;
}
if(x->right!=nil)
{
y=x->right;
}
y->parent=x->parent;
if(x->parent==nil)
{
root=y;
}else if(x->parent->left==x)
{
x->parent->left=y;
}else
{
x->parent->right=y;
}
if(node!=x)
{
node->key=x->key;
}
if(x->color==BLACK)
{
del_fix(y);
}
return x;
}
节点删除后的调整
这里先想一个问题,带待删除节点是什么颜色,才会破坏红黑树的性质?如果删除了黑色节点,就会影响红黑树的黑高,势必会破坏红黑树的性质。也就是说,如果待删除节点是红色,不需要进行调整,如果是黑色,才需要调整。
这里一共有4种情况。还是和添加的情况一样,这里需要考虑待调整节点是左孩子还是右孩子的情况。因为情况相同,这里只说明待调整节点是左孩子的情况,右孩子的情况对称操作即可。
1.兄弟节点是黑色,侄子节点也都是黑色
这种情况最为简单,只需要把这个兄弟节点的颜色翻转一下就满足了黑高平衡的要求,只不过因为兄弟节点变成了红色,需要向上迭代检查一下。
2.兄弟节点是黑色,右侄子是红色
这里需要先继承父节点的颜色,并且改变父节点和这个红色侄子的颜色,还要对父节点进行左旋。
3.兄弟节点是黑色,左侄子是红色,右侄子是黑色
这种情况在完成一定操作后可以转化成情况2。
4.兄弟节点是红色
这里先把兄弟节点和父节点的颜色互换,让后再对父节点左旋,就可以转化成前三种情况。
这里可能会发现,图片中的红黑树都不满足黑高平衡的性质。注意这是删除了一个黑色节点后的结构,当然黑高不平衡。删除操作的调整,并没有写得很详细,具体的操作还是看代码。
void del_fix(rb_node *node)
{
while(node->parent!=nil&&node->color==BLACK)
{
if(node==node->parent->left)//node是左孩子
{
rb_node *w=node->parent->right;
if(w->color==RED)//第四种情况
{
w->color=BLACK;
node->parent->color=RED;
left_rotate(node->parent);
w=node->parent->right;
}
if(w->left->color==BLACK&&w->right->color==BLACK)//第一种情况
{
w->color=RED;
node=node->parent;
}else//不全是黑色,即2、3情况
{
if(w->right->color==BLACK)//第三种情况
{
w->left->color=BLACK;
w->color=RED;
right_rotate(w);
w=node->parent->right;
}
//第二种情况
w->color=node->parent->color;
node->parent->color=BLACK;
w->right->color=BLACK;
left_rotate(node->parent);
node=root;//退出循环
}
}else//node是右孩子
{
rb_node *w=node->parent->left;
if(w->color==RED)//第四种情况
{
w->color=BLACK;
node->parent->color=RED;
right_rotate(node->parent);
w=node->parent->left;
}
if(w->right->color==BLACK&&w->left->color==BLACK)//第一种情况
{
w->color=RED;
node=node->parent;
}else//不全是黑色,即2、3情况
{
if(w->left->color==BLACK)//第三种情况
{
w->right->color=BLACK;
w->color=RED;
left_rotate(w);
w=node->parent->left;
}
//第二种情况
w->color=node->parent->color;
node->parent->color=BLACK;
w->left->color=BLACK;
right_rotate(node->parent);
node=root;//退出循环
}
}
}
node->color=BLACK;
}
红黑树本身的知识点大概就是这些,主要是理解插入节点为什么是三种情况,删除节点为什么是四种情况。代码不需要强记,需要理解其中的原理和操作。
红黑树完成之后,也可以用对数器进行检验,将自己的红黑树与网上找的一版红黑树进行对比,如果没有不同,则说明自己的红黑树的实现没有问题,这里代码不再贴出。