首页 > 技术文章 > 红黑树解读

coderD 2021-04-02 20:31 原文

解读红黑树

1、红黑树(Red Black Tree)

  • 红黑树也是一种自平衡的二叉搜索树

  • 红黑树必须满足一下5条性质

    1. 节点是RED或者BLACK

    2. 根节点是BLACK

    3. 叶子节点(外部节点,空节点)都是BLACK

    4. RED节点的子节点都是BLACK

      RED节点的parent都是BLACK

      从根节点到叶子节点的所有路径上不能有2个连续的RED节点

    5. 从任一节点到叶子节点的所有路径都包含相同数目的BLACK节点

以下即为一颗红黑树:

Snipaste_2021-03-27_15-58-43

2、红黑树的等价变换

Snipaste_2021-04-01_08-56-33 Snipaste_2021-04-01_08-57-06

如上图所示:

  • 红黑树和4阶B树(2-3-4树)具有等价性
  • BLACK节点与它的RED子节点融合在一起,形成1个B树节点
  • 红黑树的BLACK节点个数与4阶B树的节点总个数相等

2.1、红黑树 VS 2-3-4树

​ 上面的是红黑树,下面是与之等价的2-3-4树

Snipaste_2021-04-01_09-00-25

3、几个单词和一些辅助函数

  • parent:父节点
  • sibling:兄弟节点
  • uncle:叔父节点(parent的兄弟节点)
  • grand:祖父节点(parent的父节点)
// 染色
private Node<E> color(Node<E> node, boolean color) {
    if (node == null) return node;
    ((RBNode<E>)node).color = color;
    return node;
}
// 将该节点染为红色
private Node<E> red(Node<E> node) {
    return color(node, RED);
}
// 将该节点染为黑色
private Node<E> black(Node<E> node) {
    return color(node, BLACK);
}
// 返回该节点的颜色
private boolean colorOf(Node<E> node) {
    return node == null ? BLACK : ((RBNode<E>)node).color;
}
// 该节点是否为黑色
private boolean isBlack(Node<E> node) {
    return colorOf(node) == BLACK;
}
// 该节点是否为红色
private boolean isRed(Node<E> node) {
    return colorOf(node) == RED;
}

public Node<E> sibling(){
	if(isLeftChild()){
		return parent.right;
	}
	if(isRightChild()){
		return parent.left;
	}

	return null;
}

4、添加

  • 已知

    • B树中,新元素必定是添加到叶子节点中的
    • 4阶B树所有节点的元素个数x都符合1<=x<=3
  • 建议新添加的节点默认为RED,这样能让红黑树的性质尽快满足

    Snipaste_2021-04-01_09-25-19

  • 如果添加的是根节点,染成BLACK即可

4.1、添加的所有情况

  • 有4中情况满足红黑树的性质4:parent为BLACK

    • 同样也满足4阶B树的性质

    • 因此不用做任何处理

      如下图所示,如果我们要添加的是红色节点的话,又因为任何添加其实都是在叶子节点处添加,所以我们不需要对这种情况进行任何的处理,如图中的40,78,82,90都是这种情况

    image-20210402184517583

  • 有 8 种情况不满足红黑树的性质 4 :parent 为 RED( Double Red )

    • 其中前 4 种属于B树节点上溢的情况
    • 剩下的这8中情况就是我们需要进行处理的

Snipaste_2021-04-01_09-33-21

接下来我们对其进行一一解析

4.1.1、添加——修复性质4——LL\RR

​ 我们先来看这种情况,我们要在节点50的左边添加一个新的节点,或者我们要在72的右边添加一个新的节点。那么我们要做的事情很简单。我们知道新添加的这俩个节点,他们的父节点都是红色,这样的话,就会有连续的俩个节点都为红色了,那么就违背了红黑树的性质,所以我们要进行一定的调整。

判定条件:uncle(叔父节点)不是RED

​ 其实调整的过程非常的简单:

  • 我们只需要将新添加的节点的父节点染成黑色
  • 然后将新添加的祖父节点染成红色
  • 然后我们对新添加节点的祖父节点进行旋转即可

​ 也就是:

  1. parent染成BLACK,grand染成RED

  2. grand进行单选操作

    LL:右旋转

    RR:左旋转

Snipaste_2021-04-01_09-37-11

旋转后如图所示:

Snipaste_2021-04-01_09-42-53

4.1.2、添加——修复性质4——LR\RL

​ 我们再看这种情况,这种情况下,新添加的节点处于grand.right.left或者是grand.left.right,也就是我们下图所要添加的节点48或者节点74。对于这种情况,其实处理的方法也很简单,那就是我们要将新添加的节点染黑,然后将它的祖父节点染成红色,并且我们要进行旋转操作。对于下图添加节点48的情况,我们要将它的父节点进行右旋,然后将祖父节点进行左旋;对于下图中添加节点74的情况,我们只需要将它的父节点左旋,然后将它的祖父节点进行右旋就可以了。

判定条件:uncle不是RED

  • 自己染成BLACK,grand染成RED
  • 进行双旋操作
    • LR:parent左旋转,grand右旋转
    • RL:parent右旋转,grand左旋转

Snipaste_2021-04-01_09-43-48

旋转之后的样子:

Snipaste_2021-04-01_09-45-03

4.1.3、添加——修复性质4——上溢——LL

​ 对于这种情况,我们添加元素的时候,肯定会发出上溢的情况,这个时候我们就要将节点进行向上合并的操作。如下图我们要添加新的节点10的时候,我们就需要进行一系列的操作。

​ 首先,我们要将取出中间位置的节点向上合并,我们这里取节点25向上合并,做完这个操作之后,我们将25这个节点染成红色,作为是新添加的节点一样进行处理。然后因为25已经向上合并了,所以原先25节点的左子树和右子树分裂,所以我们要将新添加的节点的父节点,也就是下图的17节点染成黑色,然后我们将下图的33节点也染成黑色,因为现在的33节点也是一个叶子节点,我们知道红黑树里的叶子节点都是黑色的

判定条件:uncle是RED

  • parent、uncle染成BLACK

  • grand向上合并

    • 染成RED,当作是新添加的节点进行处理
  • grand向上合并时,可能继续发生上溢

  • 若上溢持续到根节点,只需将根节点染成BLACK

    Snipaste_2021-04-01_09-51-20

添加节点处理后如图所示:

Snipaste_2021-04-01_09-55-13

4.1.4、添加——修复性质4——上溢——RR

​ 对于这种情况,其实是和我们上面说的是相似的。对于我们要添加的新节点36来说,如果我们要添加新节点,那么必然会发生上溢的现象。所以我们同样的要选取中间位置的节点进行向上合并,这里我们取节点25进行向上合并,同样的,我们将节点25染成红色,当成新添加的节点进行处理。然后原来25节点的左右子树进行分裂。这个时候,我们应该将parent、uncle染成黑色。

判定条件:uncle是RED

  • parent、uncle染成BLACK
  • grand向上合并
    • 染成RED,当作是新添加的节点进行处理

image-20210402193121303

4.1.5、添加——修复性质4——上溢——LR

​ 在这里我就不一一进行赘述了,下面的情况大家可以自己进行理解

判定条件:uncle是RED

  • parent、uncle染成BLACK
  • grand向上合并
    • 染成RED,当做是新添加的节点进行处理

Snipaste_2021-04-01_10-00-41

4.1.6、添加——修复性质4——上溢——RL

判定条件:uncle是RED

  • parent、uncle染成BLACK
  • grand向上合并
    • 染成RED,当做是新添加的节点进行处理

Snipaste_2021-04-01_10-00-52

5、删除

  • B树中,最后真正被删除的元素都在叶子节点中

5.1、删除——RED节点

  • 对于这种情况,我们可以直接进行删除,不用做任何的调整

