数据结构树之红黑树

///

红黑树简介:

  红黑树是一棵二叉搜索树,它在每个结点上增加了一个存储位来表示结点的颜色,可以是RED 或 BLACK。通过对任何一条根到叶子的简单路径上各个结点的颜色进行约束,红黑树确保没有一条路径回避其他路径长处2倍,因而是近似平衡的。

  树的每个结点包含 5 个属性:color,key,left,right和p。如果一个结点没有子结点或者父结点,则该结点相应的指针属性的值为NULL。我们可以把这些NULL视为指向二叉搜索树叶结点的指针,而把带关键字的结点视为树的内部结点。

红黑树的性质:

  一棵红黑树是满足下面红黑性质的二叉搜索树:

  1.每个结点或是红色的,或是黑色的

  2.根节点是黑色的

  3.每个叶结点(NULL)是黑色的

  4.如果一个结点是红色的,那么他的两个子结点都是黑色的

  5.对于每个结点,从该结点到其所有后代叶结点的简单路径上,包含相同数目的黑色结点

  这 5 个性质中1,2,4都比较好理解。3与我们常说的(大部分数据结构书上说的)叶结点有一点点区别,如下图:

  

那性质5又是什么意思呢?我们再来看一个图:

  

  由红黑树的 5 个性质可知,上幅图中左图是红黑树,而右图非红黑树。右图中满足红黑树的性质1.2.3.4,但是不满足性质5:从根节点6(不包括根节点)到各叶结点的简单路径上的黑色黑色结点个数并不相等。例如:6-1有2个,而6-8和6-10都是有三个。

  这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。

要知道为什么这些特性确保了这个结果,注意到属性4导致了路径不能有两个毗连的红色节点就足够了。最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。因为根据属性5所有最长的路径都有相同数目的黑色节点,这就表明了没有路径能多于任何其他路径的两倍长。

在很多树数据结构的表示中,一个节点有可能只有一个子节点,而叶子节点包含数据。用这种范例表示红黑树是可能的,但是这会改变一些属性并使算法复杂。为此,本文中我们使用 "nil 叶子" 或"空(null)叶子",如上图所示,它不包含数据而只充当树在此结束的指示。这些节点在绘图中经常被省略,导致了这些树好像同上述原则相矛盾,而实际上不是这样。与此有关的结论是所有节点都有两个子节点,尽管其中的一个或两个可能是空叶子。

红黑树的操作:

  因为每一个红黑树也是一个特化的二叉查找树,因此红黑树上的只读操作与普通二叉查找树上的只读操作相同。然而,在红黑树上进行插入操作和删除操作会导致不再符合红黑树的性质。恢复红黑树的属性需要少量(O(log n))的颜色变更(实际是非常快速的)和不超过三次树旋转(对于插入操作是两次)。虽然插入和删除很复杂,但操作时间仍可以保持为 O(log n) 次。我们在这只讲讲红黑树的插入和删除。

  1.插入

  下面看看算法导论中给的伪代码:

复制代码

 1 /*
 2 注意以下的T.nil,是一个与普通红黑树结点相同的对象。他的color是BLACK,他也是根节点的父节点
 3     RB-INSERT(T,z)                  //向树T中增加结点z
 4     y = T.nil                      //根节点的父节点
 5     x = T.root                      //根节点
 6     while x != T.nil                //while循环内是为了寻找插入结点z的位置
 7         y = x                       //y始终是x的父节点
 8         if z.key < x.key
 9             x = x.left
10         else 
11             x = x.right
12     //跳出while循环之后,说明y结点的某个孩子是T.nil了,可以插入了!
13     z.p = y                        //z的父结点是y
14     if y == T.nil                  //如果y就是 T.nil说明该树为空,插入z后,z就是根节点
15         T.root = z
16     else if z.key < y.key          //如果z比y结点值小,则插到y的左孩子上
17         y.left = z
18     else 
19         y.right = z                //否则插到y的右孩子上
20     z.left = T.nil
21     z.right = T.nil                //将z的左右孩子都设为T.nil
22     z.color    = RED               //z的颜色设为红色
23     RB-INSERT-FIXUP(T,Z)           //插入一个红色结点会破坏红黑树的性质,需要调整
24  */

