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