红黑树
前言:耗费了五个多小时,算是差不多了
红黑树是一种结点带有颜色属性的二叉查找树,但它在二叉查找树之外,还有以下要求:
- 节点是红色或黑色。
- 根是黑色。
- 所有叶子都是黑色(叶子是NIL节点)。
- 每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
- 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。
下图就是一个典型的红黑树:
但实现上我省略了其中的 Nil 结点,一般如下图,大家理解时也可以忽略它们。
优势和用途
我们知道二叉查找树在不停地添加或删除结点后,可能会导致结点情况如下:
这种情况下,二叉查找树的查找效率最坏会降低为 O(n)
。
而红黑树由于在插入和删除结点时都会进行变色旋转等操作,在符合红黑树条件的情况下,即使一边子树全是黑色结点,另一边子树全是红黑相间,两子树的高度差也不会超过一半。一棵有 n 个结点的红黑树高度至多为 2log(n+1)
,查找效率最坏为 O(log(n))
。
所以红黑树常被用于需求查找效率稳定的场景,如 Linux 中内核使用它管理内存区域对象、Java8 中 HashMap 的实现等,所以了解红黑树也很有意义。
下面介绍一下红黑树的等同 2-3-4树。
2-3-4树
2-3-4树是四阶的 B树(Balance Tree),它的结构有以下限制:
- 所有叶子节点都拥有相同的深度。
- 节点只能是 2-节点、3-节点、4-节点之一。
- 2-节点:包含 1 个元素的节点,有 2 个子节点;
- 3-节点:包含 2 个元素的节点,有 3 个子节点;
- 4-节点:包含 3 个元素的节点,有 4 个子节点;
- 元素始终保持排序顺序,整体上保持二叉查找树的性质,即父结点大于左子结点,小于右子结点;而且结点有多个元素时,每个元素必须大于它左边的和它的左子树中元素。
下图是一个典型的 2-3-4树(来自维基百科):
2-3-4树的查询操作像普通的二叉搜索树一样,非常简单,但由于其结点元素数不确定,在一些编程语言中实现起来并不方便,实现一般使用它的等同——红黑树。
红黑树维持平衡的方式
- 变色
- 旋转(左旋、右旋、先左旋再右旋、先右旋再左旋)
对应红黑树
至于为什么说红黑树是 2-3-4树的一种等同呢,这是因为 2-3-4树的每一个结点都对应红黑树的一种结构,所以每一棵 2-3-4树也都对应一棵红黑树,下图是 2-3-4树不同结点与红黑树子树的对应。
而上文中的 2-3-4树也可以转换成一棵红黑树:
由红黑树的性质5,和 2-3-4树的性质1,为了便于理解红黑树和 2-3-4树的对应关系,我们可以把红黑树从根结点到叶子结点的黑色结点个数定义为高度
。
红黑树和 2-3-4树的结点添加和删除都有一个基本规则:避免子树高度变化,因为无论是 2-3-4树还是红黑树,一旦子树高度有变动,势必会影响其他子树进行调整,所以我们在插入和删除结点时尽量通过子树内部调整来达到平衡,2-3-4树实现平衡是通过结点的旋转和结点元素数变化,红黑树是通过结点旋转和变色。
下面来对照着 2-3-4树说一下红黑树结点的添加和删除:
结点插入
2-3-4树中结点添加需要遵守以下规则:
- 插入都是向最下面一层插入;
- 升元:将插入结点由 2-结点升级成 3-结点,或由 3-结点升级成 4-结点;
- 向 4-结点插入元素后,需要将中间元素提到父结点升元,原结点变成两个 2-结点,再把元素插入 2-结点中,如果父结点也是 4-结点,则递归向上层升元,至到根结点后将树高加1;
而将这些规则对应到红黑树里,就是:
- 新插入的结点颜色为
红色
,这样才可能不会对红黑树的高度产生影响。 - 2-结点对应红黑树中的单个黑色结点,插入时直接成功(对应 2-结点升元)。
- 3-结点对应红黑树中的
黑+红
子树,插入后将其修复成红+黑+红
子树(对应 3-结点升元); - 4-结点对应红黑树中的
红+黑+红
子树,插入后将其修复成红色祖父+黑色父叔+红色孩子
子树,然后再把祖父结点当成新插入的红色结点递归向上层修复,直至修复成功或遇到 root 结点;
如上图所示,虽然向红黑树中插入了一个新结点,但由于旋转和变色,子树的高度保持不变。
删除结点
红黑树的删除要比插入要复杂一些,我们还是类比 2-3-4树来讲:
- 查找最近的叶子结点中的元素替代被删除元素,删除替代元素后,从替代元素所处叶子结点开始处理;
- 降元:4-结点变 3-结点,3-结点变 2-结点;
- 2-结点中只有一个元素,所以借兄弟结点中的元素来补充删除后的造成的空结点;
- 当兄弟结点中也没有多个元素可以补充时,尝试将父结点降元,失败时向上递归,至到子树降元成功或到 root 结点树高减1;
将这些规则对应到红黑树中即:
- 查找离当前结点最近的叶子结点作为
替代结点
(左子树的最右结点或右子树的最左结点都能保证替换后保证二叉查找树的结点的排序性质,叶子结点的替代结点是自身)替换掉被删除结点,从替代的叶子结点向上递归修复; - 替代结点颜色为红色(对应 2-3-4树中 4-结点或 3-结点)时删除子结点直接成功;
- 替代结点为黑色(对应 2-3-4树中 2-结点)时,意味着替代结点所在的子树会降一层,需要依次检验以下三项,以恢复子树高度:
- 兄弟结点的子结点中有红色结点(兄弟结点对应 3-结点或 4-结点)能够“借用”,旋转过来后修正颜色;
- 父结点是红色结点(父结点对应 3-结点或 4-结点,可以降元)时,将父结点变黑色,自身和兄弟结点变红色后删除;
- 父结点和兄弟结点都是黑色时,将子树降一层后把
父结点当作替代结点
递归向上处理。
如上图,删除的要点是 找到替代结点
,如果替代结点是黑色,递归向上依次判断侄子结点、父结点是否可以补充被删除的黑色,整体思想就是将删除一个黑色结点造成的影响局限在子树内处理。
结点插入
插入的节点都是红色的,然后再做平衡
插入情景1:红黑树为空树
最简单的一种情景,直接把插入结点作为根结点就行,但注意,根据红黑树性质2:根节点是黑色。还需要把插入结点设为黑色。
处理:把插入结点作为根结点,并把结点设置为黑色。
插入情景2:插入结点的Key已存在
插入结点的Key已存在,既然红黑树总保持平衡,在插入前红黑树已经是平衡的,那么把插入结点设置为将要替代结点的颜色,再把结点的值更新就完成插入。
处理:
- 把I设为当前结点的颜色
- 更新当前结点的值为插入结点的值
插入情景3:插入结点的父结点为黑结点
由于插入的结点是红色的,并不会影响红黑树的平衡,直接插入即可,无需做自平衡。
处理:直接插入。
插入情景4:插入结点的父结点为红结点
再次回想下红黑树的性质2:根结点是黑色。如果插入的父结点为红结点,那么该父结点不可能为根结点,所以插入结点总是存在祖父结点。这点很重要,因为后续的旋转操作肯定需要祖父结点的参与。
情景4又分为很多子情景。
插入情景4.1:叔叔结点存在并且为红结点
从红黑树性质4可以,祖父结点肯定为黑结点,因为不可以同时存在两个相连的红结点。那么此时该插入子树的红黑层数的情况是:黑红红。显然最简单的处理方式是把其改为:红黑红。
处理:
- 将P和S设置为黑色
- 将PP设置为红色
- 把PP设置为当前插入结点
可以看到,我们把PP结点设为红色了,如果PP的父结点是黑色,那么无需再做任何处理;但如果PP的父结点是红色,根据性质4,此时红黑树已不平衡了,所以还需要把PP当作新的插入结点,继续做插入操作自平衡处理,直到平衡为止。
试想下PP刚好为根结点时,那么根据性质2,我们必须把PP重新设为黑色,那么树的红黑结构变为:黑黑红。换句话说,从根结点到叶子结点的路径中,黑色结点增加了。这也是唯一一种会增加红黑树黑色结点层数的插入情景。
我们还可以总结出另外一个经验:红黑树的生长是自底向上的。这点不同于普通的二叉查找树,普通的二叉查找树的生长是自顶向下的。
插入情景4.2:叔叔结点不存在或为黑结点,并且插入结点的父亲结点是祖父结点的左子结点
单纯从插入前来看,也即不算情景4.1自底向上处理时的情况(当自底向上处理时,可能出现的),叔叔结点非红即为叶子结点(Nil)。因为如果叔叔结点为黑结点,而父结点为红结点,那么叔叔结点所在的子树的黑色结点就比父结点所在子树的多了,这不满足红黑树的性质5。后续情景同样如此,不再多做说明了。
前文说了,需要旋转操作时,肯定一边子树的结点多了或少了,需要租或借给另一边。插入显然是多的情况,那么把多的结点租给另一边子树就可以了。
插入情景4.2.1:插入结点是其父结点的左子结点
处理: 黑叔叔 + LL –> 右旋 + 变色
- 将P设为黑色
- 将PP设为红色
- 对PP进行右旋
由图可得,左边两个红结点,右边不存在,那么一边一个刚刚好,并且因为为红色,肯定不会破坏树的平衡。
咦,可以把P设为红色,I和PP设为黑色吗?答案是可以!看过《算法:第4版》的同学可能知道,书中讲解的就是把P设为红色,I和PP设为黑色。但把P设为红色,显然又会出现情景4.1的情况,需要自底向上处理,当为后续再插入提供了便利,所以都可以。
插入情景4.2.2:插入结点是其父结点的右子结点
这种情景显然可以转换为情景4.2.1,不做过多说明了。
处理: 黑叔叔 + LR –> 左旋 + 右旋 + 变色
- 对P进行左旋
- 把P设置为插入结点,得到情景4.2.1
- 进行情景4.2.1的处理
插入情景4.3:叔叔结点不存在或为黑结点,并且插入结点的父亲结点是祖父结点的右子结点
该情景对应情景4.2,只是方向反转,不做过多说明了, RR 和 RL
好了,讲完插入的所有情景了。可能又同学会想:上面的情景举例的都是第一次插入而不包含自底向上处理的情况,那么上面所说的情景都适合自底向上的情况吗?答案是肯定的。理由很简单,但每棵子树都能自平衡,那么整棵树最终总是平衡的。
总结就是,若S节点为红色,则进行一次变色即可。若为黑色(或不存在)主体为基于PP节点的一次旋转+变色(P变黑、PP变红)(若子树分支不对,可能要先基于P节点旋转一下,P和I以标识的位置,旋转后原来的P变成I,原来的I变成P)(这里PP表示祖父节点、P表示父节点、S表示叔节点)
节点删除
红黑树插入已经够复杂了,但删除更复杂,也是红黑树最复杂的操作了。
红黑树的删除操作也包括两部分工作:一查找目标结点;而删除后自平衡。查找目标结点显然可以复用查找操作,当不存在目标结点时,忽略本次操作;当存在目标结点时,删除后就得做自平衡处理了。删除了结点后我们还需要找结点来替代删除结点的位置,不然子树跟父辈结点断开了,除非删除结点刚好没子结点,那么就不需要替代。
二叉树删除结点找替代结点有3种情情景:
- 情景1:若删除结点无子结点,直接删除
- 情景2:若删除结点只有一个子结点,用子结点替换删除结点
- 情景3:若删除结点有两个子结点,用后继结点(大于删除结点的最小结点)(可后继也可前驱)替换删除结点
补充说明下,情景3的后继结点是大于删除结点的最小结点,也是删除结点的右子树种最左结点。那么可以拿前继结点(删除结点的左子树最右结点)替代吗?可以的。但习惯上大多都是拿后继结点来替代,后文的讲解也是用后继结点来替代。
接下来,讲一个重要的思路:删除结点被替代后,在不考虑结点的键值的情况下,对于树来说,可以认为删除的是替代结点!话很苍白,我们看图。在不看键值对的情况下,图中的红黑树最终结果是删除了Q所在位置的结点!这种思路非常重要,大大简化了后文讲解红黑树删除的情景!
基于此,上面所说的3种二叉树的删除情景可以相互转换并且最终都是转换为情景1!
- 情景2:删除结点用其唯一的子结点替换,子结点替换为删除结点后,可以认为删除的是子结点,若子结点又有两个子结点,那么相当于转换为情景3,一直自顶向下转换,总是能转换为情景1。(对于红黑树来说,根据性质5.1,只存在一个子结点的结点肯定在树末了)
- 情景3:删除结点用后继结点(肯定不存在左结点),如果后继结点有右子结点,那么相当于转换为情景2,否则转为为情景1。
二叉树删除结点情景关系图如图所示。
综上所述,删除操作删除的结点可以看作删除替代结点,而替代结点最后总是在树末。有了这结论,我们讨论的删除红黑树的情景就少了很多,因为我们只考虑删除树末结点的情景了。
同样的,我们还是来约定下,如图所示。
图中的字母并不代表结点Key的大小。R表示替代结点,P表示替代结点的父结点,S表示替代结点的兄弟结点,SL表示兄弟结点的左子结点,SR表示兄弟结点的右子结点。灰色结点表示它可以是红色也可以是黑色。
删除情景1:替换结点是红色结点
我们把替换结点换到了删除结点的位置时,由于替换结点是红色,删除也了不会影响红黑树的平衡,只要把替换结点的颜色设为删除的结点的颜色即可重新平衡。
处理:颜色变为删除结点的颜色
删除情景2:替换结点是黑结点
当替换结点是黑色时,我们就不得不进行自平衡处理了。我们必须还得考虑替换结点是其父结点的左子结点还是右子结点,来做不同的旋转操作,使树重新平衡。
删除情景2.1:替换结点是其父结点的左子结点
删除情景2.1.1:替换结点的兄弟结点是红结点
若兄弟结点是红结点,那么根据性质4,兄弟结点的父结点和子结点肯定为黑色,不会有其他子情景,我们按图处理。
处理:
- 将S设为黑色
- 将P设为红色
- 对P进行左旋,得到情景2.1.2.3
- 进行情景2.1.2.3的处理(P染黑、SL染红)
方法二:(两次染色)
- 将S设为黑色
- 将SL设为红色
- 对P左旋即可
删除情景2.1.2:替换结点的兄弟结点是黑结点
当兄弟结点为黑时,其父结点和子结点的具体颜色也无法确定(如果也不考虑自底向上的情况,子结点非红即为叶子结点Nil,Nil结点为黑结点),此时又得考虑多种子情景。
删除情景2.1.2.1:替换结点的兄弟结点的右子结点是红结点,左子结点任意颜色
即将删除的左子树的一个黑色结点,显然左子树的黑色结点少1了,然而右子树又又红色结点,那么我们直接向右子树“借”个红结点来补充黑结点就好啦,此时肯定需要用旋转处理了。如图所示。
处理:(三次染色)
- 将S的颜色设为P的颜色
- 将P设为黑色
- 将SR设为黑色
- 对P进行左旋
平衡后的图怎么不满足红黑树的性质?图中是考虑到第一次替换和自底向上处理的情况,如果只考虑第一次替换的情况,根据红黑树性质,SL肯定是红色或为Nil,所以最终结果树是平衡的。如果是自底向上处理的情况,同样,每棵子树都保持平衡状态,最终整棵树肯定是平衡的。后续的情景同理,不做过多说明了。
删除情景2.1.2.2:替换结点的兄弟结点的右子结点为黑结点,左子结点为红结点(这种SR一定是空的。即SR为空,SL为红的情况,这时染色后对S右旋是比较常见的处理方式)
兄弟结点所在的子树有红结点,我们总是可以向兄弟子树借个红结点过来,显然该情景可以转换为情景2.1.2.1。如图所示。
处理:(两次染色。转为另一情景)
- 将S设为红色
- 将SL设为黑色
- 对S进行右旋,得到情景2.1.2.1
- 进行情景2.1.2.1的处理 (P染黑、SL染成原P的颜色、S染黑、对P左旋)
删除情景2.1.2.3:替换结点的兄弟结点的子结点都为黑结点(这里SL和SR一定是空的)
好了,此次兄弟子树都没红结点“借”了,兄弟帮忙不了,找父母呗,这种情景我们把兄弟结点设为红色,再把父结点当作替代结点,自底向上处理,去找父结点的兄弟结点去“借”。但为什么需要把兄弟结点设为红色呢?显然是为了在P所在的子树中保证平衡(R即将删除,少了一个黑色结点,子树也需要少一个),后续的平衡工作交给父辈们考虑了,还是那句,当每棵子树都保持平衡时,最终整棵总是平衡的。
处理:
- 将S设为红色
- 把P作为新的替换结点
- 重新进行删除结点情景处理
- 在PP中,若P为红色,显然将P设为黑色,并将其子节点设为红色,即完成平衡;若P为黑色,问题转化为移除一个黑色节点P,PP如何平衡的问题,这就是情景2了
删除情景2.2:替换结点是其父结点的右子结点
好啦,右边的操作也是方向相反,不做过多说明了,相信理解了删除情景2.1后,肯定可以理解2.2。
总体上,删除比插入要复杂,但也还好。
针对二叉平衡树,插入平衡有LL(右旋)、 RR(左旋)、 LR(先左旋再右旋)、 RL(先右旋再左旋) 四种,红黑树也类似,只是加上了变色