复制代码

  比如我们插入一个值为3的结点:在RB-INSERT-FIXUP函数执行之前,执行的结果如下图:

   由上图可以看出T.nil的作用是充当一个哨兵,它也是一个红黑树结点对象,且颜色为黑色,其他的值任意!插入3,并将3的颜色涂成红色之后,有可能会破坏红黑树的性质2和4(上图就破坏了性质5).所以我们要调用RB-INSERT-FIXUP来保持红黑树的性质。RB-INSERT-FIXUP的伪代码如下:

  

复制代码

 1 /*
 2     以下是实现RB-INSERT-FIXUP(T,Z)伪代码
 3     while z.p.color == RED                //因为z本身是红色,如果他的父结点是红色那这个循环就要继续---调节树
 4         if z.p == z.p.p.left              //如果z的父亲是z祖父的左孩子
 5             y = z.p.p.right               //令y为z祖父的右孩子,也就是说y是z的叔叔
 6             if y.color == RED             //如果y的颜色是红色
 7                 z.p.color = BLACK         //case 1    既然z是红色,为了不破坏性质4,将z的父节点涂成黑色
 8                 y.color   = BLACK         //case 1    同时也要讲z的叔叔结点涂成黑色
 9                 z.p.p.color=RED           //case 1    同时将z的祖父结点(y的父节点)涂成红色
10                 z = z.p.p                 //case 1    令z 等于 z的祖父,循环继续
11             else if z == z.p.right        //如果z是父结点的右孩子
12                 z = z.p                   //case 2    z等于z的父结点
13                 LEFT-ROTATE(T,Z)          //case 2    右旋
14             z.p.color = BLACK             //case 3    将z的父结点颜色涂成黑色
15             z.p.p.color = RED             //case 3    将z的祖父结点涂成红色
16             RIGHT-ROTATE(T,Z.P.P)         //case 3    右旋
17         else(same as then clause with 'right' and 'left' exchanged)
18     T.root.color = BLACK
19  */

复制代码

  这里伪代码里面有两个函数要注意下,LEFT-ROTATE() 和 RIGHT-ROTATE().这个分别是左旋和右旋的函数。

  

下面是左旋的伪代码:

 1 /*
 2 LEFT-ROTATE(T,x)--参考上图
 3     y = x.right            //给y赋值
 4     x.right = y.left                //将x的右结点指向y的左结点
 5     if y.left != T.nil        
 6         y.left.p = x        //设置y左结点的父节点为x
 7     y.p = x.p                //y的父结点是x的父节点
 8     if x.p == T.nil            //如果 x 是根节点
 9         T.root = y;
10     elseif x == x.p.left    //如果x是父结点的左孩子
11         x.p.left = y;        //
12     else x.p.right = y        //如果x是父结点的右孩子
13     y.left = x;                //y的左孩子是x
14     x.p = y                    //x的父节点是y
15 */    

  RB-INSERT-FIXUP要处理的情况有三种。

  a).情况一:插入结点后的结点z。z和父结点都是红色,违反性质4.如下图:

   解决方法是:将z的父结点和叔叔结点涂成黑色,并且z的指针沿z树上升(对应RB-INSERT-FIXUP代码中的case 1部分)。所得情况如下图


 

   b).情况二:调整后的结点z(此时是7)和父结点(结点2)都是红色,但是叔叔结点(结点1)是黑色,此时出现情况二。解决方法:将2作为根节点T进行左旋。得到如下图:

  c).情况三:调整后的结点z(此时是2)和父结点是红色,但是叔叔结点(8)是黑色。要进行如下操作:将z结点的父结点涂成黑色,将z的祖父结点涂成红色。再以z的父结点为根T,作一次右旋转即可得到一棵合法的红黑树,如下图:

  此时的z的父节点不再是红色,退出while循环(如果不退出循环,情况肯定是这三种中的一种)。一棵合法的红黑树形成!

