红黑树数据结构剖析
红黑树是计算机科学内比较经常使用的一种数据结构,它使得对数据的搜索,插入和删除操做都能保持在O(lgn)的时间复杂度。然而,相比于通常的数据结构,红黑树的实现的难度有所增长。网络上关于红黑树的实现资料汗牛充栋,可是乏于系统介绍红黑树实现的资料。本文经过一个本身实现的红黑树数据结构以及必要的搜索,插入和删除操做算法,为你们更系统地剖析红黑树数据结构的实现。git
对于大部分数据结构,通常都会使用抽象数据类型的方式实现,C++提供的模板机制能够作到数据结构与具体数据类型无关,就像STL实现的那样。不过本文并不是去实现STL中的红黑树,更重要的是透过红黑树的实现学习相关的算法和思想。固然,咱们仍是会借鉴STL中关于红黑树实现部分有价值内容。github
1、基本概念
在具体实现红黑树以前,必须弄清它的基本含义。红黑树本质上是一颗二叉搜索树,它知足二叉搜索树的基本性质——即树中的任何节点的值大于它的左子节点,且小于它的右子节点。windows
图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 红黑树
2、数据结构设计
和通常的数据结构设计相似,咱们用抽象数据类型表示红黑树的节点,使用指针保存节点之间的相互关系。
做为红黑树节点,其基本属性有:节点的颜色、左子节点指针、右子节点指针、父节点指针、节点的值。
图3 红黑树节点基本属性
为了方便红黑树关键算法的实现,还定义了一些简单的操做(都是内联函数)。
为了表示红黑树节点的颜色,咱们定义一个简单的枚举类型。
有了节点,剩下的就是实现红黑树的构造、插入、搜索、删除等关键算法了。
在红黑树类中,定义了树根(root)和节点数(count),其中还记录红黑树在插入删除操做时执行的旋转次数rotate_times。其中核心操做有插入操做(insert),搜索操做(find),删除操做(remove),递减操做(prev)——寻找比当前节点较小的节点,递增操做(next)——寻找比当前节点较大的节点,最大值(maximum)和最小值(minimum)操做等。其中验证操做(__ validate)经过递归操做红黑树,验证红黑树的三个基本颜色约束,用于操纵红黑树后验证红黑树是否保持平衡。
因为插入和删除操做是红黑树的关键所在,下边重点介绍这两个操做。其余的操做通常经过对树进行递归操做均可以轻松的完成,这里再也不赘述。
3、红黑树的插入操做
红黑树的插入操做和查询操做有些相似,它按照二分搜索的方式递归寻找插入点。不过这里须要考虑边界条件——当树为空时须要特殊处理(这里未采用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指针为空)就能够中止回溯了。
经过以上的树从新平衡策略能够完美地解决红黑树插入节点的平衡问题。
4、红黑树的删除操做
相比于插入操做,红黑树的删除操做显得更加复杂。不少资料都没有将红黑树的删除解释清楚,清华的数据结构教材对红黑树删除的描述也十分混乱,《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 节点删除调整流程
经过上述的调整策略,能够完美解决红黑树节点删除时平衡性问题。
5、随机测试
对数据结构准确性的测试主要考察如下操做:插入,删除,查询,遍历和验证。插入和删除操做前边作了充分的介绍,由inset和remove实现,查询操做在插入和删除操做时会间接调用,由find实现,遍历操做分为正序(由minimum和next实现)和逆序遍历(由maximim和prev实现),验证操做主要是验证插入和删除后红黑树的合法性(规则1、2、3),由validate实现。至于其余和红黑树统计特性相关的操做,好比获取树高、节点数和累计的旋转次数等能够很容易实现。
咱们使用随机数产生器随机产生一批数据插入到红黑树内,而后再随机产生一批数据做为删除操做的参数。其中每次插入和删除时都会对树的合法性进行验证,而且在插入后删除数据结束后以正序和逆序的方式输出红黑树的节点以及其余统计信息。测试代码以下:
通过大量的循环随机测试,能够验证红黑树数据结构的稳定性以及平衡性调整算法的正确性,下边是测试结果的部分截图。
本文构造的红黑树数据结构源代码下载地址为:https://github.com/fanzhidongyzby/RBTree。
读者感兴趣的话能够下载验证。
图17 测试结果
综上所述,咱们对红黑树数据结构有了更充分地了解,尤为是复杂的红黑树的插入删除平衡性调整算法,最后进行的测试验证了红黑树的核心算法的正确性。经过对红黑树数据结构的详尽剖析,相信你们对数据结构在计算机学科的重要性有了更充分地认识,但愿本文对你有所帮助。