本文部分代码和图来自大佬博客https://blog.csdn.net/v_JULY_v/article/details/6105630,向大佬致敬;
红黑树,一种二叉查找树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。
红黑树有五个性质:
性质1. 节点是红色或黑色。
性质2. 根节点是黑色。
性质3 每个叶节点(NIL节点,空节点)是黑色的。
性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
这五个性质对红黑树进行了约束,使它从根节点到最远的叶子节点的路径长与到最近的叶子节点的路径长度相差不会超过2。因此它是近似平衡的(并不是绝对平衡的)。
红黑树的插入
为了保证红黑树并不会整个全是黑色节点,我们都要默认初始的插入节点为红色,先让我们来看一看插入节点的伪代码:
RB-INSERT(T, z)
y ← nil
x ← T.root
while x ≠ T.nil
do y ← x
if z.key < x.key
then x ← x.left
else x ← x.right
z.p ← y
if y == nil[T]
then T.root ← z
else if z.key < y.key
then y.left ← z
else y.right ← z
z.left ← T.nil
z.right ← T.nil
z.color ← RED
RB-INSERT-FIXUP(T, z)
可以看到我们先找到要插入的父节点,然后按照要插入的节点与父节点的比较结果,将节点插在左子节点或者右子节点,然后为该节点赋予两个空子节点,并把改节点染成红色(如上所述),之后在对整棵树进行插入后的修复。
实际上当父节点是黑色节点的时候,我们无论如何插入一个红色节点都是不会出现违背规则的情况,所以不用进行修复。
而当父亲节点为红色的时候我们需要考虑三个情况:
● 插入修复情况1:如果当前结点的父结点是红色且祖父结点的另一个子结点(叔叔结点)是红色
● 插入修复情况2:当前节点的父节点是红色,叔叔节点是黑色,当前节点是其父节点的右子
● 插入修复情况3:当前节点的父节点是红色,叔叔节点是黑色,当前节点是其父节点的左子
让我们来看一下代码对这种情况是怎么处理的吧。
RB-INSERT-FIXUP(T, z)
while z.p.color == RED
do if z.p == z.p.p.left
then y ← z.p.p.right
if y.color == RED
then z.p.color ← BLACK ▹ Case 1
y.color ← BLACK ▹ Case 1
z.p.p.color ← RED ▹ Case 1
z ← z.p.p ▹ Case 1
else if z == z.p.right
then z ← z.p ▹ Case 2
LEFT-ROTATE(T, z) ▹ Case 2
z.p.color ← BLACK ▹ Case 3
z.p.p.color ← RED ▹ Case 3
RIGHT-ROTATE(T, z.p.p) ▹ Case 3
else (same as then clause with "right" and "left" exchanged)
T.root.color ← BLACK
如果当当前节点的父节点为红色:
情况1:如果当前结点的父结点是红色且祖父结点的另一个子结点(叔叔结点)是红色
将父节点的颜色改成黑色,叔叔节点的颜色也改成黑色,将祖父节点的颜色改成红色。最后,将当前节点指向祖父节点,重复一次判断,祖父节点的父节点是否为红色,如果为红色属于需要修复的情况几,再重复执行代码。
如图我们插入了11节点,经过第一次修复,树变成了这样:
此时当前节点转移到了祖父节点15,此时15的父节点为红色,叔叔节点为黑色,并且他为25的左子,此时应当执行情况3的策略,我们等会再讲。
情况2:当前节点的父节点是红色,叔叔节点是黑色,当前节点是其父节点的右子
当前节点的父节点做为新的当前节点,以新当前节点为支点左旋。
在10节点上插入右子15节点,此时10的兄弟节点是空节点,空节点一定是黑色的,即叔叔节点为黑色。符合情况2,应当将当前节点变为10节点,然后左旋:
此时当前节点为10,10是15的左子,且叔叔节点为黑色,此时应该有情况3。
情况3:当前节点的父节点是红色,叔叔节点是黑色,当前节点是其父节点的左子
父节点变为黑色,祖父节点变为红色,在祖父节点为支点右旋。
我们从上面的图已经发现修复好情况2以后,场景转化为情况3,那么此时现将15变为黑色,将20变成红色,然后以20为支点右旋转。
旋转后25的左子变成了15(黑),而15(黑)有两个子节点10和20(都为红色)。如下图:
这棵树已经修复完成,因为当前节点为10的父节点15已经为黑色,不需要继续修复了。
红黑树的删除
"我们删除的节点的方法与常规二叉搜索树中删除节点的方法是一样的,如果被删除的节点不是有双非空子女,则直接删除这个节点,用它的唯一子节点顶替它的位置,如果它的子节点分是空节点,那就用空节点顶替它的位置,如果它的双子全为非空,我们就把它的直接后继节点内容复制到它的位置,之后以同样的方式删除它的后继节点,它的后继节点不可能是双子非空,因此此传递过程最多只进行一次。”
二叉树的删除方法我们已经提过了,这里稍微总结一下:
- 没有儿子,即为叶结点。直接把父结点的对应儿子指针设为NULL,删除儿子结点就OK了。
- 只有一个儿子。那么把父结点的相应儿子指针指向儿子的独生子,删除儿子结点也OK了。
- 有两个儿子。这是最麻烦的情况,因为你删除节点之后,还要保证满足搜索二叉树的结构。其实也比较容易,我们可以选择左儿子中的最大元素或者右儿子中的最小元素放到待删除节点的位置,就可以保证结构的不变。当然,你要记得调整子树,毕竟又出现了节点删除。习惯上大家选择左儿子中的最大元素,其实选择右儿子的最小元素也一样,没有任何差别,只是人们习惯从左向右。这里咱们也选择左儿子的最大元素,将它放到待删结点的位置。左儿子的最大元素其实很好找,只要顺着左儿子不断的去搜索右子树就可以了,直到找到一个没有右子树的结点。那就是最大的了。
然后我们直接来看红黑树删除的伪代码:
1 if left[z] = nil[T] or right[z] = nil[T]
2 then y ← z
3 else y ← TREE-SUCCESSOR(z)
4 if left[y] ≠ nil[T]
5 then x ← left[y]
6 else x ← right[y]
7 p[x] ← p[y]
8 if p[y] = nil[T]
9 then root[T] ← x
10 else if y = left[p[y]]
11 then left[p[y]] ← x
12 else right[p[y]] ← x
13 if y ≠ z
14 then key[z] ← key[y]
15 copy y's satellite data into z
16 if color[y] = BLACK
17 then RB-DELETE-FIXUP(T, x)
18 return y
1. 如果被删除节点的左子为空或者右子为空(两个子节点至少有一个为空),就先把当前节点地址赋值给y。否则(两个节点都不为空的情况),对z进行处理TREE-SUCCESSOR(z)后的地址赋值给y。(具体怎么处理的并不知道。。)。
2. 如果是左子节点不为空,就将左子的父节点指向被删除的节点的父节点(即左子的祖父节点),如果左子为空,右子不为空,则将右子的父节点指向被删除的节点的父节点(即右子的祖父节点)。即如果有左子,则将左子设为删除节点的替换节点,如果没有左子,那么将右节点设为替换节点。
3. 如果被删除节点的父节点为空,则当前被删除的节点为根节点,那么将替换节点设为根节点。否则,如果当被删除的节点是左子,则将祖父节点的左子设为替换节点,如果当前被删除节点为右子,那么就将祖父节点的右子指向替换节点。其实到这步基本是上就已经
4. 如果被删除节点有两个子节点,那么把y的关键字赋值给当前节点(这步不知道做了什么事情,有了解的亲可以教我)
5. 如被删除的节点颜色是黑色的,那么需要进行修复。
上面的部分基本上就是二叉树的删除,只是在删除的节点为黑节点时需要对红黑树进行修复,因为如果删除是红节点并不会影响红黑树规则。
“在删除节点后,原红黑树的性质可能被改变,如果删除的是红色节点,那么原红黑树的性质依旧保持,此时不用做修正操作,如果删除的节点是黑色节点,原红黑树的性质可能会被改变,我们要对其做修正操作。那么哪些树的性质会发生变化呢,如果删除节点不是树唯一节点,那么删除节点的那一个支的到各叶节点的黑色节点数会发生变化,此时性质5被破坏。如果被删节点的唯一非空子节点是红色,而被删节点的父节点也是红色,那么性质4被破坏。如果被删节点是根节点,而它的唯一非空子节点是红色,则删除后新根节点将变成红色,违背性质2。!
那么我们来看修复代码:
1 while x ≠ root[T] and color[x] = BLACK
2 do if x = left[p[x]]
3 then w ← right[p[x]]
4 if color[w] = RED
5 then color[w] ← BLACK ▹ Case 1
6 color[p[x]] ← RED ▹ Case 1
7 LEFT-ROTATE(T, p[x]) ▹ Case 1
8 w ← right[p[x]] ▹ Case 1
9 if color[left[w]] = BLACK and color[right[w]] = BLACK
10 then color[w] ← RED ▹ Case 2
11 x ← p[x] ▹ Case 2
12 else if color[right[w]] = BLACK
13 then color[left[w]] ← BLACK ▹ Case 3
14 color[w] ← RED ▹ Case 3
15 RIGHT-ROTATE(T, w) ▹ Case 3
16 w ← right[p[x]] ▹ Case 3
17 color[w] ← color[p[x]] ▹ Case 4
18 color[p[x]] ← BLACK ▹ Case 4
19 color[right[w]] ← BLACK ▹ Case 4
20 LEFT-ROTATE(T, p[x]) ▹ Case 4
21 x ← root[T] ▹ Case 4
22 else (same as then clause with "right" and "left" exchanged)
23 color[x] ← BLACK
“上面的修复情况看起来有些复杂,下面我们用一个分析技巧:我们从被删节点后来顶替它的那个节点开始调整,并认为它有额外的一重黑色。这里额外一重黑色是什么意思呢,我们不是把红黑树的节点加上除红与黑的另一种颜色,这里只是一种假设,我们认为我们当前指向它,因此空有额外一种黑色,可以认为它的黑色是从它的父节点被删除后继承给它的,它现在可以容纳两种颜色,如果它原来是红色,那么现在是红+黑,如果原来是黑色,那么它现在的颜色是黑+黑。有了这重额外的黑色,原红黑树性质5就能保持不变。现在只要恢复其它性质就可以了,做法还是尽量向根移动和穷举所有可能性。"--saturnman。
从代码中可以看到,如果删除的不是根节点,且节点颜色为黑色的时候,分两种大情况:
1. 如果是以下情况,恢复比较简单:
- a)当前节点是红+黑色
解法,直接把当前节点染成黑色,结束此时红黑树性质全部恢复。如下:
途中有两种情况符合 此要求
a. 删除5节点.此时兄弟节点为红色,删除5节点后,8节点顶上,然后8节点变为黑色。变化结束。
b. 为删除18节点,此时会红+黑,兄弟节点为红色,但是处理方法应该没什么不同
如图所示确实并没有不同之处。
- b)当前节点是黑+黑且是根节点, 解法:什么都不做,结束
如我们删除20节点,即把左子树最左边的数据提到根节点:
2. 比较复杂的是以下4种:
删除修复情况1:当前节点是黑+黑且兄弟节点为红色(此时父节点和兄弟节点的子节点分为黑)
删除修复情况2:当前节点是黑加黑且兄弟是黑色且兄弟节点的两个子节点全为黑色
删除修复情况3:当前节点颜色是黑+黑,兄弟节点是黑色,兄弟的左子是红色,右子是黑色
删除修复情况4:当前节点颜色是黑-黑色,它的兄弟节点是黑色,但是兄弟节点的右子是红色,兄弟节点左子的颜色任意
让我们来具体看一下伪代码,从代码中可以看出,当x不为根节点且颜色为黑时会进入修复循环(此时是已被删除节点已被替换节点替换,当前节点x实际上指的是替换节点)。
删除修复情况1:当前节点是黑+黑且兄弟节点为红色(此时父节点和兄弟节点的子节点分为黑)
解法:把父节点染成红色,把兄弟结点染成黑色,之后重新进入算法(我们只讨论当前节点是其父节点左孩子时的情况)。此变换后原红黑树性质5不变,而把问题转化为兄弟节点为黑色的情况(注:变化前,原本就未违反性质5,只是为了把问题转化为兄弟节点为黑色的情况)。 即如下图操作,当A为替换节点(我自己无法模拟,使用了别人的图):
变化后:
删除修复情况2:当前节点是黑加黑且兄弟是黑色且兄弟节点的两个子节点全为黑色。
解法:把当前节点和兄弟节点中抽取一重黑色追加到父节点上,把父节点当成新的当前节点,重新进入算法。(此变换后性质5不变),即调用RB-INSERT-FIXUP(T, z) 的第9-10行代码操作,如下:
if color[left[w]] = BLACK and color[right[w]] = BLACK
10 then color[w] ← RED ▹ Case 2
11 x ← p[x] ▹ Case 2
变换后:
删除修复情况3:当前节点颜色是黑+黑,兄弟节点是黑色,兄弟的左子是红色,右子是黑色。
解法:把兄弟结点染红,兄弟左子节点染黑,之后再在兄弟节点为支点解右旋,之后重新进入算法。此是把当前的情况转化为情况4,而性质5得以保持,即调用RB-INSERT-FIXUP(T, z) 的第12-16行代码,如下所示:
12 else if color[right[w]] = BLACK
13 then color[left[w]] ← BLACK ▹ Case 3
14 color[w] ← RED ▹ Case 3
15 RIGHT-ROTATE(T, w) ▹ Case 3
16 w ← right[p[x]] ▹ Case 3
变化前:
变化后:
删除修复情况4:当前节点颜色是黑-黑色,它的兄弟节点是黑色,但是兄弟节点的右子是红色,兄弟节点左子的颜色任意。
解法:把兄弟节点染成当前节点父节点的颜色,把当前节点父节点染成黑色,兄弟节点右子染成黑色,之后以当前节点的父节点为支点进行左旋,此时算法结束,红黑树所有性质调整正确,即调用RB-INSERT-FIXUP(T, z)的第17-21行代码,如下所示:
//调用RB-DELETE-FIXUP(T, x) 的第17-21行代码
17 color[w] ← color[p[x]] ▹ Case 4
18 color[p[x]] ← BLACK ▹ Case 4
19 color[right[w]] ← BLACK ▹ Case 4
20 LEFT-ROTATE(T, p[x]) ▹ Case 4
21 x ← root[T] ▹ Case 4
变化前:
变化后: