数据结构树之红黑树

红黑树简介:

  红黑树是一棵二叉搜索树,它在每个结点上增加了一个存储位来表示结点的颜色,可以是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循环(如果不退出循环,情况肯定是这三种中的一种)。一棵合法的红黑树形成!

  

原文地址:https://www.cnblogs.com/zhuwbox/p/3634895.html