zoukankan      html  css  js  c++  java
  • [转载]数据结构树之红黑树

    本文转载自水目沾博客:http://www.cnblogs.com/zhuwbox/p/3634895.html

    红黑树简介:

      红黑树是一棵二叉搜索树,它在每个结点上增加了一个存储位来表示结点的颜色,可以是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().这个分别是左旋和右旋的函数。左旋和右旋的过程我已经在我的另一篇博客中用图解释的很清楚了:http://www.cnblogs.com/zhuwbox/p/3636783.html

      

    下面是左旋的伪代码:

    复制代码
     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循环(如果不退出循环,情况肯定是这三种中的一种)。一棵合法的红黑树形成!

  • 相关阅读:
    PHP数组(数组正则表达式、数组、预定义数组)
    面向对象。OOP三大特征:封装,继承,多态。 这个讲的是【封存】
    uvalive 3938 "Ray, Pass me the dishes!" 线段树 区间合并
    LA4329 Ping pong 树状数组
    HDU 1257 最少拦截系统
    HDU 1260 Tickets
    codeforce 621D
    codeforce 621C Wet Shark and Flowers
    codeforce 621B Wet Shark and Bishops
    codeforce 621A Wet Shark and Odd and Even
  • 原文地址:https://www.cnblogs.com/HXloveLL/p/3680039.html
Copyright © 2011-2022 走看看