红黑树
是一种自平衡的二叉搜索树,叶子节点为空节点,都为黑色
- 节点是红或黑
- 根节点是黑色的
- 叶子节点为空,且是黑色的==>这个特性保证有效节点度为2,实现的时候空节点可用同一个空节点表示
- 不能有连续的两个红色节点(红色节点的父与子节点只能是黑色的)
- 从任意节点出发到其能到的所有叶子节点的路径中包含相同数目的黑色节点
==>最深深度最大不过是最浅深度的两倍,这个性质确保了查找效率最坏情况下不会太高
优点:增删改都是logn ==> 1亿的数据每个操作最坏也才27*2次
- 数组查:n,删:n
- 链表:操作几乎会带上一次查,算上查找的复杂度话都是n
- AVL树,调整时最坏情况需要知道深度,也是n
- 链表加上索引==>查快了,但空间上去:具体实现可以是b+树,或者跳表!
1.插入操作
调整策略:
- 旋转能够保证依旧是二叉搜索树,但是没法保证红黑树特性,可能还要额外需要一些修改颜色的策略!
默认的插入节点颜色:
- 默认插入节点默认为红色(如果是黑色,插入必破坏红黑树,绝对要调整,默认为黑色就不那么明智)
- 默认为红色插入后,除了没法保证红色节点父亲一定是黑色节点外,其他特性都能保持
插入操作的特点:插入操作必然是在叶子节点发生的,也就是说插入的位置其实是取代一个叶子节点
- 这个特点说明插入的节点的子节点就是树的叶子节点,必然为黑色,讨论插入的情况直接忽略插入后节点子节点的情况
插入一个节点的可能有情况:
- 第一种情况:父亲节点为黑色,这种是最好情况,不需要调整
- 第二种情况:父亲节点为红色,那么爷爷节点的情况必然为黑,这种情况出现了两个连续的红色
只要将插入节点,以及其父亲,爷爷节点以及叔叔节点提取出来视作一颗红黑树然后进行调整,一一列出情况,然后其情况一一找到对应调整策略即可
可能没法一次性就调整成功,因为调整策略可能会修改我们提取出来的局部的红黑树的根节点(也就是爷爷节点)的颜色,再次放回到原树中时,可能导致从爷爷节点出现两个红的情况,这时我们再以爷爷节点作为插入的节点看待,直到到达整棵树的根,最后一次调整可能会调整树根颜色,我们需要修改回黑色!
最坏情况分类
左和右的情况完全对称!只讨论左侧情况,右侧同理
圆圈表示节点,三角形表示为一棵树(用一棵树来表示,调整不是一轮就能完成!)
情况1:导致不平衡的节点为a,调整后原来根节点变为红色,放回原树时需要检查是否需要继续向上进行调整
情况2:导致不平衡的节点为a,根节点没有变化,无需进行继续的调整
情况3:导致不平衡的节点为a,根节点已经改变,调整需要考虑是否继续调整
情况4:导致不平衡的节点为b,调整后根节点颜色没有改变无需继续调整
调整后不关注其他子节点颜色:原父亲节点就是红色,3+4调整后其父亲依旧是红色,原来父亲为红,修改颜色为黑更不会出现问题
情况1和情况3的修改策略几乎一致,可以归为一类考虑,同样情况2,情况4可以归为一类考虑!
最终实现时别忘记没有导致不平衡的情况,以及与左侧完全对称的右侧的情况!
2.删除操作
删除操作讨论的情况比增加复杂,但是最终通过不同情况之间调整步骤,单侧情况最终会归为相对较少的几类,同样只讨论左侧,右侧完全对称
删除的特点
- 删除中间的节点时,我们采取找到其右子树的最小值来替换带删除的节点,然后删除最小值所在节点即可 也可以找左子树中最大值所在位置,然后仿照如下方式进行讨论即可 你甚至可以两种策略都用上,那种删除时调整情况简单用那种==>这样做也许效率变高了,是否真正变高需要更细致的分析
- 最终会将所有的删除操作所删除的节点的特点是:左子树为一个叶子节点 然后只要根据这个删除策略产生的情况一一讨论即可
初步讨论
如下,被删除节点简单的周围环境可能会有:(只列举被删除节点和他子节点的情况)
首先被删除节点右树不为Nil,那么该节点就只能为黑色,否则一定不满足红黑树的定义,从而就有上面3种情况
初步讨论删除后恢复平衡策略
- 情况1:如果删除后,红色节点顶替该节点,为了保持平衡,修改顶替上来的节点的颜色为黑色即可
- 情况2:如果是根节点直接删除即可,如果不是根节点,只看上面绘制出来的节点,删除后无论怎么做都不能保持平衡,需要更多信息
- 情况3:红色删除即可,没有任何影响
情况2的调整解决了,整个删除带来的不平衡问题也就解决了
对于情况2,用nil来顶替删除节点的位置,将这个nil看做是一棵树,nil保持原来的颜色,这棵树的黑路径与其兄弟的黑路径之差为1
将情况2的父亲,兄弟,侄子都画出来,删除后看看情况,如下就是一种删除后的情况,后面只讨论删除后情况,然后调整即可
真正删除的情况只有在叶子节点(a的左右子树都为Nil,直接丢掉一个,得到右边情况)
删除后出现的问题是左边黑高比右边黑高少1(删除黑色节点在右边时,右比左少1,将删除节点在左边的情况讨论清楚,右边的调整方案自然也就出来了)==>我们的目标尽量调整后使得左黑高+1,右黑高不变,显然这是最理想的情况!
对删除后父兄侄进行讨论然后做局部的调整,调整后放回原树,显然调整结果可能不是左边黑高+1回到初始状态,而是右边黑高-1,这时整棵树高度-1,放回原树必然会导致不平衡,只要将整棵树作为原树其中一个左右子树的一部分,接着按照情况进行调整,直到到达根为止。最后到达根时,如果根节点因为调整变为红色,需要改回黑色。
情况2不同情况导致的问题可能不是一次调整就能解决,当放回原来的红黑树后,又要根据放回的位置继续调整,直到根节点!调整的最大次数和树高相关。通过下面的例子很快就看到调整后局部黑高比原来少1的情况
为什么左子树根的颜色不需要考虑?通过对不同情况讨论,然后得到调整策略,简单分析就能得到答案,其他不讨论的节点的颜色也是同理
- 有的情况,调整的策略不只有一种,下面的调整情况考虑了在最终解决问题的时的统一策略,或许有更好策略组合,使得最终解决问题的策略更少(《算法导论》一书中只用不到30行代码就完成了调整,简洁得让人抓狂,给你一种感觉你能懂,但是你又搞不懂的感觉)
- 调整时尽量不要使得黑色侄子的位置上替换兄弟或者父亲节点,他可能是叶子节点,除非能够证明其一定不是叶子节点
我们将其父亲,兄弟,侄子节点颜色情况一一列举,来观察调整规律,最后进行整合:
命名解释:
- 对侄子的命名,离不平衡的树(也就是绿色的树)最远称为侄1,另外一个侄子称为侄2
- 同时如果要进行3+4重构(有时候不能叫做重构,旋转更加贴切,但是3+4能一步完成需要两次旋转的情况,这两种情况很好区分)树分别以a,b,c命名,其左右子树分别以1,2,3,4进行命名,按照小到大来排序
=_=,实现的时候发现一个问题,3+4调整虽然简单,但是在红黑树中使用时要保证叶子节点参与时不会出错,没有旋转来健壮!另外所有的3+4重构使用旋转都能实现
讨论的顺序
- 父亲为红,分别讨论兄弟,侄子的情况
- 父亲为黑,分别讨论兄弟,侄子的情况
先来观察父亲为红色的情况,这种情况的兄弟只能为黑色,根据侄子的颜色讨论即可:
情况1:
侄子1为红色==>不用关注侄2的颜色
观察到修改后原来父亲位置的颜色没有被改变,整体树高也没有改变,放回后不用继续调整
情况2:
侄子1颜色为黑色,侄子2为红色
可以一步到达最终情况,但.....显然这种情况调整后后续也无需继续调整,红色侄子2必然是有效节点,只能用旋转,重构没法完成
情况3:
侄子都为黑
侄子可能是叶子节点,不能贸然进行3+4重构,按照上述方式调整,显然这种情况,整体高度也没有变化,父亲由红变黑,更不用再继续调整
父亲为红色的情况讨论完毕!可以知道:只要父亲为红,只要一次调整就可以结束!
父亲为黑色,兄弟可以为黑可以为红
情况1:
兄红,双侄黑
这种情况问题转换后,变为了父亲为红色的节点,显然这种情况两次调整后也就结束了
情况2:
兄黑,双侄子黑
这种情况下,侄子有可能是叶子节点,不能贸然的进行3+4重构,当然3+4重构也解决不了问题,按照上述调整,整体黑高变少了
放回原树,需要以父亲作为根看新的绿色的部分继续接着调整!
情况3:
兄黑,侄子2红
转换侄子1红的情况,再次进行调整效果和这种调整方式一致
为了最终能和前面的情况(父红,侄1黑,侄2红)放到一起讨论,当然如果前面那种情况直接采取调整到最终平衡情况,这里也是同样的道理!
情况4:
兄黑,侄子1红,不考虑侄2的颜色,无论什么都能一起解决
直接平衡,无需调整
总结
- 只有一种情况能够导致需要接着调整!
- 没有任何一种情况会修改父亲节点为红色,不会出现双红冲突,很容易就能得到整个调整过程都不会出现双红冲突
上述情况如果转化为代码,需要多个if-else结果,有一些情况过程几乎一致,有的也只有一点点区别,仔细分析也能转换成一致的过程,从而简化代码
情况1:侄1为红
兄弟改父色,父改黑色,侄1变黑,父兄进行一次左旋(3+4重构效果一致),最终平衡不用再继续调整
情况2:侄1黑,侄2红
兄弟改红,侄2改黑,旋转
情况3:兄弟为黑,两侄子为黑
父改黑,兄变红,原父为黑需要重新调整
情况4:兄红两侄黑
父变红,兄变黑,以父亲往矮的方向进行旋转