//

红黑树数据结构剖析

红黑树是计算机科学内比较常用的一种数据结构,它使得对数据的搜索,插入和删除操作都能保持在O(lgn)的时间复杂度。然而,相比于一般的数据结构,红黑树的实现的难度有所增加。网络上关于红黑树的实现资料汗牛充栋,但是乏于系统介绍红黑树实现的资料。本文通过一个自己实现的红黑树数据结构以及必要的搜索,插入和删除操作算法,为大家更系统地剖析红黑树数据结构的实现。

对于大部分数据结构,一般都会使用抽象数据类型的方式实现,C++提供的模板机制可以做到数据结构与具体数据类型无关,就像STL实现的那样。不过本文并非去实现STL中的红黑树,更重要的是透过红黑树的实现学习相关的算法和思想。当然,我们还是会借鉴STL中关于红黑树实现部分有价值内容。

一、基本概念

在具体实现红黑树之前,必须弄清它的基本含义。红黑树本质上是一颗二叉搜索树,它满足二叉搜索树的基本性质——即树中的任何节点的值大于它的左子节点,且小于它的右子节点。

图1 二叉搜索树

按照二叉搜索树组织数据,使得对元素的查找非常快捷。比如图1中的二叉搜索树,如果查询值为48的节点,只需要遍历4个节点即可完成。理论上,一颗平衡的二叉搜索树的任意节点平均查找效率为树的高度h,即O(lgn)。但是如果二叉搜索树的失去平衡(元素全在一侧),搜索效率就退化为O(n),因此二叉搜索树的平衡是搜索效率的关键所在。为了维护树的平衡性,数据结构内出现了各种各样的树,比如AVL树通过维持任何节点的左右子树的高度差不大于1保持树的平衡,而红黑树使用颜色的概念维持树的平衡,使二叉搜索树的左右子树的高度差保持在固定的范围。相比于其他二叉搜索树树,红黑树对二叉搜索树的平衡性维持有着自身的优势。

顾名思义,红黑树的节点是有颜色概念的,即非红即黑。通过颜色的约束,红黑树维持着二叉搜索树的平衡性。一颗红黑树必须满足以下几点条件:

规则1、根节点必须是黑色。

规则2、任意从根到叶子的路径不包含连续的红色节点。

规则3、任意从根到叶子的路径的黑色节点总数相同。

如图2所示,为一颗合法的红黑树,可以发现红黑树在维持二叉搜索树的基本性质的前提下,并满足了红黑树的颜色条件,整体上保持了二叉搜索树的平衡性。(构造如下红黑树的数据序列为:(50,35,78,27,56,90,45,40,48),读者可以自行验证。)

图2 红黑树

二、数据结构设计

和一般的数据结构设计类似,我们用抽象数据类型表示红黑树的节点,使用指针保存节点之间的相互关系。

作为红黑树节点,其基本属性有:节点的颜色、左子节点指针、右子节点指针、父节点指针、节点的值。

图3 红黑树节点基本属性

为了方便红黑树关键算法的实现,还定义了一些简单的操作(都是内联函数)。

//红黑树节点
template<class T>
class rb_tree_node
{
    typedef rb_tree_node_color node_color;
    typedef rb_tree_node<T> node_type;
public:
    node_color color;//颜色
    node_type*parent;//父节点
    node_type*left;//左子节点
    node_type*right;//右子节点
    T value;//值
    rb_tree_node(T&v);//构造函数
    inline node_type*brother();//获取兄弟节点
    inline bool on_left();//自身是左子节点
    inline bool on_right();//自身是右子节点
    inline void set_left(node_type*node);//设置左子节点
    inline void set_right(node_type*node);//设置左子节点
};

为了表示红黑树节点的颜色,我们定义一个简单的枚举类型。

//红黑树节点颜色
enum rb_tree_node_color
{
    red=false,
    black=true
};

有了节点,剩下的就是实现红黑树的构造、插入、搜索、删除等关键算法了。

//红黑树
template<class T>
class rb_tree
{
public:
    typedef rb_tree_node<T> node_type;
    rb_tree();
    ~rb_tree();
    void clear();
    void insert(T v);//添加节点
    bool insert_unique(T v);//添加唯一节点
    node_type* find(T v);//查询节点
    bool  remove(T v);//删除节点
    inline node_type* maximum();//最大值
    inline node_type* minimum();//最小值
    inline node_type* next(node_type*node);//下一个节点
    inline node_type* prev(node_type*node);//上一个节点
    void print();//输出
    int height();//高度
    unsigned count();//节点数
    bool validate();//验证
    unsigned get_rotate_times();//获取旋转次数
private:
    node_type*root;//树根
    unsigned rotate_times;//旋转的次数
    unsigned node_count;//节点数
    void __clear(node_type*sub_root);//清除函数
    void __insert(node_type*&sub_root,node_type*parent,node_type*node);//内部节点插入函数
    node_type* __find(node_type*sub_root,T v);//查询
    inline node_type* __maximum(node_type*sub_root);//最大值
    inline node_type* __minimum(node_type*sub_root);//最小值
    void __rebalance(node_type*node);//新插入节点调整平衡
    void __fix(node_type*node,node_type*parent,bool direct);//删除节点调整平衡
    void __rotate(node_type*node);//自动判断类型旋转
    void __rotate_left(node_type*node);//左旋转    
    void __rotate_right(node_type*node);//右旋转
    void __print(node_type*sub_root);//输出
    int  __height(node_type*&sub_root);//高度
    bool __validate(node_type*&sub_root,int& count);//验证红黑树的合法性
};

在红黑树类中,定义了树根(root)和节点数(count),其中还记录红黑树在插入删除操作时执行的旋转次数rotate_times。其中核心操作有插入操作(insert),搜索操作(find),删除操作(remove),递减操作(prev)——寻找比当前节点较小的节点,递增操作(next)——寻找比当前节点较大的节点,最大值(maximum)和最小值(minimum)操作等。其中验证操作(__ validate)通过递归操作红黑树,验证红黑树的三个基本颜色约束,用于操纵红黑树后验证红黑树是否保持平衡。

由于插入和删除操作是红黑树的关键所在,下边重点介绍这两个操作。其他的操作一般通过对树进行递归操作都可以轻松的完成,这里不再赘述。

三、红黑树的插入操作

红黑树的插入操作和查询操作有些类似,它按照二分搜索的方式递归寻找插入点。不过这里需要考虑边界条件——当树为空时需要特殊处理(这里未采用STL对树根节点实现的特殊技巧)。如果插入第一个节点,我们直接用树根记录这个节点,并设置为黑色,否则作递归查找插入(__insert操作)。

默认插入的节点颜色都是红色,因为插入黑色节点会破坏根路径上的黑色节点总数,但即使如此,也会出现连续红色节点的情况。因此在一般的插入操作之后,出现红黑树约束条件不满足的情况(称为失去平衡)时,就必须要根据当前的红黑树的情况做相应的调整(__rebalance操作)。和AVL树的平衡调整通过旋转操作的实现类似,红黑树的调整操作一般都是通过旋转结合节点的变色操作来完成的。

红黑树插入节点操作产生的不平衡来源于当前插入点和父节点的颜色冲突导致的(都是红色,违反规则2)。

图4 插入冲突

如图4所示,由于节点插入之前红黑树是平衡的,因此可以断定祖父节点g必存在(规则1:根节点必须是黑色),且是黑色(规则2:不会有连续的红色节点),而叔父节点u颜色不确定,因此可以把问题分为两大类:

1、叔父节点是黑色(若是空节点则默认为黑色)

这种情况下通过旋转和变色操作可以使红黑树恢复平衡。但是考虑当前节点n和父节点p的位置又分为四种情况:

A、n是p左子节点,p是g的左子节点。

B、n是p右子节点,p是g的右子节点。

C、n是p左子节点,p是g的右子节点。

