1、概述

RB-tree(红黑树) 是除了 AVL-tree 之外,另一个被广泛使用的平衡二叉搜索树。所谓 RB-tree,不仅是一个二叉搜索树,而且必须满足以下规则:
1、每个节点不是红色就是黑色。
2、根节点为黑色。
3、如果节点为红,其子节点必须为黑。
4、任一节点至NULL(树尾端)的任何路径,所含之黑节点数必须相同。

根据规则4,新增节点必须为红;根据规则3,新增节点之父节点必须为黑。当新节点根据二叉搜索树的规则达到其插入点,却未能符合上述条件时就必须调整颜色并旋转树形

2、插入节点操作前述

假设我们在上图所示的 RB-tree 中分别插入 3,8,35,75,根据二叉搜索树的规则,这四个新节点的落脚处应该如下图所示:
他们都破坏了 RB-tree 的规则,因此我们必须调整树形,也就是旋转树形并改变节点颜色。

为了方便讨论,这里整理一些代名词。
X:新节点,P:父节点,G:祖父节点,S:伯父节点,GG:曾祖父节点

根据二叉搜索树的规则,新节点 X 必为叶节点。根据红黑树的规则 4,X 必为红。
若此时 P 亦为红(这就违反了规则 3,必须调整树形),则 G 必为黑(因为原为 RB-tree,必须遵循规则3,隐含父子节点不不得同时为红)。于是,根据 X 的插入位置及外围节点(S 和 GG)的颜色,有了以下四种情况:

情况1:S 为黑且 X 为外侧插入。我们先对 P,G 做一次单旋转,并更改 P,G 颜色,即可重新满足红黑树的规则 3。

情况2:S 为黑且 X 为内侧插入。我们必须先对 P,X 做一次单旋转,并更改 G,X 颜色,再将结果对 G 做一次单旋转,即可满足红黑树规则。如下图:


情况3:S 为红, X 为外侧插入且 GG 为黑。先对 P 和 G 做一次单旋转,并改变 X 的颜色。


情况4:S 为红, X 为外侧插入且 GG 为红。先执行情况 3 中的步骤,因为改变了颜色,GG 亦为红,还要持续网上做,直到不再有父子节点同时为红的情况。

3、由上而下的程序

为了避免情况 4 “父子节点皆为红色” 的情况持续向 RB-tree 的上层结构发展,形成处理时效上的瓶颈,我们可以施行一个由上而下的程序:此程序的目的是通过提前的修改,把情况 4 给规避掉。假设新增节点为 A,那么就延着 A 的路径,只要看到有某节点 X 的两个子节点皆为红色,就把 X 改为红色,并把两个子节点改为黑色。如下图所示:

改掉之后,如果 X 的父节点 P 亦是红色(注意,此时 S 绝不可能为红),就得像情况 1 一样做一次单旋转并改变颜色,或是像情况2 一样做一次双旋转并改变颜色。

在此之后,节点 35 的插入就很单纯了:要么直接插入,要么插入后在做一次单或者双旋转就可以了。

4、RB-tree 的节点设计

RB-tree 有红黑二色,并且拥有左右子节点。由于 RB-tree 的各种操作时常需要上溯其父节点,所以特别在数据结构中安排了一个 parent 指针。为了有更大的弹性,节点分为两层。如下所示:

typedef bool __rb_tree_color_type;
const __rb_tree_color_type  __rb_tree_red = false; //红色为 0
const __rb_tree_color_type  __rb_tree_black = true;    //黑色为 1struct __rb_tree_node_base
{typedef __rb_tree_color_type   color_type;typedef __rb_tree_node_base* base_ptr;color_type color;      //节点颜色,非红即黑base_ptr  parent;     //RB树的许多操作,必须知道父节点base_ptr   left;       //指向左节点base_ptr right;      //指向右节点
};template<class Value>
struct __rb_tree_node : public __rb_tree_node_base
{typedef __rb_tree_node<Value>*   link_type;Value value_field;        //节点值
};

5、RB-tree 的迭代器

RB-tree 因为是双层节点结构,便有了双层迭代器。其中主要的意义是:__rb_tree_node 继承自 __rb_tree_node_base,__rb_tree_iterator 继承自 __rb_tree_base_iterator。如下图所示:

RB-tree 迭代器属于双向迭代器,但不具备随机定位能力,较为特殊的是其前进和后退操作。前进操作 operator++() 调用了基层迭代器的 increment(),后退操作 operator–() 则调用了基层迭代器的 decrement()。前进和后退的举止行为完全依据二叉搜索树的节点排列法则,在加上实现上的某些特殊技巧。

//基层迭代器
struct __rb_tree_base_iterator
{typedef __rb_tree_node_base::base_ptr  base_ptr;typedef bidirectional_iterator_tag iterator_categorty; //双向迭代器typedef ptrdiff_t    difference_type;base_ptr    node;   //它用来与容器之间产生一个连接关系//以下是前向的操作,相当于查找下一个比当前值大的数void increment(){if ( node->right != 0 )             //如果有右子节点,就向右走{node = node->right;       while ( node->left != 0 )       //然后一直往左子树走到底,便是答案node = node->left;     }else                               //没有右子节点{base_ptr y = node->parent;     //找出父节点while( node == y->right )       //如果现行节点本身是个右子节点{                               //则需要一直上溯,直到 “不为右子节点” 为止node = y;y = y->parent;}if( node->right != y)           //若此时的右子节点不等于此时的父节点,此时的父节点即为解答node = y;}//注意,以上最后的判断,是为了应付一种特殊情况:我们欲寻找根节点的下一节点,而恰巧根节点无右子节点//当然,以上特殊做法必须配合 RB-tree 根节点与特殊之 hearder 之间的特殊关系}//以下是向后的操作,相当于找下一个比自己小的数void decrement(){//如果node是红节点,且父节点的父节点等于自己,右子节点即为解答//这种情况发生于node 为 header 时if( node->color == __rb_tree_red && node->parent->parent == node )node = node->right;else if( node->left != 0 )       //如果有左子节点{base_ptr y = node->left;  //令y指向左子节点while( y->right != 0 )        //当y有右子节点时,一直往右走到底就是y = y->right;node = y;}else                         //即非根节点,亦无左子节点{base_ptr y = node->parent;    //找出父节点while( node == y->left )    //当前节点为左子节点,一直向上走,直到不为左子节点{node = y;y = y->parent;}node = y;                    //此时的节点就是答案}}
};//RB-tree 的正规迭代器
template<class Value, class Ref, class Ptr>
struct __rb_tree_iterator : public __rb_tree_base_iterator
{
typedef Value   value_type;
typedef Ref     reference;
typedef Ptr     pointer;
typedef __rb_tree_iterator<Value, Ref, Ptr> self;
......
self& operator++()
{increment();return *this;
}
self operator++(int)
{self tmp = *this;increment();return tmp;
}
self& operator--()
{decrement();return *this;
}
self operator--(int)
{self tmp = *this;decrement();return tmp;
}
};

在__rb_tree_base_iterator 的 increment() 和 decrement() 两函数中,有两处令人费解的情况,原因如下图所示:


在根节点上会多一个节点,此节点的左子节点是整棵树的最小数,此节点的右子节点是整棵树的最大数,此节点的父节点是根节点

6、RB-tree 的数据结构

这里主要介绍一下 RB-tree 的关键点 header。我们知道,树状结构的各种操作,最需注意的就是边界情况的发生,也就是走到根节点时要有特殊的处理。为了简化处理,SGI STL 特别为根节点在设计一个父节点,名为 header,并令初始状态为下图所示:


代码如下:

template<......>
class tb_tree
{
typedef rb_tree_node*   link_type;
typedef size_t          size_type;
......
protected:size_type node_count;     //追踪记录树的大小link_type header;         //这是实现上的一个技巧Compare     key_compare;    //节点间的键值比较准则,是个仿函数对象protected://RB 树初始化时,会对 header 节点进行初始化void init(){header = get_node();           //产生一个节点空间,令header指向它color(header) = __rb_tree_red; //令header 为红色,用来区分 header 和 rootroot ( ) = 0;leftmost() = header;rightmost() = header;    //令 header 的左右子节点为自己}//以下三个函数用来方便取得 header 的成员link_type& root() const { return (link_type&) header->parent; }link_type& leftmost() const { return (link_type&) header->left; }link_type& rightmost() const { return (link_type&) header->right; }iterator begin() { return leftmost(); }   //RB 树的起头为最左节点处iterator end() { return header; }        //RB 树的终点为 header 所指处Compare key_comp() const { return key_compare; }
};

接下来,每当插入新节点时,不但要依照 RB-tree 的规则来调整,并且维护 header 的正确性,使其父节点指向根节点,左子节点指向最小节点,右子节点指向最大节点。

7、RB-tree的元素操作

