最近闲来无事,一直没有研究过红黑树,B树,B+树之类的,打算自己用C语言实现一下它们。
红黑树的性质定义:
- 节点只能是黑色或者红色。
- 根节点必须是黑色。
- 每个叶子节点是黑色节点(称之为NIL节点,又被称为黑哨兵);可以理解为红黑树中每个节点都有两个子节点,除了黑色的空节点。
- 每个红色节点的两个子节点都是黑色(或者说从每个叶子节点到根的所有路径上不能有两个连续的红色节点)。
- 从任一节点到它所能到达的叶子节点的所有简单路径都包含相同数目的黑色节点。
上面就是红黑树的定义了,光有定义并不能帮助我们更好去理解红黑树。因此我在网上找了一些资料,发现一颗红黑树可以等价于一颗234树,红黑树就是234树的一种实现方式。而一颗234树可以对应多颗红黑树。
234树与红黑树的对应关系
举个例子
这是一颗234树,我们现在将他转化成一颗红黑树
红黑树1
红黑树2
由此我们可以知道,一颗234树可以对应多个红黑树,而一颗红黑树只有对应的一颗234树。
这里我们说一下234树的特点
-
每个节点每个节点有1、2或3个key,分别称为2(孩子)节点,3(孩子)节点,4(孩子)节点。
-
所有叶子节点到根节点的长度一致(也就是说叶子节点都在同一层)。
-
每个节点的key从左到右保持了从小到大的顺序,两个key之间的子树中所有的key一定大于它的父节点的左key,小于父节点的右key
因此我们可以得到这样的对应关系
-
2节点对应红黑树上的一个黑色节点
-
3节点对应红黑树上两种状态,上黑下红
-
4节点对应红黑树上,上黑下两红
-
裂变状态下(中间状态,本身是4节点,现在又插入一个节点),上红下两黑
红黑树的旋转
现在我们大概知道了234树和红黑树转换关系,接下来就是我们如何去理解旋转的问题。
需要理解旋转,我们就必须去理解,AVL树的一些概念
avl树对子树的高度有一种要求:对于每个节点来说,这个节点的左右子树的高度最多差1
为了保持AVL树的平衡,就产生了旋转的概念,那我们为什么不使用AVL树来实现MAP那?我自己理解因为旋转的次数太多,可能会程序的运行效率吧。红黑树就是一种近似平衡,不会产生那么多次的旋转,效率会更高一点。
如图所示,这就是左旋和右旋的转换示意图。
这里我们就把对应的红黑树的定义的代码贴出来
#define RED 0
#define BLACK 1
typedef struct _RBTREE_NODE
{
int key;
void* value;
struct _RBTREE_NODE* right;
struct _RBTREE_NODE* left;
struct _RBTREE_NODE* parent;
unsigned char color;
}RBTREE_NODE,*PRBTREE_NODE;
typedef struct _RBTREE
{
struct _RBTREE_NODE* root;
struct _RBTREE_NODE* nil;
}RBTREE,*PRBTREE;
这里是根据我们的示意图写出来旋转的代码
旋转的代码(包括左旋和右旋)
//左旋
void rbtree_left_rotate(PRBTREE T,PRBTREE_NODE x)
{
//右子树
PRBTREE_NODE y = x->right;
x->right = y->left;
if (y->left!=T->nil)
{
y->left->parent = x;
}
y->parent = x->parent;
if (x->parent==T->nil)
{
T->root = y;
}
else if (x == x->parent->left)
{
x->parent->left = y;
}
else
{
x->parent->right = y;
}
y->left = x;
x->parent = y;
}
//右旋
void rbtree_right_rotate(PRBTREE T, PRBTREE_NODE y)
{
//右子树
PRBTREE_NODE x = y->left;
y->left = x->right;
if (x->right != T->nil)
{
x->right->parent = y;
}
x->parent = y->parent;
if (y->parent == T->nil)
{
T->root = x;
}
else if (y == y->parent->right)
{
y->parent->right = x;
}
else
{
y->parent->left = x;
}
x->right = y;
y->parent = x;
}
红黑树的插入
现在我们就来进行红黑树的最难的两个部分的研究与学习,为了更好的理解红黑树插入的过程,我来先演示一下1-10的234树插入的过程,我们借助234树的插入,让我们去了解红黑树的插入,这样更便于我们去理解。
首先是1-3的插入
从4开始的插入操作,就会开始影响我们234树的节点定义,需要开始进行调整。
插入4节点
插入5节点
插入6节点
插入7节点
插入8节点
插入9节点
插入10节点
到这里,我们演示了一下1-10的234树的插入的过程。
由于234树与红黑树是等价的。
我们总结一下234树插入的规律。
- 插入都是向最下层插入的
- 将2节点升级成3节点,或者将3节点升级成4节点。
- 向4节点插入元素后,需要将中间元素作为新的父节点,原结点变成两个2节点,再把元素插入2节点中,如果父节点也是4节点,则递归向上层进行分裂,至到根结点后将树高加1;
我们通过234树的插入规律应用到红黑树的插入规律:
- 新插入的节点颜色为红色,这样才可能不会对红黑树的高度产生影响。(为什么插入的节点默认是红色的)
- 2节点对应红黑树中的单个黑色结点,插入时直接成功(2节点升级为3节点)。
- 3节点对应红黑树中的黑+红子树,插入后将其修复成 红+黑+红 子树(对应3节点的图形);
- 4节点对应红黑树中的红+黑+红子树,插入后将其修复成红色祖父+黑色父叔+红色孩子子树,然后再把祖父节点当成新插入的红色节点递归向上层修复,直至修复成功或遇到root结点;
在这里我们针对几种插入的情况进行分析:
首先是二节点插入一个新的元素变成3节点
其次就是3节点变成一个4节点,这里我们讨论了几种情况方便我们编写代码。
这些就是从3节点编程一个4节点的情况,我们列举出来了所有可能的情况。
最后就是4节点插入元素进行裂变的过程的处理
以上就是对插入情况的总结,我们来完成对应的红黑树的插入的代码的实现。
我们首先就是正常的进行二叉树的插入,然后再插入后在对红黑树进行修改处理。
正常的二叉树的插入
void rbtree_insert(PRBTREE T, PRBTREE_NODE node)
{
PRBTREE_NODE x = T->root;
PRBTREE_NODE y = T->nil;
while (x!=T->nil)
{
y = x;
if (node->key<x->key)
{
x = x->left;
}
else if (node->key > x->key)
{
x = x->right;
}
else {
return;
}
}
if (y==T->nil)
{
T->root = node;
}
else
{
if (y->key > node->key)
{
y->left = node;
}
else
{
y->right = node;
}
}
node->parent = y;
node->left = T->nil;
node->right = T->nil;
node->color = RED;
rbtree_insert(T,node);
}
插入后调整
void rbtree_insert_fixup(PRBTREE T, PRBTREE_NODE node)
{
//node == Red
while (node->parent->color==RED)
{
if (node->parent==node->parent->parent->left)
{
//叔叔节点
//4->2+2
PRBTREE_NODE y = node->parent->parent->right;
if (y->color==RED)
{
node->parent->color = BLACK;
y->color = BLACK;
node->parent->parent->color = RED;
node = node->parent->parent; //z --> RED
}
else
{ //y=BLACK
if (node==node->parent->right)
{
// 15
// /
// 12
// \
// 14
node = node->parent;
rbtree_left_rotate(T, node);
}
// 15
// /
// 14
// /
// 12
node->parent->color = BLACK;
node->parent->parent->color = RED;
rbtree_right_rotate(T, node->parent->parent);
}
}
else
{
PRBTREE_NODE y = node->parent->parent->left;
if (y->color == RED)
{
//叔叔节点
//4->2+2
node->parent->color = BLACK;
y->color = BLACK;
node->parent->parent->color = RED;
}
else
{
if (node==node->parent->left)
{
// 15
// \
// 17
// /
// 16
node = node->parent;
rbtree_right_rotate(T, node);
}
// 15
// \
// 16
// \
// 17
node->parent->color = BLACK;
node->parent->parent->color = RED;
rbtree_left_rotate(T, node->parent->parent);
}
}
}
}
二叉树的删除
现在我们到了红黑树最难理解的部分,就是红黑树的删除操作,我们首先需要了解一下234树的删除,再去对比红黑树的删除,帮助我们更好的去理解红黑树的删除。
这里我们忽略了二叉树删除的时候会寻找前驱和后驱节点的查找。默认已经理解了这个概念,不然文章就太长了。(如果看的人多,可以留下评论,我再把这部分内容补上。)
前驱节点:当前节点左子树的最右节点(例如1节点的前驱节点为5),若无左子树,则:当前节点是其父节点的右子树(5节点的前驱节点为2).
后继节点:当前节点右子树的最左节点(1节点的后继节点为6),若无右子树,则:当前节点为其父节点的左子树(6节点的后继节点为3).
查找前驱或者后继节点
PRBTREE_NODE rbtree_min(PRBTREE T, PRBTREE_NODE x) {
while (x->left != T->nil) {
x = x->left;
}
return x;
}
PRBTREE_NODE rbtree_max(PRBTREE T, PRBTREE_NODE x) {
while (x->right != T->nil) {
x = x->right;
}
return x;
}
PRBTREE_NODE rbtree_successor(PRBTREE T, PRBTREE_NODE x) {
if (x->right != T->nil) {
return rbtree_min(T, x->right);
}
PRBTREE_NODE y = x->parent;
while ((y != T->nil) && (x == y->right)) {
x = y;
y = y->parent;
}
return y;
}
PRBTREE_NODE rbtree_predecessor(PRBTREE T, PRBTREE_NODE x) {
if (x->right != T->nil) {
return rbtree_max(T, x->left);
}
PRBTREE_NODE y = x->parent;
while ((y != T->nil) && (x == y->left)) {
x = y;
y = y->parent;
}
return y;
}
234树的删除,我们这里也是分几种情况去讨论的,看我们的图把
比较复杂的地方,是2节点删除的问题,一般非2节点的删除,并不会有什么太大的影响。
非二节点的删除
3节点的删除
4节点的删除
现在就是二节点删除的一些情况了,我们这里来讨论一下
对于2节点的删除,需要转换为3、4节点中节点的删除
父节点为非2节点,兄弟节点为2节点
父节点为非2节点,兄弟节点为非2节点
父节点为2节点,兄弟节点非2节点
父节点为2节点,兄弟节点2节点
这里就是234树删除节点的几种情况,我们应该如何的处理,如图所示。
我们大概的总结一下
- 查找最近的叶子结点中的元素替代被删除元素,删除替代元素后,从替代元素所处叶子结点开始处理;
- 降低节点:4-结点变3-结点,3-结点变2-结点;
- 2-结点中只有一个元素,所以借兄弟结点中的元素来补充删除后的造成的空结点;
- 当兄弟结点中也没有多个元素可以补充时,尝试将父结点降低,失败时向上递归,至到子树降元成功或到Root结点树高减1
将这些规则对应到红黑树中即:
- 查找离当前结点最近的叶子结点作为替代结点(左子树的最右结点或右子树的最左结点都能保证替换后保证二叉查找树的结点的排序性质,叶子结点的替代结点是自身)替换掉被删除结点,从替代的叶子结点向上递归修复;
- 替代结点颜色为红色(对应 2-3-4树中 4-结点或 3-结点)时删除子结点直接成功;
- 替代结点为黑色(对应 2-3-4树中 2-结点)时,意味着替代结点所在的子树会降一层,需要依次检验以下三项,以恢复子树高度:
兄弟结点的子结点中有红色结点(兄弟结点对应 3-结点或 4-结点)能够“借用”,旋转过来后修正颜色;
父结点是红色结点(父结点对应 3-结点或 4-结点,可以降元)时,将父结点变黑色,自身和兄弟结点变红色后删除;
父结点和兄弟结点都是黑色时,将子树降一层后把父结点当作替代结点递归向上处理.
我们需要先正常删除我们的二叉树节点,然后在删除的节点为黑色节点的时候,进行调整,如果删除的是红色节点,不会影响红黑树的平衡,不需要平衡。
判断是红色节点的情况,5的兄弟节点应该是7,不是8.
红黑树的调整删除的代码
void rbtree_delete_fixup(PRBTREE T, PRBTREE_NODE x)
{
//如果 X不是根节点,同时X的颜色是黑色
while ((x != T->root) && (x->color == BLACK)) {
//如果删除的节点是左节点,同时是234树的2节点
if (x == x->parent->left) {
//情况2:如果兄弟节点有子节点,就借用
PRBTREE_NODE w = x->parent->right;
//这里判断是否是红色节点,如果是红色节点,说明不是真正的兄弟节点,需要进行左旋
if (w->color == RED) {
w->color = BLACK;
x->parent->color = RED;
rbtree_left_rotate(T, x->parent);
w = x->parent->right;
}
//情况3如果兄弟节点没有孩子节点,(通过自损,避免树的不平衡)
if ((w->left->color == BLACK) && (w->right->color == BLACK)) {
w->color = RED;
x = x->parent;
}
else {
//情况2 的第1种小情况,如果兄弟节点是3节点,需要通过2次旋转(借1个节点)
//右孩子为空的情况
// 6(R) 6(R)
// / \ / \
// 5(B) 7(B)(W) ==> 5(B) 6.5(b)
// / \
// 6.5(R) 7(r)
if (w->right->color == BLACK) {
w->left->color = BLACK;
w->color = RED;
rbtree_right_rotate(T, w);
w = x->parent->right;
}
//情况2 的第2种小情况,如果兄弟节点是4节点,需要通过1次旋转(借2个节点,避免了1次旋转)
w->color = x->parent->color;
x->parent->color = BLACK;
w->right->color = BLACK;
rbtree_left_rotate(T, x->parent);
//停止循环的条件
x = T->root;
}
}
//相反的情况不做讨论
else {
PRBTREE_NODE w = x->parent->left;
if (w->color == RED) {
w->color = BLACK;
x->parent->color = RED;
rbtree_right_rotate(T, x->parent);
w = x->parent->left;
}
if ((w->left->color == BLACK) && (w->right->color == BLACK)) {
w->color = RED;
x = x->parent;
}
else {
if (w->left->color == BLACK) {
w->right->color = BLACK;
w->color = RED;
rbtree_left_rotate(T, w);
w = x->parent->left;
}
w->color = x->parent->color;
x->parent->color = BLACK;
w->left->color = BLACK;
rbtree_right_rotate(T, x->parent);
x = T->root;
}
}
}
//如果是替代节点是红色,直接染黑
x->color = BLACK;
}
红黑树的删除代码
PRBTREE_NODE rbtree_delete( PRBTREE T,PRBTREE_NODE node)
{
PRBTREE_NODE y = T->nil;
PRBTREE_NODE x = T->nil;
if ((node->left==T->nil)||(node->right==T->nil))
{
// 要删除的node 为15
// 15 15 y
// / 或 \
//14 16 x
//这种就是再删除有孩子节点的节点
y = node;
}else
{
//要删除的node为15
// 15 y
// / \
// 14 16 x
//它的左右节点都不为空
//要找到它的后驱节点
y = rbtree_successor(T, node);
}
//如果它的孩子不为空,需要用孩子来替换它,这里就是再获取它的孩子节点
if (y->left!=T->nil)
{
x = y->left;
}else if(y->right!=T->nil)
{
x = y->right;
}
//3.4节点可以直接删除
//设置孩子节点的父亲为被删除节点的父亲
x->parent = y->parent;
if (y->parent==T->nil)
{
//如果是根节点的情况
//三节点
// 15
//四节点
// 16
// /
// 14
T->root = x;
}else if(y==y->parent->left)
{
//如果是那种拐弯的树
//类似这种情况
// 4 4
// / \ 删除4的时候,y=5 / \
// 2 6 2 6
// / \ / \ / \ / \
// 1 3 5 8 删除后 1 3 nil 8
// / \ / \
// 7 10 7 10
// / \ / \
// 9 11 9 11
y->parent->left = x;
}else
{
y->parent->right = x;
}
if (y!=node)
{
node->key = y->key;
node->value = y->value;
}
//这里红色节点并不需要去处理,需要处理的仅有删除的节点的颜色为黑色的时候。
if (y->color==BLACK)
{
rbtree_delete_fixup(T, x);
}
}
推荐一个零声学院免费教程,个人觉得老师讲得不错,
分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,
fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,
TCP/IP,协程,DPDK等技术内容,点击立即学习:
服务器
音视频
dpdk
Linux内核