最近闲来无事,一直没有研究过红黑树,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内核

C++红黑树的实现的更多相关文章

  1. 红黑树——算法导论(15)

    1. 什么是红黑树 (1) 简介     上一篇我们介绍了基本动态集合操作时间复杂度均为O(h)的二叉搜索树.但遗憾的是,只有当二叉搜索树高度较低时,这些集合操作才会较快:即当树的高度较高(甚至一种极 ...

  2. jdk源码分析红黑树——插入篇

    红黑树是自平衡的排序树,自平衡的优点是减少遍历的节点,所以效率会高.如果是非平衡的二叉树,当顺序或逆序插入的时候,查找动作很可能会遍历n个节点 红黑树的规则很容易理解,但是维护这个规则难. 一.规则 ...

  3. 谈c++ pb_ds库(二) 红黑树大法好

    厉害了,没想到翻翻pb_ds库看到这么多好东西,封装好的.现成的splay.红黑树.avl... 即使不能在考场上使用也可以用来对拍哦 声明/头文件 #include <ext/pb_ds/tr ...

  4. 定时器管理:nginx的红黑树和libevent的堆

    libevent 发生超时后, while循环一次从堆顶del timer——直到最新调整的最小堆顶不是超时事件为止,(实际是del event),但是会稍后把这个timeout的 event放到ac ...

  5. 从2-3-4树到红黑树(下) Java与C的实现

    欢迎探讨,如有错误敬请指正 如需转载,请注明出处   http://www.cnblogs.com/nullzx/ 相关博客: 从2-3-4树到红黑树(上) 从2-3-4树到红黑树(中) 1. 实现技 ...

  6. 红黑树/B+树/AVL树

    RB Tree 红黑树  :http://blog.csdn.net/very_2/article/details/5722682 Nginx的RBTree实现   :http://blog.csdn ...

  7. 论AVL树与红黑树

    首先讲解一下AVL树: 例如,我们要输入这样一串数字,10,9,8,7,15,20这样一串数字来建立AVL树 1,首先输入10,得到一个根结点10 2,然后输入9, 得到10这个根结点一个左孩子结点9 ...

  8. DataStructure——红黑树学习笔记

    1.前言 本文伪码和解释参考: http://blog.csdn.net/v_JULY_v/article/details/6105630 C实现的源码本文未贴出,请见: http://blog.cs ...

  9. 红黑树(Red-Black tree)

    红黑树又称红-黑二叉树,它首先是一颗二叉树,它具体二叉树所有的特性.同时红黑树更是一颗自平衡的排序二叉树.我们知道一颗基本的二叉树他们都需要满足一个基本性质–即树中的任何节点的值大于它的左子节点,且小 ...

  10. map,hash_map, hash_table, 红黑树 的原理和使用

    在刷算法题的时候总是碰到好多题,号称可以用hash table来解题.然后就蒙圈了. 1.首先,map和hash_map的区别和使用: (1)map底层用红黑树实现,hash_map底层用hash_t ...

随机推荐

  1. Python 潮流周刊第 38 期(摘要)+赠书5本

    本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章.教程.开源项目.软件工具.播客和视频.热门话题等内容.愿景:帮助所有读者精进 Python 技术,并增长职 ...

  2. .NET 云原生架构师训练营(模块二 基础巩固 引入)--学习笔记

    2.1 引入 http协议 web server && web application framework .net 与 .net core asp .net core web api ...

  3. Linux-Shell变量的算术运算

    一.算术运算符 1.+.- 求和. a+b   a-b 2. *./.% 求乘积,商,余数    a*b   a/b    a/b 3.** 幂运算,例如 3**3 是求 3 的立方,即 27 4. ...

  4. Pandas—read_csv()/read_table()文本文件的读取

    对于CSV及txt后缀的文本文件,分别使用pandas模块中的read_csv函数和read_table函数 文件类型 函数名称 CSV read_csv() txt read_table() 1. ...

  5. 使用SpeechRecognition进行语音识别

    操作系统 : CentOS7.7.1908_x64 gcc版本 :4.8.5 Python 版本 : 3.6.8 安装语音识别环境: virtualenv -p /usr/bin/python3 py ...

  6. Python 中获取文件名

    Python 获取文件名import osimport sys # ①获取当前文件名os.path.basename(__file__)# ②获取程序启动文件名os.path.basename(sys ...

  7. col命令

    col命令 在很多UNIX说明文件里,都有RLF控制字符,当我们把说明文件的内容输出成纯文本文件时,控制字符会变成乱码,col命令则能有效滤除这些控制字符. 语法 col [options] 参数 - ...

  8. 单例模式五种实现方式以及在JDK中的体现

    单例模式五种实现方式以及在JDK中的体现 一.五种实现方式 1.饿汉式 构造私有 提供一个静态私有的成员常量,类型就是单例类型,值是用私有构造创造出来的唯一实例 提供公共的静态方法获取上述的静态成员常 ...

  9. sql题目---day39

    # 1.查询所有的课程的名称以及对应的任课老师姓名 #where select teacher.tname,course.cname from teacher,course where course. ...

  10. django学习第一天---MVC和MTV框架,request对象的属性,url路由系统

    jinja2模板渲染简单使用 下载安装 pip install jinja2 使用示例 html文件中写法 <!DOCTYPE html> <html lang="zh-C ...