红黑树插入删除的个人理解笔记

本文参考:1.【数据结构】史上最好理解的红黑树讲解,让你彻底搞懂红黑树
2.红黑树详解
3.《算法导论(第3版)》
4.平衡树系列(1)——红黑树及其插入
5.平衡树系列(2)——红黑树的删除

一.红黑树特性

红黑树

一棵红黑树是满足下列红黑性质的二叉搜索树:

  1. 每个结点或是红色的,或是黑色
  2. 根结点是黑色
  3. 每个叶子结点(null结点)是黑色
  4. 如果一个结点是红色的,则它的两个子结点都是黑色
  5. 对每个结点,从该结点到其所有后代叶结点的简单路径上,都包含相同数目的黑色结点

我个人简记为:红或黑根为黑叶为黑红则黑路同黑

第4条特性可以推出两个延伸结论:

  • 红色结点的父结点也为黑色(反证:设红色的本结点为A,其父结点为B。如果父结点B为红色,则这个红色父结点B有一个红色的子结点A,违反特性四)
  • 从根结点到叶子结点的所有路径上不会存在2个连续的红色结点

自然,黑色结点的父子结点是红色黑色都可以

以下画图非必要的话都省略黑色叶子结点(null结点)

二.一些杂项小点

  • 黑高(black height,bh):从某个结点 x 出发(不含该结点)到达一个叶结点的任意一条简单路径上的黑色结点个数称为该结点的黑高。比如第一部分图中55结点的黑高为2。
  • 结点的结构:一般含有关键字value,左孩子left,右孩子right,父结点parent,颜色color这几个属性,颜色color属性值只有红RED、黑BLACK两个。为了简便,value一般使用int型值进行说明,实际中只要能比较大小的各种类型都可做关键字
enum class RBColor { RED, BLACK };

class RBTreeNode {
public:
    int key;
    RBTreeNode* p;
    RBTreeNode* left;
    RBTreeNode* right;
    RBColor color;
    // 其他添加属性、函数等
};
  • 后面章节的伪代码截取自《算法导论(第3版)》,红黑树结构如下。其中根结点的父结点也设为null结点(T.nil),指向的null叶子结点统一指向T.nil结点(为了节约空间),T.nil结点颜色为黑色,父结点、子结点、关键字值为任意,代码中并不判断这些属性
    算法导论红黑树

三.红黑树的旋转

  • 左旋、右旋
    左旋是“向左”从父结点降为(左)子结点,右旋是“向右”从父结点降为(右)子结点
    对于所关注的结点X、Y与子树α、β、γ来说,始终保持关键字 α ≤ X ≤ β ≤ T ≤ γ \alpha \leq X \leq \beta \leq T \leq \gamma αXβTγ,所以有些父子关系会有变化。右旋就是 Y 从 X 的父亲变为X的右孩子,X 接替了 Y 的父结点的孩子结点位置,β 从 X 的右子树变为 Y 的左子树,左旋同理。旋转之后,左旋是最左侧支(α支)增加了一个结点(Y),中间支(β支)结点数没变,只是连接关系变了,最右支(γ支)减少了一个结点(X)
    左旋、右旋

四.红黑树的插入

插入一个结点首先按照一般二叉搜索树的规则,找到要插入结点的位置,成为某个结点的子结点去替代原来的null叶子结点,并修改相应的父子关系指针。但是红黑树要对一个结点染色,对插入的新结点,统一先设为红色,然后再进行树的调整。因为插入一个红色结点并不会改变经过该结点的路径的黑高,所以性质1 2 3 5自动满足(插入时需要将该结点的子结点设为黑色null叶子结点,所以满足性质3),而性质4可能因为插到红结点下而违反,此时进行树的调整。

插入伪代码(算法导论第3版pp.178):
插入伪代码

情况1:结点 z 插到某个黑结点 X 下面

插到黑结点 X 下面有四种情况

  • 成为有一个孩子的黑结点(45结点、76结点)的左孩子(情况1.1)、右孩子(情况1.2)

  • 成为没有孩子的黑结点(88结点)的左孩子(情况1.3)、右孩子(情况1.4)

因为是插到黑色结点下,并没有违反性质4,所以不需要做多余工作
情况1

情况2:结点 z 插到某个红结点 X 下面,z 的叔结点为黑色

  • z 为 X 的左孩子,X 为 X 的父结点(z 的祖父结点)的左孩子(情况2.1)
  • z 为 X 的右孩子,X 为 X 的父结点(z 的祖父结点)的右孩子(情况2.2)
  • z 为 X 的左孩子,X 为 X 的父结点(z 的祖父结点)的右孩子(情况2.3)
  • z 为 X 的右孩子,X 为 X 的父结点(z 的祖父结点)的左孩子(情况2.4)

因为情况2.2、2.4分别与2.1、2.3镜像对称,此处只分析2.1和2.3。
情况2


在2.1中新插入的红色60结点有红色72父结点,破坏了红黑树性质4,需要对这一局部进行调整。

如果是将60染黑,则从根结点到60结点路径的黑高与其他路径不同,需要对其他路径也做调整,工作量过大。因此只能将72染黑。这样之后72结点往下是黑高平衡的,但是加上76也是黑的,如果从80结点往80-76-72-60-null走那就bh(80)=3,而72染色之前bh(80)=2,黑色太多了,所以把76染成红的,恢复到bh(80)=2。(注:并没有bh()=…的写法,此处为了简便而写)

这一染色带来了新的问题,右边80-76-null这一路的bh(80)=1,黑高变低了,那就来一个妙计——旋转,对76结点右旋,这样72结点旋上去,76结点旋成72的右孩子。左侧80-72-60-null黑高没变bh(80)=2,因为旋走的是红结点;而右侧走80-72-76-null的黑高bh(80)=2,旋上来的72结点是黑色,增加了黑高,整个树就平衡了。

情况2.1


在2.3中新插入的红色48结点有红色50父结点,也破坏了红黑树性质4,需要对这一局部进行调整。

如果像2.1一样直接染色然后旋转,即45染红、50染黑,45左旋,那么右侧38-50-null的黑高bh(36)=2,左侧同样bh(36)=2,但是还是出现了45-48的两连红结点,直接成了最初结构的镜像了,相当于啥也没做,所以这样做不合适。

因此需要采取另外的策略:先旋转一次。将50结点右旋,形成的结构就很像情况2.1了,只是结构上是个镜像对称。将插入的48看做原来存在的结点,而50看做插入的子结点,然后照2.1情况染色旋转就可以了。

情况2.3

上面两种情况都是叔结点为黑色,不同点在于插入结点和插入结点的父结点,各自相对于其父结点的是否是相同方向的孩子。情况2.1就是60是72的左孩子,72是76的左孩子,都是左孩子。情况2.2是2.1的镜像。情况2.3中48是50的左孩子,而50是45的右孩子。情况2.4是2.3的镜像。

所以说总共做的操作,在不是相同方向孩子的时候再开始要多一次旋转,同方向孩子就直接染色旋转。

情况3:结点 z 插到某个红结点 X 下面,z 的叔结点为红色

  • z 为 X 的左孩子,X 为 X 的父结点(z 的祖父结点)的左孩子(情况3.1)
  • z 为 X 的右孩子,X 为 X 的父结点(z 的祖父结点)的右孩子(情况3.2)
  • z 为 X 的左孩子,X 为 X 的父结点(z 的祖父结点)的右孩子(情况3.3)
  • z 为 X 的右孩子,X 为 X 的父结点(z 的祖父结点)的左孩子(情况3.4)

情况3


在3.1中新插入的红色10结点有红色17父结点和33叔结点,破坏了红黑树性质4,需要对这一局部进行调整。