Snipaste_2021-04-02_09-11-20

5.2、删除——BLACK节点

  • 有3种情况

    • 拥有2个RED子节点的BLACK节点
      • 不可能被直接删除,因为会找它的子节点代替删除
      • 不用考虑这种情况
    • 拥有1个RED子节点的BLACK节点
    • BLACK叶子节点

    我们来说一说,为什么拥有2个RED子节点的BLACK节点的这种情况。如下图,假如我们要删除的是节点55,我们要处理的情况就是找到节点55的前驱结点或者后继节点进行替换节点55,然后将它的前驱结点或者后继节点删除。也就是说,如果我们要删除的节点是55的话,其实我们删除的就是它的前驱结点或者后继节点。

Snipaste_2021-04-02_09-14-23

5.2.1、删除——拥有1个RED子节点的BLACK节点

  • 判定条件:用以替代的子节点是RED
  • 将替代的子节点染成BLACK即可保持红黑树性质

我们来简单的叙述一下这种操作,如下图,我们要删除节点46和节点76,因为要替代他们俩位置的都是红色的子节点50和72,所以进行删除的时候我们只需要将俩个节点删除,然后将节点50和72染成黑色就好了

Snipaste_2021-04-02_09-17-28

操作之后的样子为:

Snipaste_2021-04-02_09-19-37

5.2.2、删除——BLACK叶子节点——sibling为BLACK

  • BLACK 叶子节点被删除后,会导致B树节点下溢(比如删除88)
  • 如果 sibling 至少有 1 个 RED 子节点
    • 进行旋转操作
    • 旋转之后的中心节点继承parent的颜色
    • 旋转之后的左右节点染为BLACK

image-20210402201658738

5.2.3、删除——BLACK叶子节点——sibling为BLACK

  • 判定条件:sibling没有1个RED子节点
  • 将sibling染成RED、parent染成BLACK即可修复红黑树性质
  • 如果parent是BLACK
    • 会导致parent也下溢
    • 这时只需要将parent当作被删除的节点处理即可

image-20210402202421726

5.2.4、删除——BLACK叶子节点——sibling为RED

  • 如果sibling是RED
    • sibling 染成 BLACK,parent 染成 RED,进行旋转
    • 于是又回到 sibling 是 BLACK 的情况

Snipaste_2021-04-02_11-00-33

6、红黑树的平衡

  • 相比AVL树,红黑树的平衡标准比较宽松:没有一条路径会大于其他路径的2倍
  • 是一种弱平衡、黑高度平衡
  • 红黑树的最大高度是 2 ∗ log2(n + 1) ,依然是 O(logn) 级别

7、平均时间复杂度

  • 搜索:O(logn)
  • 添加:O(logn),O(1) 次的旋转操作
  • 删除:O(logn),O(1) 次的旋转操作

8、AVL树 VS 红黑树

  • AVL树
    • 平衡标准比较严格:每个左右子树的高度差不超过1
    • 最大高度是 1.44 ∗ log2 n + 2 − 1.328(100W个节点,AVL树最大树高28)
    • 搜索、添加、删除都是 O(logn) 复杂度,其中添加仅需 O(1) 次旋转调整、删除最多需要 O(logn) 次旋转调整
  • 红黑树
    • 平衡标准比较宽松:没有一条路径会大于其他路径的2倍
    • 最大高度是 2 ∗ log2(n + 1)( 100W个节点,红黑树最大树高40)
    • 搜索、添加、删除都是 O(logn) 复杂度,其中添加、删除都仅需 O(1) 次旋转调整
  • 搜索的次数远远大于插入和删除,选择AVL树;搜索、插入、删除次数几乎差不多,选择红黑树
  • 相对于AVL树来说,红黑树牺牲了部分平衡性以换取插入/删除操作时少量的旋转操作,整体来说性能要优于AVL树
  • 红黑树的平均统计性能优于AVL树,实际应用中更多选择使用红黑树

推荐阅读