[日记]LeetCode算法·二十六——二叉树⑥ 红黑树(插入与删除,附图)

承接上一篇AVL树AVL树,红黑树相较于AVL树,就相当于完全二叉树相当于AVL树,如何在性能退化和维护成本之间做出CS中经典的trade-off

文章目录

  • 红黑树的概念
  • 红黑树查询效率
  • 红黑树的插入
    • 1 插入节点N为根节点
    • 2 插入节点N的父节点P为黑色
    • 3 N的父节点P为红色,且叔叔节点U也为红色
    • 4 父节点P为红色,叔叔节点U为黑色,P为左孩子,N为右孩子
    • 5 父节点P为红色,叔叔节点U为黑色,P为左孩子,N为左孩子
    • 红黑树插入总结
  • 红黑树的删除
    • 1 删除节点X为红色
    • 2 替换节点N为根节点
    • 3 S为黑,S右孩子为红,N为P的左孩子
    • 4 S为黑色,S左孩子为红,S右孩子为黑,N为P的做孩子
    • 5 S为红色,其余节点为黑色
    • 6 N、P、S、SL、SR全为黑
    • 7 P为红,N、S、SL、SR为黑
    • 删除总结
  • 总结

红黑树的概念

相较于AVL树,通过平衡因子维护一个绝对平衡的二叉树,红黑树采用不严格平衡,从而减少了对树结构的调整,在大量IO的情况下有着更加优秀的性能。
红黑树是根据以下的性质定义,实现自平衡

  1. 节点是红色或者黑色
  2. 根节点是黑色
  3. 叶节点是黑色(这里的叶节点表示nullptr节点,而非一般的叶节点)
  4. 每个红色节点的子节点都是黑色的(不能有连续的两个红色节点)
  5. 从任意节点到其每个叶子的简单路径都包含相同数量的黑色节点(黑高一致)

这里需要注意的是,红黑树的叶节点并非左右子树都为nullptr的节点,而是nullptr节点。
其中相对关键的性质是性质4性质5,通过这两条性质,我们就可以保证红黑树的搜索效率为O(logn)

红黑树查询效率

为何通过以上的5条性质就可以保证红黑树的性能不会退化严重,依然可以维持在O(logn),类似于AVL树,我们可以采用数学归纳法进行证明。
首先我们证明一个引理:
以任意节点 x 为根的子树中包含至少 2 b h ( x ) − 1 个内部节点 以任意节点x为根的子树中包含至少2^{bh(x)}-1个内部节点 以任意节点x为根的子树中包含至少2bh(x)1个内部节点 b h ( x ) 为 x 的黑高,即从节点 x 出发到达一个叶节点的任意一条简单路径上黑色节点的数量 ( 不包含 x 本身 ) bh(x)为x的黑高,即从节点x出发到达一个叶节点的任意一条简单路径上黑色节点的数量(不包含x本身) bh(x)x的黑高,即从节点x出发到达一个叶节点的任意一条简单路径上黑色节点的数量(不包含x本身)

  1. b h ( x ) = 0 bh(x)=0 bh(x)=0时, x x x为叶子结点,以 x x x为根节点的子树包含0个内部节点,结论成立。
  2. 考虑一个高度为正值且有两个子节点的内部节点 x x x。每个子节点的黑高为 b h ( x ) bh(x) bh(x) b h ( x ) − 1 bh(x)-1 bh(x)1,这取决于子节点本身是红色 b h ( x ) bh(x) bh(x)还是黑色 b h ( x ) − 1 bh(x)-1 bh(x)1。因此,以 x x x为根节点的子树,至少包含 2 b h ( x ) − 1 − 1 + 2 b h ( x ) − 1 − 1 + 1 = 2 b h ( x ) − 1 个节点 2^{bh(x)-1}-1+2^{bh(x)-1}-1+1=2^{bh(x)}-1个节点 2bh(x)11+2bh(x)11+1=2bh(x)1个节点,证明完毕。

另外根据性质4,我们可以知道: 从根节点到叶节点的任意一条简单路径上至少有 1 2 的节点为黑色节点 从根节点到叶节点的任意一条简单路径上至少有\frac{1}{2}的节点为黑色节点 从根节点到叶节点的任意一条简单路径上至少有21的节点为黑色节点即黑高至少为高度的一半,因此,假设一个最糟糕的红黑树,其节点数为n,高度为h,则黑高 b h ≥ 1 2 h bh\geq\frac{1}{2}h bh21h,根据引理,我们有 n ≥ 2 b h ( x ) − 1 ≥ 2 h 2 − 1 n\geq2^{bh(x)}-1\geq2^{\frac{h}{2}}-1 n2bh(x)122h1 故 h ≤ 2 l o g ( n + 1 ) 故h\le2log(n+1) h2log(n+1)因此红黑树的查询效率为O(logn)

红黑树的插入