D、n是p右子节点,p是g的左子节点。

情况A,B统一称为外侧插入,C,D统一称为内侧插入。之所以这样分类是因为同类的插入方式的解决方式是对称的,可以通过镜像的方法相似完成。

首先考虑情况A:n是p左子节点,p是g的左子节点。针对该情况可以通过一次右旋转操作,并将p设为黑色,g设为红色完成重新平衡。

图5 左外侧插入调整

右旋操作的步骤是:将p挂接在g节点原来的位置(如果g原是根节点,需要考虑边界条件),将p的右子树x挂到g的左子节点,再把g挂在p的右子节点上,完成右旋操作。这里将最终旋转结果的子树的根节点作为旋转轴(p节点),也就是说旋转轴在旋转结束后称为新子树的根节点!这里需要强调一下和STL的旋转操作的区别,STL的右旋操作的旋转轴视为旋转之前的子树根节点(g节点),不过这并不影响旋转操作的效果。

类比之下,情况B则需要使用左单旋操作来解决平衡问题,方法和情况A类似。

图6 右外侧插入

接下来,考虑情况C:n是p左子节点,p是g的右子节点。针对该情况通过一次左旋,一次右旋操作(旋转轴都是n,注意不是p),并将n设为黑色,g设为红色完成重新平衡。

图7 左内侧插入

需要注意的是,由于此时新插入的节点是n,它的左右子树x,y都是空节点,但即使如此,旋转操作的结果需要将x,y新的位置设置正确(如果不把p和g的对应分支设置为空节点的话,就会破坏树的结构)。在之后的其他操作中,待旋转的节点n的左右子树可能就不是空节点了。

类比之下,情况D则需要使用一次右单旋,一次左单旋操作来解决平衡问题,方法和情况C类似。

图8 右内侧插入

2、叔父节点是红色

当叔父节点是红色时,则不能直接通过上述方式处理了(把前边的所有情况的u节点看作红色,会发现节点u和g是红色冲突的)。但是我们可以交换g与p,u节点的颜色完成当前冲突的解决。

图9 叔父节点为红的插入

但是仅仅这样做颜色交换是不够的,因为祖父节点g的父节点(记作gp)如果也是红色的话仍然会有冲突(g和gp是连续的红色,违反规则2)。为了解决这样的冲突,我们需要从当前插入点n向根节点root回溯两次。

第一次回溯时处理所有拥有两个红色节点的节点,并按照图9中的方式交换父节点g与子节点p,u的颜色,并暂时忽略gp和p的颜色冲突。如果根节点的两个子节点也是这种情况,则在颜色交换完毕后重新将根节点设置为黑色。

第二次回溯专门处理连续的红色节点冲突。由于经过第一遍的处理,在新插入点n的路径上一定不存在同为红色的兄弟节点了。而仍出现gp和p的红色冲突时,gp的兄弟节点(gu)可以断定为黑色,这样就回归前边讨论的叔父节点为黑色时的情况处理。

图10 消除连续红色节点

由于发生冲突的两个红色节点位置可能是任意的,因此会出现上述的四种旋转情况。不过我们把靠近叶子的红色节点(g)看作新插入的节点,这样面对A,B情况则把p的父节点gp作为旋转轴,旋转后gp会是新子树的根,而面对C,D情况时把p作为旋转轴即可,旋转后p为新子树的根(因此可以把四种旋转方式封装起来)。

在第二次回溯时,虽然每次遇到红色冲突旋转后都会提升g和gp节点的位置(与根节点的距离减少),但是无论g和gp谁是新子树的根都不会影响新插入节点n到根节点root路径的回溯,而且一旦新子树的根到达根节点(parent指针为空)就可以停止回溯了。

通过以上的树重新平衡策略可以完美地解决红黑树插入节点的平衡问题。

四、红黑树的删除操作

相比于插入操作,红黑树的删除操作显得更加复杂。很多资料都没有将红黑树的删除解释清楚,清华的数据结构教材对红黑树删除的描述也十分混乱,《STL源码剖析》中侯sir对红黑树的删除更是闭口不谈。这里参考了STL对红黑树删除操作的实现方式,并做了适当的修改(红黑树使用哨兵节点表示空节点,而这里使用空指针的方式,因此要杜绝空指针的引用问题)。

由于红黑树就是二叉搜索树,因此节点的删除方式和二叉搜索树相同。不过红黑树删除操作的难点不在于节点的删除,而在于删除节点后的调整操作。因此红黑树的删除操作分为两步,首先确定被删除节点的位置,然后调整红黑树的平衡性。

先考虑删除节点的位置,如果待删除节点拥有唯一子节点或没有子节点,则将该节点删除,并将其子节点(或空节点)代替自身的位置。如果待删除节点有两个子节点,则不能将该节点直接删除。而是从其右子树中选取最小值节点(或左子树的最大值节点)作为删除节点(该节点一定没有两个子节点了,否则还能取更小的值)。当然在删除被选取的节点之前,需要将被选取的节点的数据拷贝到原本需要删除的节点中。选定删除节点位置的情况如图11所示,这和二叉搜索树的节点删除完全相同。

图11 删除点的选定

图11中用红色标记的节点表示被选定的真正删除的节点(节点y)。其中绿色节点(yold)表示原本需要删除的节点,而由于它有两个子节点,因此删除y代替它,并且删除y之前需要将y的值拷贝到yold,注意这里如果是红黑树也不会改变yold的颜色!通过上述的方式,将所有的节点删除问题简化为独立后继(或者无后继)的节点删除问题。然后再考虑删除y后的红黑树平衡调整问题。由于删除y节点后,y的后继节点n会作为y的父节点p的孩子。因此在进行红黑树平衡调整时,n是p的子节点。

下边考虑平衡性调整问题,首先考虑被删除节点y的颜色。如果y为红色,删除y后不会影响红黑树的平衡性,因此不需要做任何调整。如果y为黑色,则y所在的路径上的黑色节点总数减少1,红黑树失去平衡,需要调整。

y为黑色时,再考虑节点n的颜色。如果n为红色,因为n是y的唯一后继,如果把n的颜色设置为黑色,那么就能恢复y之前所在路径的黑色节点的总数,调整完成。如果n也是黑色,则需要按照以下四个步骤来考虑。

设p是n的父节点,w为n节点的兄弟节点。假定n是p的左子节点,n是p的右子节点情况可以镜像对称考虑。

步骤1:若w为红色,则断定w的子节点(如果存在的话或者为空节点)和节点p必是黑色(规则2)。此时将w与p的颜色交换,并以w为旋转轴进行左旋转操作,最后将w设定为n的新兄弟节点(原来w的左子树x)。

通过这样的转换,将原本红色的w节点情况转换为黑色w节点情况。若w原本就是黑色(或者空节点),则直接进入步骤2。

图12 节点删除情况1

步骤2:无论步骤1是否得到处理,步骤2处理的总是黑色的w节点,此时再考虑w的两个子节点x,y的颜色情况。如果x,y都是黑色节点(或者是空节点,如果父节点w为空节点,认为x,y也都是空节点),此时将w的颜色设置为红色,并将n设定为n的父节点p。此时,如果n为红色,则直接设定n为黑色,调整结束。否则再次回到步骤1做相似的处理。注意节点n发生变化后需要重新设定节点w和p。

考虑由于之前黑色节点删除导致n的路径上黑色节点数减1,因此可以把节点n看作拥有双重黑色的节点。通过此步骤将n节点上移,使得n与根节点距离减少,更极端的情况是当n成为根节点时,树就能恢复平衡了(因为根节点不在乎多一重黑色)。另外,在n的上移过程中可能通过后续的转换已经让树恢复平衡了。

图13 节点删除情况2

步骤3:如果步骤2中的w的子节点不是全黑色,而是左红(x红)右黑(y黑)的话,将x设置为黑色,w设置为红色,并以节点x为旋转轴右旋转,最后将w设定为n的新兄弟(原来的x节点)。