对于这种情况,像情况2一样染色旋转就不行了,会造成25-33的连续红结点。此时就直接地,将父结点17和叔结点33直接染黑,祖父25结点染红,就可以保证这一区域不破坏所有性质了。

祖父25节点染红后会导致从这里网上出现问题,因为原来这里是黑的,染成红的就有可能出现新的连续红结点,所以此时就需要将关注结点设为这个祖父结点,看25结点往上需要进行何种调整。现在这棵树上就是又形成了情况3.1,再调整也就是38、80染黑,55染红,关注结点继续向上移到55。《算法导论》中的树也贴在下面,这里就是情况3.1转为了情况2.4。(本文为了行文,使用的情况分类与算法导论中稍有不同,但考虑的情况相同)

情况3.1

算法导论中的树:
算法导论情况3.1

其余的情况3.2、3.3、3.4在采用3.1的调整方法后,原本插入的结点所处的境况并没有什么区别,父结点黑的,和自己的红色不会破坏性质4,黑高因为祖父也变了色所以和整棵树没有发生变化,插入结点和父结点的左右孩子关系也不会产生多余的操作,只是祖父结点需要当成新插入的结点看待,继续运行算法而已。总共做的操作就是,父结点叔结点染黑,祖父染红,指针z更新为指向祖父,开始下一轮循环。

算法伪代码

以上分析的三种大情况共12种小情况是 RB-INSERT-FIXUP 所做的工作。将算法导论中伪代码贴在下面,并根据本文所分情况加以说明。(算法导论第3版pp.178-179)

RB-INSERT-FIXUP

最后我尝试理解一下为什么主要看叔结点的颜色:

插入的时候,由于是插入 z 到原叶子(null)结点的位置,而原null结点为黑色,其父结点 X 并没有红黑色的限制。而设定插入的结点 z 为红色,所以其父结点 X 根据 X 原本颜色的不同,会进行一定的调整。

  • 如果父节点是黑色,啥也不用管。
  • 如果父结点是红色,子结点 z 插入后红色父结点 X 一定要变黑,或者进行父子关系转换之后变黑。而变黑之后从祖父到父亲这一支黑高增加了,所以祖父要变红,又导致祖父到叔叔这一支黑高减少了。所以要看叔叔的颜色是红还是黑。
  1. 如果是红,那就染黑,相当于祖父的黑色下沉到父亲和叔叔这一高度,与原来的黑高相同,万事大吉。
  2. 但是叔叔是黑色时候,没办法再染黑了,那就旋转一下,因为父亲黑色祖父红色,转掉祖父之后父亲到了祖父位置,父亲 - z 这一支转走了红色,黑高没变,而叔叔一支成了原父亲X - 原祖父 - 叔叔,转到顶的X恢复了这一支的黑高,也就完事了。

五.红黑树的删除

首先是一个替换算法。删除树中的一个结点要是不补上,这个分支就和断树枝一样掉下去了。补上的方式就是选择树中的另一个结点替换掉被删的结点,然后其他什么被删结点的子结点之类的再接上去。《算法导论》中替换算法的实现仅改变了一个父指针一个子指针,其他的父子指针指向不做改动,而是由删除算法来做。

替换伪代码(算法导论第3版pp.183):

RB-TRANSPLANT替换

删除的代码看起来比较麻烦,但是结构上与普通二叉树的删除相似。我个人感觉代码乱在除了给定要删除的结点的指针 z 外,还算法中还使用了结点指针 y、x 来追踪替换节点那一套。因为删除的时候拿掉了结点,可能会破坏红黑树的性质,产生父子红结点啊,黑高不相等啊之类的问题,需要这些临时指针来修复。删除算法思想是替换,不过我听过两种版本,一种是将 被替换的结点 与 替换上来的结点 的关键字值交换,然后删掉 替换上来的结点;另一种是《算法导论》中的,使用 替换上来的结点 代替 被替换的结点 的父子关系指针,然后删除 被替换的结点。我就直接按照《算法导论》中的伪代码做备注。