红黑树查询与BST没有区别,在此关注红黑树的插入与删除,另外注明本文只关注节点N或父节点P位于左子树的情况,右子树的情况可以根据对称得到。
关于插入,我们可以明确的一点是,应该插入节点应为红色,因为插入红色节点不会影响性质5,即黑高不变
在此我们列出插入节点的若干种情况,以及对应的处理方法。

1 插入节点N为根节点

此时,我们只需要将插入节点进行染色,红→黑即可,如图所示

2 插入节点N的父节点P为黑色

此时我们不需要进行任何操作,因为插入红节点不影响性质5,且父节点P为黑色不影响性质4

3 N的父节点P为红色,且叔叔节点U也为红色

如图所示
[日记]LeetCode算法·二十六——二叉树⑥ 红黑树(插入与删除,附图)_第1张图片
此时违反了性质4,我们进行的处理如下:

  • 将P和U染为黑色
  • 将祖父节点G染为红色
  • 下一步关注组父节点G,进一步递归判断

之所以需要递归判断,是因为G的父节点可能为红色,再一次违反性质4

4 父节点P为红色,叔叔节点U为黑色,P为左孩子,N为右孩子

如图所示
[日记]LeetCode算法·二十六——二叉树⑥ 红黑树(插入与删除,附图)_第2张图片

注意,这里所对应的还有P为G的右孩子,N为P的左子树的对称情况,在此不再赘述。
我们对这种情况需要进行的处理为N、P左旋,下一步关注P节点。
此时左旋后,我们的红黑树依然违反了性质4,因此进一步关注节点P,并且通过情况5进行处理

5 父节点P为红色,叔叔节点U为黑色,P为左孩子,N为左孩子

如图所示
[日记]LeetCode算法·二十六——二叉树⑥ 红黑树(插入与删除,附图)_第3张图片

注意,这里所对应的还有P为G的右孩子,N为P的右孩子的对称情况,在此不再赘述。
此时我们的处理方法如下:

  1. PG右旋
  2. P染黑
  3. G染红

通过以上步骤,我们发现,各个点位的黑高都没有发生变化,且去除了相邻的红色节点,因此完成了红黑树的调整

红黑树插入总结

  1. 找到插入的位置,将红色新节点N插入
  2. 判断N是否是根节点,是的话,染为黑色,否则继续
  3. 判断N的父节点是否为黑色,是的话,返回,否则继续
  4. 判断N的叔叔节点是否为红色,是的话,将P、U染为黑,G染为红,N=G,进一步判断
  5. 判断N所在的分支是否和P所在的分支不一致,不一致的话,则进行旋转,上升N,下降P,并归为一边,继续判断
  6. 若P和N所在分支一致,则对P、G进行旋转,上升P,下降G,并交换P与G的颜色,返回

红黑树的删除

相比于插入,红黑树的删除也确实更加复杂。同样适用替换删除法,删除前驱或后继节点。
和AVL树一致,我们不关心最终删除的节点是否与想删除的一致,我们只关心被删除的节点,如果被删除的节点为红色,则较为简单,否则会比较复杂,我们将进一步的进行分析,同样地,以下分析依然基于N为左子树或P为左子树的情况,对称的情况不再赘述
以下各节点的名字所代表的含义分别如下:

  1. X为最终被删除的节点
  2. N为替换X的节点
  3. S为X的兄弟节点,替换后也就是N的兄弟节点
  4. SL为S的左节点,SR为S的右节点
  5. P为N的父节点
  6. G为P的父节点,N的组父节点

1 删除节点X为红色

此时直接删除节点X,并用黑色的N节点(必然是黑色)替换,则不会影响性质4与性质5,是最为简单的一种删除情况。

2 替换节点N为根节点

替换后节点N为根节点,此时我们将节点N染色为黑色,相当于所有节点都去出了一个黑色节点,黑高一致,完毕。

3 S为黑,S右孩子为红,N为P的左孩子

如图所示,其中蓝色的P节点与SL节点,代表这些节点可红可黑。
[日记]LeetCode算法·二十六——二叉树⑥ 红黑树(插入与删除,附图)_第4张图片

之所以上来就考察如此复杂的情况,是因为这种情况可以通过一定操作完成删除的调整。同理,可以适用于N为右孩子,S左孩子为红的对称情况
具体操作如下:

  1. P、S左旋
  2. P与S交换颜色
  3. SR染色为黑

