红黑树的删除调整
插入调整是站在祖父节点向下看,而删除调整则是站在父节点向下看。
删除调整发生的场景
删除怎样的节点会引起红黑树的失衡
删除度为0的红色节点
平衡条件中并没有对度为0的红色节点有什么限制,因此,度为0的红色节点可以直接删除,不影响红黑树的平衡
删除度为1的红色节点
根据之前我们学习的AVL树删除度为1的节点的调整策略,我们要将度为1的红色节点的唯一子节点挂到他爸爸上面去(托孤),这样,即使他的子节点是黑色节点,也不会影响红黑树的平衡,因为每条路径上的黑色节点依然相同。
但是,我们还需要思考一个问题,在红黑树中,有可能存在度为1的红色节点吗?
答案是不可能存在的。因为,一个红色节点,子节点必定是黑色,那么,但凡红色节点有一个黑色的子节点,为了保证红黑树的平衡,另一棵子树肯定不可能为空,必然至少会包含一个黑色节点,那么,这样就不可能存在度为1的红色节点了。因此,我们在进行红黑树的删除调整时,可以不考虑这种情况。
删除度为1的黑色节点
经过上面度为1的红色节点的思考,那么我们再来想一下,是否可能存在度为1的黑色节点呢?如果一个黑色节点度为1,那么,他的子节点要么是红色,要么是黑色。假如说他的子节点是黑色,那这个节点的另一边的子树必然不可能为空,不然每条路径上黑色节点数量就不相同了。因此,如果当前节点是度为1的黑色节点,那么他的子节点只可能是红色,而且这个红色节点不可能存在子节点,即这个红色节点必定度为0(因为如果不是度为0,那么这个红色节点下面一定是黑色节点,必然会导致当前节点发生失衡)。
到此,我们明确了,一个度为1的黑色节点下面只能是度为0的红色节点,那么,我们要怎么删除这个度为1的黑色节点呢?
首先,将黑色节点删掉,然后将黑色节点的子节点挂到他的父节点上,由于黑色节点的子节点必然是度为0的红色节点,因此,我们只需要将这个节点的颜色由红色变成黑色即可。
删除度为0的黑色节点
度为0的黑色节点的删除是红黑树删除调整最麻烦的一步,由于删除了一个度为0的黑色节点,那么必然会导致红黑树原节点所在的那条路径上少了一个黑色节点。那我们该怎么办呢?大家别忘了我们之前定义过所有实际叶子节点上,都还挂着NIL节点,我们首先将所有节点以往隐藏不去考虑的NIL节点显示出来,那么,当把一个度为0的黑色节点删除之后,原黑色节点的父级下面就挂了NIL节点,但是此时,由于所有路径都挂了NIL节点,并且按照约定,NIL节点算是黑色节点,我们原先黑色节点所在的路径依然是少了一个黑色节点。此时,我们可以把顶替这个黑色节点的NIL节点标记成双重黑,这个节点算是两个黑色节点,这样,我们的红黑树又重归平衡了。
上面说的几种情况,我们几乎都不需要做太多的额外调整,而我们红黑树的删除调整,就是为了干掉这个双重黑节点。
删除调整情况
我们上面说了,删除调整主要是为了干掉双重黑节点,在下面的示例中,标记了🌚的便是双重黑节点。
删除调整情况一
这种情况主要值针对双重黑节点的兄弟节点是黑色节点,兄弟节点的子节点也是黑色节点的情况,具体如下图:
这种情况处理起来比较简单,既然黑70是双重黑,那么,我们先把黑70减去一重黑,减去之后,发现右子树比原先少了一重黑,我们就在其父节点黑50上加一重黑,然而,此时左子树又多了一重黑,我们让黑50的左子树减去一重黑变成红色,此时左右子树黑色又平衡了。那么大家可能会疑问了,此时黑50变成双重黑了,没有干掉呀!大家不要忘了,我们的调整操作是一个递归回溯的过程,我们上面给出的树只是红黑树中的一段子树,我们只需要再递归回溯时继续向上调整,直到最终双重黑到了根节点处,此时,我们直接将双重黑减去一重黑变成普通的黑色,由于是根节点,减去一重黑色后,左右子树都同时少了一重黑色,因此不会导致左右子树每个路径下面的黑色节点数量失衡。到此,我们第一种情况的调整就算完成了。
删除调整情况二
双重黑节点的兄弟节点是黑色节点,兄弟节点的右子树是红色节点(RR型失衡)
那么,首先,我们来确定一下,上面哪些节点的颜色是确定的。
首先,双重黑节点黑5一定是确定的,因为当前就是在讨论这个问题。
由于情况二明确了双重黑的兄弟节点是黑色,那么黑25号节点的颜色也是确定的
由于情况二明确了双重黑的兄弟节点的右子树根节点是红色,所以红28节点也是确定的
用于红28确定了是红色,因此黑26和黑32肯定确定为黑色。
由于节点红22是左子树根节点,他既可能是黑色也可能是红色,因此,红22节点的颜色时不确定的
由于红22号节点没办法确定,因此黑23号节点也是不确定的。
因此,我们可以知道,确定性的节点有:黑5、黑25、红28、黑26、黑32。
这种类型的失衡,我们又称为RR失衡,那么,大家是否还记得,在AVL树中,RR失衡要怎么调整呢?是不是直接拽着红22节点进行一个x左旋即可,以下为左旋之后的情况
那么接下来就来分析一下删除调整怎么调。
当22号节点是红色时
-
由于黑23号节点的颜色不确定,如果红22号节点真的是红色的话,那是不是意味着黑23号节点的颜色时确定性黑色了?这就与我们上面分析的结果相悖了。因此,我们不管红22节点真实的颜色时红色还是黑色,此时我们都需要将红22节点调整成黑色。即,变为黑22。
-
对于以黑25作为根节点的子树来说,左子树有3个确定性的黑色节点,而其他路径上只有2个确定性的黑色节点(注:由于黑23不是确定性的黑色,因此不算),因此,我们将根节点黑25先变成红色
-
但是,此时,以红25为根节点的右子树的黑色节点比之前少了一个,又因为红28号节点是确定性的红色,因此,我们可以把红28号节点改成黑色
-
到此,以黑25为根节点的左子树又重归平衡了。
总结一下,删除调整情况二且22号节点是红色时的调整方法:RR型失衡,先将失衡子树进行一次左旋,然后将左子树根节点和右子树根节点变成黑色,根节点变成红色,最后将双重黑节点去掉一重黑即可
当22号节点是黑色时
如果原22号节点的颜色时黑色的话,那么原先以22号节点的子树就应该有3个黑色节点,为了保证调整之后黑色节点数量不变,我们需要将25号节点改成黑色。
总结一下,删除调整情况二且22号节点是黑色时的调整方法:RR型失衡,先将失衡子树进行一次左旋,然后将左子树根节点和右子树根节点变成黑色,根节点变成黑色,最后将双重黑节点去掉一重黑即可
总结
综上,调整情况二的调整方法为:RR型失衡,先将失衡子树进行一次左旋,然后将左子树根节点和右子树根节点变成黑色,根节点变成调整前原根节点(22号节点)的颜色,最后将双重黑节点去掉一重黑即可
删除调整三
双重黑节点的兄弟节点是黑色,兄弟节点的左子树根节点是红色(RL型失衡)
调整方法与AVL树RL类型失衡调整类似,先抓着失衡子树根节点进行小右旋,然后将新根节点与原根节点的颜色互换,调整成RR类型的失衡,然后再根据RR类型的调整方式进行调整即可。
总结
先将失衡子树右旋,然后将新根节点与原根节点的颜色互换转换为RR类型失衡。然后再根据RR类型失衡进行调整即可
删除调整四
双重黑节点的兄弟节点是红色节点的情况。
先将失衡子树左旋,然后将根节点(即原双重黑节点的兄弟节点)改成黑色,然后将原先的根节点改成红色,这样,我们就得到了一个双重黑色节点兄弟节点是黑色的情况,继续删除调整三的步骤调整即可。
完整红黑树代码演示
前端项目中使用红黑树
我们经常使用的ESLint,用于对我们项目的语法、代码格式等进行合法性或优化检查,其中针对代码格式化检查时,需要知道当前代码的token与上一个token的偏移量,在ESLint当中便使用了红黑树的方式辅助,快速高效的进行插入与查找等操作。此处使用的是functional-red-black-tree,有兴趣的同学,可以去了解一下细节。ESLint