通过这样的转换,让原本w子节点左红右黑的情况转化为左黑右红的情况。若w的右子节点原本就是红色(左子节点颜色可黑可红),则直接进入步骤4。

图14 节点删除情况3

步骤4:该步骤处理w右子节点y为红色的情况,此时w的左子节点x可黑可红。这时将w的右子节点y设置为黑色,并交换w与父节点p的颜色(w原为黑色,p颜色可黑可红),再以w为旋转轴左旋转,红黑树调整算法结束。

通过该步骤的转换,可以彻底解决红黑树的平衡问题!该步骤的实质是利用左旋恢复节点n上的黑色节点总数,虽然p和w虽然交换了颜色,但它们都是n的祖先,因此n路径上的黑色节点数增加1。同时由于左旋,使得y路径上的黑色节点数减1,恰巧的是y的颜色为红,将y设置为黑便能恢复y节点路径上黑色节点的总数。

图15 节点删除情况4

总结以上步骤,对红黑树节点删除的平衡性调整归纳为如下流程。

图16 节点删除调整流程

通过上述的调整策略,可以完美解决红黑树节点删除时平衡性问题。

五、随机测试

对数据结构准确性的测试主要考察以下操作:插入,删除,查询,遍历和验证。插入和删除操作前边做了充分的介绍,由inset和remove实现,查询操作在插入和删除操作时会间接调用,由find实现,遍历操作分为正序(由minimum和next实现)和逆序遍历(由maximim和prev实现),验证操作主要是验证插入和删除后红黑树的合法性(规则1、2、3),由validate实现。至于其他和红黑树统计特性相关的操作,比如获取树高、节点数和累计的旋转次数等可以很容易实现。

我们使用随机数产生器随机产生一批数据插入到红黑树内,然后再随机产生一批数据作为删除操作的参数。其中每次插入和删除时都会对树的合法性进行验证,并且在插入后删除数据结束后以正序和逆序的方式输出红黑树的节点以及其他统计信息。测试代码如下:

#include"rb_tree.h"
#include <time.h> 
#include <windows.h>

int main()
{
    srand((unsigned)GetCurrentTime());
    int times=10,len=30;
    while(times--)
    {
        rb_tree<int> tree;
        for(int i=0;i<len;i++)
        {
            int num=rand()%len;
            tree.insert_unique(num);
            if(!tree.validate())cout<<"插入时失去平衡"<<endl;
        }
        cout<<"正序:";
        for(rb_tree<int>::node_type*node=tree.minimum();node;node=tree.next(node))
        {
            cout<<node->value<<" ";
        }
        cout<<"\n旋转次数-黑高-节点数:"<<tree.get_rotate_times()
            <<" "<<tree.height()<<" "<<tree.count()<<endl;
        cout<<"删除:";
        for(int i=0;i<len;i++)
        {
            int num=rand()%len;
            if(tree.remove(num))cout<<num<<" ";
            if(!tree.validate())cout<<"删除时失去平衡"<<endl;
        }
        cout<<endl;
        cout<<"逆序:";
        for(rb_tree<int>::node_type*node=tree.maximum();node;node=tree.prev(node))
        {
            cout<<node->value<<" ";
        }
        cout<<"\n旋转次数-黑高-节点数:"<<tree.get_rotate_times()
            <<" "<<tree.height()<<" "<<tree.count()<<endl;
        cout<<"________________________________________________________________________________"<<endl;
    }
    return 0;
}

经过大量的循环随机测试,可以验证红黑树数据结构的稳定性以及平衡性调整算法的正确性,下边是测试结果的部分截图。

本文构造的红黑树数据结构源代码下载地址为:GitHub - fanzhidongyzby/RBTree: The red-black tree data structrue.

读者感兴趣的话可以下载验证。

图17 测试结果

综上所述,我们对红黑树数据结构有了更充分地了解,尤其是复杂的红黑树的插入删除平衡性调整算法,最后进行的测试验证了红黑树的核心算法的正确性。

 

///

/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值