这里主要描述 RB-tree 的插入操作。RB-tree 有两种插入操作:insert_unique() 和 insert_equal() ,前者表示被插入节点的键值在整棵树中必须独一无二,后者表示被插入节点的键值在整棵树中可以重复,因此,无论如何插入都会成功。
下面会用到一个仿函数 KeyOfValue,意思是:通过实值取出键值。

7.1、insert_equal()

插入新值;节点键值允许重复,返回值是一个 RB-tree 迭代器,指向新增节点

template<class Key, class Value, class KeyOfValue, class Compare, class Alloc>
typename rb_tree<......>::iterator insert_equal(const Value& v)
{link_type y = header;link_type x = root();   //从根节点开始while( x != 0) //从根节点开始,往下寻找适当的插入点{y = x;x = key_compare( KeyOfValue()(v), key(x)) ? left(x) : right(x);//遇 “大” 则往左,遇 “小于或等于” 则往右}return __insert( x, y, v);
}

7.2、insert_unique()

插入新值:节点键值不允许重复,若重复则插入无效。注意,返回值是个pair,第一元素是个 RB-tree 迭代器,指向新增节点,第二元素表示插入成功与否

template<.......>
pari<typename rb_tree<...>::iterator, bool > insert_unique(cosnt Value& v)
{link_type y = header;link_type x = root();   //从根节点开始bool comp = true;while( x != 0 ){y = x;comp = key_compare( KeyOfValue()(v), key(v));    //v键值小于目前节点之键值x = comp ? left(x) : right(x);   //遇 “大” 往左,遇 “小于或等于” 则往右}iterator j = iterator(y);  //令迭代器 j 指向插入点之父节点 yif( comp )      //如果离开 while 循环时 comp 为真{if( j == begin() )   //插入点之父节点为最左节点return pari<iterator,bool>(__insert(x,y,v),true);else       //否则,调整 j,回头准备测试--j;}//新键值不与既有节点之键值重复,执行以下安插操作if( key_compare(key(j.node), KeyOfValue()(v)))return pair<iterator,bool>(__insert(x,y,v),true);//进行至此,表示新值一定与书中键值重复,那么就不该插入新值return pari<iterator,bool>(j,false);
}

7.3、__insert()

真正的插入执行操作

template<......>
typename rb_tree<...>::iterator __insert(base_ptr x_, base_ptr y_, const Value& v)
{//参数 x_ 为新值插入点,参数 y_ 为插入点之父节点,参数 v 为新值link_type x = (link_type) x_;link_type y = (link_type) y_;link_type z;// key_compare 是键值大小比较准则if( y == header || x != 0 || key_compare(KeyOfValue()(v), key(y))){z = create_node(v); //产生一个新节点left(y) = z;  //这使得当 y 即为 header 时,leftmost() = zif( y == header ){root() = z;rightmost() = z;}else if( y == leftmost() )   //如果 y 为最左节点leftmost() = z;            //维护 leftmost(),使它永远指向最左节点}else{z = create_node(v);         //产生一个新节点right(y) = z;             //令新节点成为插入点之父节点 y 的右子节点if( y == rightmost())rightmost() = z;     //维护 rightmost(),使它永远指向最右节点}parent(z) = y;                 //设定新节点left(z) = 0;                    right(z) = 0;__rb_tree_rebalance( z, header->parent);   //参数一为新增加点,参数二为root++node_count;                               //节点数累加return iterator(z);                          //返回一个迭代器,指向新增节点
}

任何插入操作完成之后,都要做一次调整操作(旋转及改变颜色),将树的状态调整到符合 RB-tree 的要求。__rb_tree_rebalance()具备如此能力的一个全局函数:

inline void __rb_tree_rebalance(__rb_tree_node_base* x, __rb_tree_node_base*& root)
{x->color = __rb_tree_red;  //新节点必须为红节点while( x != root && x->parent->color == __rb_tree_red ) //父节点为红节点{if(x->parent == x->parent->parent->left)    //父节点为祖父节点的左节点{__rb_tree_node_base* y = x->parent->parent->right; //令 y 为伯父节点if( y && y->color == __rb_tree_red)     //伯父节点存在,且为红{x->parent->color = __rb_tree_black;          //更改父节点为黑y->color = __rb_tree_black;                    //更改伯父节点为黑x->parent->parent->color = __rb_tree_red;   //更改祖父节点为黑x = x->parent->parent;}else    //无伯父节点,或伯父节点为黑{if( x == x->parent->right )                          //如果新节点为父节点的右子节点{x = x->parent;__rb_tree_rotate_left( x, root);             //第一参数为左旋点}x->parent->color = __rb_tree_black;                   //改变颜色x->parent->parent->color = __rb_tree_red;__rb_tree_rotate_right(x->parent->parent, root); //第一参数为右旋点}}else    //父节点为祖父节点的右子节点{    __rb_tree_node_base* y = x->parent->parent->left; //令 y 为伯父节点if( y && y->color == __rb_tree_red)             //有伯父节点且为红{x->parent->color = __rb_tree_black;           //更改父节点为黑y->color = __rb_tree_black;                    //更改伯父节点为黑x->parent->parent->color = __rb_tree_red;   //更改祖父节点为红x = x->parent->parent;                     //准备继续往上层检查}else                        //无伯父节点,或伯父节点为黑{if( x == x->parent->left )                       //如果新节点为父节点的左子节点{x = x->parent;__rb_tree_rotate_right( x, root);            //第一参数为右旋转}x->parent->color = __rb_tree_black;               //改变颜色x->parent->parent->color = __rb_tree_red;__rb_tree_rotate_left(x->parent->parent, root);  //第一参数为左旋点}}}root->color = __rb_tree_black;     //根节点永远为黑色
}

这个树形调整操作,就是前面所说的那个 “由上而下的程序”。 某些时候只需调整节点颜色,某些时候要做单旋转,某些时候要做双旋转。某些时候要左旋,某些时候要右旋,下面是左旋和右旋的全局函数。

inline void __rb_tree_rotate_left(__rb_tree_node_base* x, __rb_tree_node_base*& root)
{//x 为旋转点,令 y 为旋转点的右子节点__rb_tree_node_base* y = x->right;x->right = y->left;if( y->left != 0 )y->left->parent = x; //别忘了回头设定父节点y->parent = x->parent;//令 y 完全顶替 x 的地位if( x == root )                      // x 为根节点root = y;else if( x == x->parent->left )      // x 为其父节点的左子节点x->parent->left = y;else                              // x 为其父节点的右子节点x->parent->right = y;y->left = x;x->parent = y;
}inline void __rb_tree_rotate_right(__rb_tree_node_base* x, __rb_tree_node_base*& root)
{// x 为旋转点,y 为旋转点的左子节点__rb_tree_node_base* y = x->left;x->left = y->right;if( y->right != 0 )y->right->parent = x; //回头设定父节点y->parent = x->parent;//令 y 完全顶替 x 的地位if( x == root )                  // x 为根节点root = y;else if( x == x->parent->right)      // x 为其父节点的右子节点x->parent->right = y;else                             // x 为其父节点的左子节点x->parent->left = y;y->right = x;x->parent = y;
}

上面说的单旋转,就是调用一次__rb_tree_rotate_left() 或 __rb_tree_rotate_right()
双旋转,就是调用两次__rb_tree_rotate_left() 或 __rb_tree_rotate_right(),一般来说两次单旋转不一样。

8、元素的搜索

RB-tree 是一个二叉搜索树,元素搜索是非常快的。代码如下:

template<......>
typename rb_tree<...>::iterator find(const Key& k)
{link_type y = header;link_type x = root();while( x != 0 ){if( !key_compare(key(x), k))y = x, x = left(x);elsex = rifht(x);}iterator j = iterator(y);return (j == end() || key_compare(k, key(j.node))) ? end() : j;
}

好了就整理到这了,RB-tree 的理论,我是能看懂,代码勉强也能看懂,只能照书copy了,等技能在深入了,在回头看看,看看能不能整理的好一点。

感谢大家,我是假装很努力的YoungYangD(小羊)

参考资料:
《STL源码剖析》

