树 Story —— 红黑树

1,103 阅读9分钟

红黑树(Red Black Tree) 是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现「关联数组」。

红黑树是在1972年由Rudolf Bayer发明的,当时被称为平衡二叉B树(symmetric binary B-trees)。后来,在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的“红黑树”。 [2]

红黑树是一种特化的 AVL 树(平衡二叉树),都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。 

它虽然是复杂的,但它的最坏情况运行时间也是非常良好的,并且在实践中是高效的: 它可以在O(log n)时间内做查找,插入和删除,这里的n 是树中元素的数目。

红黑树并不是平衡二叉树,而是使用染色的方式,让二叉查找树保证一定的平衡。红黑树和自平衡二叉(查找)树区别:

  1. 红黑树放弃了追求完全平衡,追求大致平衡,在与平衡二叉树的时间复杂度相差不大的情况下,保证每次插入最多只需要三次旋转就能达到平衡,实现起来也更为简单。
  2. 平衡二叉树追求绝对平衡,条件比较苛刻,实现起来比较麻烦,每次插入新节点之后需要旋转的次数不能预知。

红黑树除了继承了二叉查找树的原则外,还规定了以下几个约定:

  1. 每个节点要么是黑色,要么是红色。
  2. 根节点是黑色。
  3. 每个叶子节点是黑色(叶子节点为 nil 节点)。
  4. 每个红色结点的两个子结点一定都是黑色。
  5. 任意一结点到每个叶子节点的路径都包含相同数量的黑节点。

注:红黑树的所有叶子节点都为「黑色」的 nil 节点。图中实例部分省略了 nil 节点,因为画不下了。

插入节点

插入节点包含了四种情况。

  1. 空树

  2. 插入节点已存在

  3. 插入节点的父节点为黑色

  4. 插入节点的父节点为红色

    4.1 插入节点的叔节点为红色

    4.2 插入节点的叔叔节点为黑色

    4.3 插入节点没有叔叔节点

1. 空树

插入节点直接为「黑色」根节点。

2. 插入节点索引值已存在

使用新节点替换当前节点的数据即可,颜色不需要改变。

3. 插入节点的父节点为黑色

直接插入,插入节点颜色为「红色」。

插入节点 3,直接在节点 2 的右节点插入即可。

以上三点,插入都比较直接和简单,下面的第四点会更复杂一些。

4. 插入节点的父节点为红色

插入节点默认是红色,所以插入节点和父节点的颜色不符合「红色节点的两个子节点一定是黑色」的约定,那么我们需要进行节点变色或者旋转。而变色和旋转对应了如下几个场景。

红色的子节点不能是红色

4.1 插入节点的叔叔节点为红色

这时候,父节点和叔节点都为红色,祖父节点为黑色。为了保证祖父节点的两个子树的黑色节点路径数量一致性,我们需要将父节点和叔叔节点都置为「黑色」。而同时为了保证祖父节点所在子树和祖父同层其他子树的黑色路径数量一致,需要把祖父节点变为「红色」

节点 12 和 节点 17 变为黑色, 节点 15 变为 红色,红黑树保持平衡

如果祖父节点变为红色后,祖父的父节点也为红色,则将祖父节点视为新「插入节点」,向上递归平衡步骤。

4.2 插入节点的叔叔节点为黑色

如果插入节点的叔叔节点为黑色,而父节点为红色,则为了满足「每个红色结点的两个子结点一定都是黑色」,所以祖父节点一定不是红色。同时由于叶子节点 Nil 都为黑色,所以此情况同 4.3 没有叔叔节点。

4.3 插入节点没有叔叔节点

如果插入节点没有叔叔节点(或者叔节点为黑色),那么插入节点为红色,父节点也是红色,则需要通过「旋转」来使其满足「每个红色结点的两个子结点一定都是黑色」。如果插入节点为左子树的左节点(同平衡二叉树的 LL),那么需要「右旋」其祖父节点来平衡,如果插入节点为右子树的右节点(同平衡二叉树的 RR),则需要「左旋」。

1)「右旋」祖父节点

2)将父节点置为**「黑色」**

3)将原祖父节点置为「红色」

插入节点 0.5,则右旋起祖父节点

而如果插入节点为左子树的右节点(同平衡二叉树的 LR),那么先需要「左旋」父节点,使其符合 LL,再「右旋」原祖父节点来平衡。如果插入节点为右子树的做节点(同平衡二叉树的 RL),则需要「右旋」父节点,使其符合 RR,再「左旋」原祖父节点。

1)「左旋」父节点,再进行 LL 情形下的旋转

插入节点 1.5,则左旋父节点,再右旋其原祖父节点,连续两次旋转,并重置颜色

删除节点

删除节点分为两个步骤:

步骤 1 使用「二叉查找树」的删除节点方法,找到需要被删除的节点的替换节点,然后将要删除的节点与替换节点交换(对应位置的颜色「不」交换)。

在二叉查找树中,删除节点 5,则找到后继节点 6 ,替换后节点 6 在原来节点 5 的位置,删除交换后的节点 5

步骤 2 需要将替换后的要删除节点删除,再对树进行平衡。

替换节点有可能是红色,也可能是黑色,两种颜色对应的策略不同。删除红色节点相对简单,因为不涉及黑色节点路径一致问题。如果删除的是黑色节点,则一定要进行再平衡。

替换节点为红色

如果替换节点(颜色不变,数据为要删除的节点)颜色为「红色」,且替换节点的子节点都是叶子节点(nil),可以直接删除。

替换节点为黑色

但是如果替换节点颜色为**「黑色」**,那么其「兄弟节点一定是黑色的」。否则黑色节点路径将不一致。并且当替换节点为「黑色」,且兄弟节点为「黑色」的时候,「兄弟节点的子节点只能是红色(非 nil)」,否则黑色节点路径不一致。

当替换节点为黑色,对应有如下两种情况(以替换节点在左子节点为例,与右子节点相互对称):

  1. 兄弟节点有红色右子节点
  2. 兄弟节点「只」有红色左子节点
  3. 兄弟节点没有子节点

1. 兄弟节点有红色右子节点

假设我们删除节点 2

先找到替换节点 2.5

利用二叉查找树的原理,找到节点 2 的后继节点 2.5,并交换数据,为删除节点 2 做准备。替换节点 2 的兄弟节点 7 为黑色,并且兄弟节点的右子节点 8 为红色。

进行颜色更改和旋转:

1)将兄弟节点 7 置为父节点 5 颜色,这里为「红色」

2)将父节点 5 颜色置为**「黑色」**

3)将兄弟节点 7 的右子节点 8 置为**「黑色」**

4)「左旋」父节点 5

5)删除替换节点 2

简单一句话来说:替换节点的父节点和兄弟节点的右子节点置为「黑色」,兄弟节点置为「原父节点颜色」,并「左旋」父节点。

最终得到一个新的红黑树

2. 兄弟节点「只」有红色左子节点

1)找到替换节点 2.5

2)将兄弟节点置为「红色」,兄弟节点左子节点置为「黑色」

3)「右旋」兄弟节点

这样我们得到了同情况 1 的场景:兄弟节点有右子节点。这样按照情况 1 进行颜色跳转和旋转即可平衡。

3. 兄弟节点没有子节点

此情况比较特殊,由于兄弟节点没有红色节点可借,所以直接把兄弟节点置为红色,并设置父节点为新的「替换节点」,向上递归。

由于节点 15 的兄弟节点 18 为黑色,且没有红色的子节点,则将兄弟节点 18 置为红色,并将父节点 17 设置为新的「替换节点」身份。

以此类推,节点 17 同样遇到兄弟节点 11 没有红色子节点的情况,将兄弟节点 11 置为红色,并将父节点 16 置为新的「替换节点」。

由于红黑树是自下而上的自平衡,且红色节点的父子节点不能是红色。所以将节点 16 置为黑色。

此时红黑树达到平衡状态。

不管是删除还是插入,红黑树的平衡都是自下而上,保证子树是平衡的,那么整棵树就是平衡的。而删除节点时,中心思想是删除红色节点并不影响黑色节点路径,所以与二叉查找树类似,找到替换节点(只替换数据,不替换颜色)后,如果替换节点为红色,则直接删除即可。

但是假如找到的替换节点为黑色,则删除替换节点后,红黑树不能保持平衡,需要想兄弟节点「借」一个红色节点,但是当兄弟子节点不是红色,且没有红色子节点的时候,只能将兄弟节点置为红色。然后将平衡的问题向父级节点抛出。

插入练习题

1. 插入节点 10.5

2. 插入节点 0.5

3. 插入节点 0

5. 插入节点 1.3