让我们对删除前后的子树进行分析。

  • 我们假设, C ( x ) C(x) C(x)为判断节点x是否为黑色的函数,若为黑色,则 C ( x ) = 1 C(x)=1 C(x)=1,反之, C ( x ) = 0 C(x)=0 C(x)=0
  • 删除前, b h ( G → N ) = G → P → X → N = 2 + C ( P ) bh(G→N)=G→P→X→N=2+C(P) bh(GN)=GPXN=2+C(P) b h ( G → S L ) = G → P → S → S L = 1 + C ( P ) + C ( S L ) bh(G→SL)=G→P→S→SL=1+C(P)+C(SL) bh(GSL)=GPSSL=1+C(P)+C(SL) b h ( G → S R ) = G → P → S → S R = 1 + C ( P ) bh(G→SR)=G→P→S→SR=1+C(P) bh(GSR)=GPSSR=1+C(P)
  • 删除后, G → S L 与 G → S R G→SL与G→SR GSLGSR不变, G → N G→N GN改变为 1 + C ( P ) 1+C(P) 1+C(P)
  • 旋转后, b h ( G → N ) = G → S → P → N = 2 + C ( P ) bh(G→N)=G→S→P→N=2+C(P) bh(GN)=GSPN=2+C(P) b h ( G → S L ) = G → S → P → S L = 1 + C ( P ) + C ( S L ) bh(G→SL)=G→S→P→SL=1+C(P)+C(SL) bh(GSL)=GSPSL=1+C(P)+C(SL) b h ( G → S R ) = G → S → S R = 1 + C ( P ) bh(G→SR)=G→S→SR=1+C(P) bh(GSR)=GSSR=1+C(P),因为S和P的对调与颜色互换,使得经过SL的黑高不变的同时,N的黑高+1,恢复到了删除前。而通过对SR的染色,弥补了S染为 C ( P ) C(P) C(P)时的黑高减少,旋转前后根节点颜色不变,意味着不需要继续向上迭代
  • 旋转前后,针对之前的P和S来看,N侧的黑高+1

4 S为黑色,S左孩子为红,S右孩子为黑,N为P的做孩子

不再赘述对称情况,直接上图
[日记]LeetCode算法·二十六——二叉树⑥ 红黑树(插入与删除,附图)_第5张图片

操作如下:

  1. SL、S右旋
  2. SL染为黑色
  3. S染为红色
  4. 转入情况3处理

通过分析,旋转前后,不影响P到右子树任何叶节点的黑高。

5 S为红色,其余节点为黑色

如图所示
[日记]LeetCode算法·二十六——二叉树⑥ 红黑树(插入与删除,附图)_第6张图片

我们已经意识到在黑节点之中插入1个红节点,不会影响红黑树的定义,在这种情况下,我们面对S为红,其余为黑的操作如下:

  1. P、S左旋
  2. P染红
  3. S染黑
  4. 转入情况3情况4

6 N、P、S、SL、SR全为黑

如图所示
[日记]LeetCode算法·二十六——二叉树⑥ 红黑树(插入与删除,附图)_第7张图片

我们需要做的是将S染红将关注节点设为P
相比于删除前,N和S的黑高同时下降1,P的左右子树保持了一致,即P满足红黑树定义,但经过P的路径,将比不经过P的路径黑高小1这相当于P成为了替换节点,其父节点被删去的情况,因此根据P的父节点、兄弟节点进行进一步的判断,直到转入情况1或2或3。

7 P为红,N、S、SL、SR为黑

如图所示
[日记]LeetCode算法·二十六——二叉树⑥ 红黑树(插入与删除,附图)_第8张图片

此时我们交换P与S的颜色,S黑高减1,与N保持一致,相当于P的黑高减一与情况6染色后情况一致将关注节点设为P,进一步判断。

删除总结

分析了各种情况的删除,我们可以做一些简单的总结。

  • 情况1、2、3,可以通过一定的处理,完成对红黑树的调整
  • 情况4通过不改变黑高的方法,转换为了情况3
  • 情况5通过不改变黑高的方法,转化为了情况3或4
  • 情况6和7,通过改变另一侧的黑高,完成了P的子树调整,但过P和不过P产生了黑高的不一致,与P为替换节点的情况一致,需要对P进行进一步的判断
  • 以上操作都建立在P或N为左子树情况,右子树情况进行对称即可。

总结

峣峣者易折,皎皎者易污。这句话用来形容AVL树、红黑树、最佳排序树实在再合适不过。
最佳的性质最容易破坏而难以保持,且“最佳”往往是一种全局性质无法从局部进行把握和检查,一旦被破坏,也无法从局部调整恢复,这就是最佳排序树(这个的博客还没出)往往应用较少的原因。
AVL树和红黑树则都是在检索效率和动态操作之间做出了取舍,纵观两者,其搜索效率性能都发生了常数级的退化,但同时也支持了局部的动态调整操作,两者的插入与删除均维持在O(logn)。而红黑树则是对检索效率的进一步放弃以及更加复杂的算法复杂度,换取了更少的旋转操作,无论是C++STL中的map,还是Java中的TreeMap底层都是红黑树的实现。
——2023.5.18

你可能感兴趣的:(leetcode,笔记,算法,leetcode,计算机,二叉树,红黑树)