C++ STL(第十三篇:RB-tree)相关推荐

  1. STL学习——RB-tree篇

    STL学习--RB-tree篇 简介 RB-tree(红黑树)是一棵平衡二叉搜索树,它需要满足以下规则: 1)每个节点不是红色就是黑色: 2)根节点为黑色: 3)如果节点为红,其子节点必须为黑: 4) ...

  2. 《孙子兵法》十三篇注译(15--结束语)

      结束语 (读<孙子兵法>,悟管理.营销之道) 中国自古为思想精英荟萃之地,也是的兵学昌盛之国,素有"兵法之国"之称.产生于2500年前的不朽名著<孙子兵法&g ...

  3. python学习[第十三篇] 条件和循环

    python学习[第十三篇] 条件和循环 if语句 单一if 语句 if语句有三个部分构成,关键字if本身,判断结果真假的条件表达式,以及表达式为真或非0是执行的代码 if expression: e ...

  4. CCIE-LAB-第十三篇-DHCP+HSRPV2+Track+DHCP Realy

    CCIE-LAB-第十三篇-DHCP+HSRPV2+Track+DHCP Realy 实际中,思科只会给你5个小时去做下面的全部配置 这个是CCIE-LAB的拓扑图 问题 这次的相对来说有点多 翻译: ...

  5. CCNA-第十三篇-NAT-上

    CCNA-第十三篇-NAT-上 NAT- netword address translation 网络地址转换 NAT不仅仅是用于共享地址上网,NAT是一个很大的东西 核心思想是转换地址,以及端口号 ...

  6. 第四十三篇 面向对象进阶2

    目录 第四十三篇 面向对象进阶2 一.类的封装 1.封装分为两个层面 2.应用 二.类的property特性 1. BMI(Body Mass Index):身体体质指数 2.装饰器用法(只在Pyth ...

  7. NHibernate 操作视图 第十三篇

    NHibernate 操作视图 第十三篇 在NHibernate中,可以把视图当表一样操作,只需要记住一点就是,视图是只读的,因此映射实体的setter应该改为protected. 新建一个视图如下: ...

  8. 刷题日记【第十三篇】-笔试必刷题【数根+星际密码+跳台阶扩展问题+快到碗里来】

    刷题日记[第十三篇]-笔试必刷题[数根+星际密码+跳台阶扩展问题+快到碗里来] 1.方法区在JVM中也是一个非常重要的区域,它与堆一样,是被[线程共享]的区域. 下面有关JVM内存,说法错误的是?(c ...

  9. 九宫怎么排列和使用_广告视频配音剪映零基础入门教程第二十三篇:剪辑之九宫格教程...

    朋友圈最火九宫格视频你们知道是怎样制作的吗?我们常常在玩朋友圈的时候想用九宫格照片,但是你们有没有遇到这种情况,想玩九宫格却发现找不到那么多能用的照片,那这时候怎么办呢?玩腻了平常图片的发法,今天我们 ...

  10. STM32F429第二十三篇之电容按键

    文章目录 前言 硬件分析 原理 源程序 主函数 TpadInit GetTimeUntoched GetTimeCharge TpadScan 前言 本文主要介绍电容按键的原理与使用方法,主要使用的A ...

最新文章

  1. hadoop错误之ClassNotFoundException
  2. C++用法的学习心得
  3. Java 定时任务调度(8)--ElasticJob 入门实战(ElasticJob-Lite使用)
  4. 广义动量定理之速度V的应用分析
  5. go中defer的一个隐藏功能
  6. 最简单的排序算法(C和C++实现)
  7. 推荐一个GitHub项目!docker视频教程下载
  8. php工程师绩效考核表_如何对程序员绩效考核?
  9. 11210怎么等于24_巧算24点
  10. Jetson AGX Xavier 固态硬盘安装并挂载到/home与无线模块安装
  11. MySQL数据库插入中文时出现Incorrect string value: '\xE6\x97\xB7\xE5\x85\xA8' for column 'sz_name' at row 1...
  12. 面向开发的内存调试神器,如何使用ASAN检测内存泄漏、堆栈溢出等问题
  13. 怎么从H5广告页内复制微信号直接调起微信客户端添加好友
  14. 浅谈幼儿园计算机论文,浅谈幼儿园科学教育活动中,计算机的辅助教学
  15. linux飞腾cpu,基于飞腾CPU+银河麒麟操作系统编译安装tensorflow
  16. 计算机价格谈判议程,价格谈判的几个小技巧
  17. WPF教程(五) XAML是什么?
  18. python中sn的意思_Python中random模块生成随机数详解
  19. 设计数据密集型应用——数据系统的未来
  20. 同时用公司内网和手机热点上网(真的有用!)

热门文章

  1. Python:实现费马检测算法(附完整源码)
  2. VUE通过自定义指令,只允许输入大写英文以及数字
  3. 根据脸部毛孔生长方向去护肤
  4. 一个简体/繁体字在线转换工具源码
  5. C语言-小写转换大写
  6. 牛逼,手机居然可以无线投屏到笔记本电脑!
  7. 笔记本分屏后鼠标卡顿问题解决
  8. html flappybird小游戏代码,原生js实现Flappy Bird小游戏
  9. oneplus 驱动_450美元的旗舰旗舰产品OnePlus Nord动手实践
  10. 关于巴伦——Marchand巴伦