替换伪代码(算法导论第3版pp.183):
替换伪代码

有点难想象的 if-else 情况我做了几个图帮助理解,省略了与所关注区域不相关的结构以及实际的结点颜色

图△1

图△2

图△3

图△4

最后删除z的时候,从画的图可以看出来,z虽然还与树有连接,但是与整个树的枝干结构无关了,可以放心拿掉。这样看的话,删掉的是z,而y替换上去了(不过如果z只有0或1子结点时y==z,拿掉的z也是拿掉了y,但是不影响后面分析),相当于是把“y指向的结点所在的原本位置”(简称为“原位置”)的结点删掉了(这个“原位置”可以想象一个萝卜坑,坑所在的层级/位置没动,但是坑里面原来的东西拿掉了),y-original-color就是“原位置”结点的颜色。“原位置”结点拿掉之后,孩子x连到树上,并占据了这个“原位置”的坑。x为根的子树往下没有动,还保持红黑树性质,但是从x往上看,父结点、祖父结点……之类向下经过x的路径就有可能破坏红黑树性质,所以从x开始调整,依次往上调整

而且回到删除之前的状态看,“原位置”结点颜色如果是红色的话,以上图中表明不论如何,“原位置”的 父节点 和 子结点x 的颜色都是黑色(根据性质4),“相当于删除了‘原位置’的结点y”后即使x接上来也是黑色与黑色节点相接,所以根本不会出现问题,也就不用进入 RB-DELETE-FIXUP 调整操作。

删除操作的调整操作也分多种情况,相比于插入主要看叔结点来说,删除调整主要看兄弟节点与兄弟结点的孩子节点(侄子结点)情况。也就是以删除后,x 结点已经接到了树上之后,开始从 x 结点看起。而删除调整的主要原因是“原位置”结点的颜色是黑色,拿掉“原位置”结点并且 x 接上来之后,经过 x 往下的路径的黑高减少了1,于是要在 x 这个地方补一层黑色。《算法导论》中使用 y-original-color “下推”到 x 上来理解,然后说 x 的颜色根据原本 x 的颜色的不同,现在变成了“红黑色”、“双黑色”。不过我个人更喜欢想象成,x 指针附带了一层黑膜,但是这个黑膜只能贴在红结点上,没办法贴在黑色结点上,需要调整出一个红结点来让这个膜贴上去。或者是让这一支平白增加一个黑结点,也就不用贴膜了。

下面这些分析以 x 是其父结点的左孩子为例,右孩子情况的话镜像对称写法来写。

情况1:x 是红色

没啥好说的,本身就是红色,可以直接染黑(“贴膜”)上去,结束。(80结点可以为红色,因为 x 是替代上来的,之前80和76结点之间还有一个结点被拿掉了)
情况1

情况2:x 是黑色,兄弟 w 为黑色,w 的右孩子为红色

  • x 的父结点为黑色,兄弟 w 为黑色,w 的右孩子为红色,w 的左孩子为任意色(情况2.1)
  • x 的父结点为红色,兄弟 w 为黑色,w 的右孩子为红色,w 的左孩子为任意色(情况2.2)

其实不分情况也没问题,分出来只是说明起来好说。时刻记住是 x 这一支少了一个黑高,而需要 x 贴一个黑膜就行。

情况2.1
情况2.2

为了便于说明,下面用一个简写符号bn(black number)表示某一条路径上的黑色结点个数,以及color(x)表示x结点的颜色函数,取值如下。

c o l o r ( x ) = { 0 如 果 x 是 红 色 结 点 1 如 果 x 是 黑 色 结 点 color(x)=\begin{cases} 0 & 如果x是红色结点 \\ 1 & 如果x是黑色结点 \end{cases} color(x)={01xx

由于 x 支少了一个黑高,需要补一个黑高出来。因为兄弟节点 w 是黑色,所以可以从 x 和 w 都抽一层黑色出来,扔到 x 的父结点上。如果是情况2.2,那就把 x 的父结点染黑,x 的黑膜抽走了黑高恢复了,w 的黑色抽走了要染红(注意此时还没说到90结点的染色)。看起来似乎行了,但是 w 结点是变红了,其子结点怎么样呢?不管84怎么样,90是红的,这样88-90两个红结点不行,必须得再染。染88吗?最开始染色刚把它染红又转回去了,不行。那就染90为黑色。84先做观察。

回看染色前后各路径的黑结点个数,也就是从 x 的父结点的那个位置 的结点,到α、β、γ、δ、ε、ζ各个子树为止之间的黑结点个数,当然,调整的整个算法要保证调整前后各路径的黑高不变,或者整个树的黑高统一增减。

初始状态时80-76的bn=2(这里有一层 x 指针附带的黑膜),80-88-84的bn=1+color(84),80-88-90的bn=1;染色后80-76的bn=2,80-88-84的bn=1+color(84),80-88-90的bn=2,出现问题了。80-88-90支的bn变多了,需要变回去。怎么办呢?旋转。绕 x 的父节点转一下,变成图上结尾情况。旋走了黑色,旋上了红色,这样88-80-76的bn=2,88-80-84的bn=1+color(84),88-90的bn=1,回到了初始状态,完美。

如果是情况2.1,x的父节点已经是黑的了,没法从 x 和 w 都抽一层黑色出来,染到 x 的父结点上。那可以旋转一下(想象情况2.1图中结尾时,90结点还没染黑的图形)。分析旋转前后,80-76和88-80-76的bn=3,80-88-84和88-80-84的bn=2+color(84),bn状态没变。而80-88-90的bn=2,88-90的bn=1,少了一个,正好90还是红色,给它染黑,也解决了。

总结上面两种情况,90结点都染黑了,也有一个同方向旋转操作(这里是左旋),x 的父节点染成了黑,w 结点染成了 x的父节点 的颜色(x的父节点为黑时保持了黑色,也可以认为是染成了父节点颜色)。所以总共做的操作就是 w 结点染 x的父节点 的颜色,x 的父节点染黑(或者说是 x的父节点 和 w 颜色互换),w 的右子结点染黑,绕 x 的父节点旋转。

情况3:x 是黑色,兄弟 w 为黑色,w 的右孩子为黑色,w 的左孩子为红色

回想一下情况2中,与情况3的比较,为什么都是 w 有红色孩子,但是这边要单独分一个情况出来。因为在情况2中最后有一次旋转(这里是左旋)操作,最右侧一支需要通过把 w 的右孩子染黑来保证黑高。不过现在 w 的右孩子是黑色,没法再染色了,而左孩子是红色,可以看看能不能从左边把这个红色借过来染色。

情况3

从前面分析中已经可以得出一定的结论来了,要借一个结点来,只能是旋转操作。剩下就是颜色问题。如果先不变色,那么80-76旋转前后根本没动到这里,bn来说虽然 x 还有一层黑膜,但是这个旋转前后没变,先放着以后再来处理。80-88-84-γ 一支变成了80-84-γ,不染色的话这边少了1个bn。80-88-84-δ变为80-84-88-δ,bn没变。80-88-90变为80-84-88-90,bn没变。另外注意γ、δ原来接在84下面,所以一定是黑色的子树根。

虽然借来了红色结点,但是有一支少了一个bn,那就染色吧。观察可以发现,没染色时84为红,88为黑,84-88再往下的路径bn都没变,所以可以把子结点的黑色挪一下到父结点上,也就是把84染成黑的,88染红,这样84-γ就增加了一个bn,回到旋转前的bn状态,84-88支路只是换了换颜色,并且也没破坏性质4。

借出来一个红结点之后,w 是黑色,w 的右孩子是红色,左孩子实际上是黑色,正好满足情况2,于是重新循环,后面就按情况2来走。

这种情况也就是,x 的兄弟结点 w 染红,w 的左子结点染黑,旋转 w,做一个循环再进来走情况2的调整方式。

情况4:x 是黑色,兄弟 w 为黑色,w 的右孩子为黑色,w 的左孩子为黑色

  • x 的父结点为黑色,兄弟 w 为黑色,w 的右孩子为黑色,w 的左孩子为黑色(情况4.1)
  • x 的父结点为红色,兄弟 w 为黑色,w 的右孩子为黑色,w 的左孩子为黑色(情况4.2)

情况4.1
情况4.2

这种情况没办法了,兄弟的孩子也都是黑色的,没法借了,只能做一些冒险的事情,也就是——既然我少一层黑,那兄弟也少一层就好了。将兄弟 w 染红,然后 x 指向其父节点。也就是将 x 缺少一层黑色这个事情,变成以 x 的父结点为根的子树缺少一层黑色,这样其实以 x 的父结点为根的子树黑高平衡了,但是与整个树相比就少了一层黑高。把 x 挪到这个父节点上,就回到了起始情况——x 为根的树黑高平衡,但是少一层黑高。

情况4.1时,由于最初 x 的父节点为黑色,x 上溯,其父节点成为新的 x 结点时,就需要考虑这个新 x 结点与其兄弟结点的关系了,也就是再进循环,变成情况2、3、4、5。而情况4.2就比较简单了,变成了情况1,直接通过循环条件分流出来染黑就行。

所以,总共就做了很少的工作——兄弟结点 w 的染色,然后上溯,再重新进循环来判断。

情况5:x 是黑色,兄弟 w 为红色

其实兄弟结点 w 为红色就限制了父结点、w 的子结点都是黑色。

情况5

这里看起来挺像情况4.1的中间状态,但是其实并不一样。4.1中 w 支少了一层黑高,而这里 w 支是黑高平衡的,所以并不能按情况4一样下一步 x 上溯。

这样的话可以借鉴情况3的思想,右边有红结点,通过旋转借一个过来染黑或者什么的。如果也只是绕 x的父节点 旋转(左旋),分析各路径的bn数可以看到,80-88-90变为88-90,bn数少了1,所以像情况3一样,从下面拿一个给色上来,也就是80染红,88染黑,所有路径就都恢复初始状态了。然后,情况就成了 x 有一个黑色兄弟结点,就可以做循环按照情况2、3、4来处理。

这种情况也就是,x 的兄弟结点 w 染黑,x 的父节点染红,旋转 x的父节点,做一个循环再进来走情况2、3、4的调整方式。

算法伪代码

以上分析的五种大情况是 RB-DELETE-FIXUP 所做的工作。将算法导论中伪代码贴在下面,并根据本文所分情况加以说明。(算法导论第3版pp.185):

RB-DELETE-FIXUP

最后也尝试总结一下为什么主要看兄弟以及兄弟孩子的颜色:

删除之后,x 这个子树黑高少了1,需要看看能不能找个红结点涂出黑色来。如果本身 x 就是红的,直接涂黑就行了。而如果是黑的,那就通过旋转父亲,从兄弟那里借一个红结点过来,甚至是兄弟结点的孩子节点也会在考虑范围内。毕竟转了 x 的父亲之后,x 这一支的结点增加了,可以找一个红结点涂黑,但是最右侧(或者镜像情况的最左侧)一支黑高可能减少了,于是也得看具体情况是否要加一句涂黑。有个特殊情况就是兄弟和其孩子都是黑的,如果再往其他范围考虑就得不偿失了,就直接把兄弟也削一层黑色,x 指针往上推,让新的 x 为根的子树考虑去吧。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值