JDK源码那些事儿之红黑树基础下篇
说到HashMap,就一定要说到红黑树,红黑树作为一种自平衡二叉查找树,是一种用途较广的数据结构,在jdk1.8中使用红黑树提升HashMap的性能,今天就来说一说红黑树,上一讲已经给出插入平衡的调整操作,这一讲就说说更为复杂的删除平衡操作。
前言
限于篇幅,本文只对红黑树的基础进行说明,暂不涉及源码部分,大部分摘抄自维基百科,这里也贴出对应链接:
维基百科(中文):https://zh.wikipedia.org/wiki/红黑树
维基百科:https://en.wikipedia.org/wiki/Red–black_tree
笔者这里会根据维基百科的讲解做些说明,方便初学者理解。
删除平衡
在正式进入红黑树的删除说明之前,想下二叉搜索树中的删除是如何做的?
参照二叉搜索树的删除调整原理:
如果需要删除的节点有两个儿子,那么问题可以被转化成删除另一个只有一个儿子的节点的问题(为了表述方便,这里所指的儿子,为非叶子节点的儿子)。对于二叉查找树,在删除带有两个非叶子儿子的节点的时候,我们要么找到它左子树中的最大元素、要么找到它右子树中的最小元素,并把它的值转移到要删除的节点中(如在这里所展示的那样)。我们接着删除我们从中复制出值的那个节点,它必定有少于两个非叶子的儿子。因为只是复制了一个值(没有复制颜色),不违反任何性质,这就把问题简化为如何删除最多有一个儿子的节点的问题。它不关心这个节点是最初要删除的节点还是我们从中复制出值的那个节点。
那么红黑树中会出现哪些情况呢?
- 删除节点的左右子树都非空
- 删除节点的左子树为空,右子树非空
- 删除节点的右子树为空,左子树非空
- 删除节点的左右子树都为空
对于第一种情况,我们可以找到删除节点的后继节点,将值替换,然后删除后继节点,这样保证了该树仍然是一棵二叉树,但是在删除后继节点时可能破坏了红黑树的特性,故需要进行调整。强调一下,红黑树中的叶子节点指的都是NIL节点。
这样来看,被删除的节点一定有一个右子树,可能为NIL也可能为非空子树,接下来就具体看看情况。
1.如果删除节点E为红色,则E子节点F则必为黑色(红黑树特性),这种情况只有在E的两个节点都为叶子节点时才会发生。故删除之后红黑树平衡不用调整。可以自行画图验证:
- 删除节点E有两个非叶子节点,这不可能,因为E已经是后继节点。
- E有一个非叶子节点(右子节点),则这个非叶子节点F为黑色,通过E的两条黑色路径不同,不满足特性5
2.如果删除节点E为黑色,F为红色,这种情况下,F节点有两个叶子节点(需保证红黑树特性,黑色路径需保持一致)则F放置到E处时只需要变色就可以使得红黑树平衡
3.如果删除节点E为黑色,F也为黑色,这种情况只有在E的两个节点都为叶子节点时才会发生。参考上边情况1的验证。这里删除了一个黑色节点,红黑树平衡被破坏(黑色路径不同了),需要进行调整
针对3这里就又会有以下几种情况:
N为删除的位置节点,现在被删除的节点的子节点取代(这里子节点对应上边的F,即删除后,N的位置为叶子节点),N为黑色,P为N的父母,S为P的右子,SL代表S的左子,SR代表S的右子。
S必不为叶子节点,反推下,如果为叶子节点,在未删除之前P的两边黑色路径就不一致了(未删除之前是P->E->F这种,自行画图理解)。注意,下面列举的情况都是在删除E节点后,子节点取代E形成的情况,在此基础上进行红黑树的调整。按照顺序每种情况进行判断处理
。注意每种情况处理之后会重新标记,以适应下次情况的对比调整,并且下列过程只以第一次调用时说明,递归调用下列顺序过程时将叶子节点当成一个已经平衡的局部红黑树即可。和之前的插入平衡调整类似,每次都是局部化调整。
第一种情况:如果N为根节点,不需要调整平衡了,原有树只有一个非叶子节点,两个叶子节点,删除了根节点,相当于删除了红黑树。继续第二种情况判断。
第二种情况:如果N(这里是叶子节点NIL)是其父P的左子节点,S为红色,P必为黑色,参照下图,反转P和S的颜色,然后在P处向左旋转,将S转换为N的祖父母。这里N处的黑色路径少一个,还未平衡。N是其父P的右子节点参照相似处理。SL标记为新的S继续以N,P,S这块局部树进行处理。继续第三种情况处理。
第三种情况:如果P,S和S的孩子都是黑色,左图显示了出现的情况,在N替换掉之前的父节点后形成的状态,这里重新绘制S为红色,变为右图,在这个局部中满足平衡红黑树的特性4和5,但是通过P节点的黑色路径相比原有结构减少了1,还需要进行调整,需重新进行平衡。这里重新平衡即从第一种情况再次顺序执行,以P节点进行重新平衡,相当于以P为新的N,黑色路径少1,再次进行平衡调整。不满足当前情况,再继续执行第4种情况处理。
第四种情况:如果S和S的孩子是黑色,但P是红色的。在这种情况下,我们只需交换S和P的颜色。这不会影响通过S的路径上的黑色节点数量,但它确实会在通过N的路径上添加一个黑色节点数,从而弥补这些路径上已删除的黑色节点。将达到红黑树平衡。不满足当前情况,则继续第五种情况的处理。
第五种情况:如果S是黑色,S的左孩子是红色,S的右孩子是黑色,N是其父母的左孩子。在这种情况下,我们在S处右转,这样S的左边孩子就成了S的父母和N的新兄弟。然后我们交换S及其新父母的颜色。所有路径仍然有相同数量的黑色结点,但是P的左子树因为删除一个节点导致黑色路径少1,还未完全平衡。这里进行调整主要是为了满足第六种情况,继续第六种情况的处理。
第六种情况: 如果S是黑色,S的右子是红色,N是其父P的左子。在这种情况下,我们向左旋转P,这样S成为P和S的右子的父亲。然后,我们交换P和S的颜色,并使S的右子节点变黑。比较删除前与N替换调整后的属性,满足4和5,与删除前是一致的。
总结
删除操作相对插入操作之后的平衡要复杂的多,不过按照情况一步步处理也是比较明了的,同样为了方便初学者理解,从上边的过程我们也可以发现,在一次局部平衡调整中,最多进行3次旋转操作,我这里再进行一个流程梳理,帮助各位更好的理解红黑树的删除操作。
到此关于红黑树的基础已经介绍完毕,下一章我将就JDK源码中的TreeNode进行讲解说明,看一看红黑树是如何在源码中实现的。
参考资料: