红黑树(Red–black tree)是一种自平衡二叉查找树,支持集合、查找、上一元素、下一元素、最大、最小、插入、删除等操作。

红黑树 有着最坏情况运行时间,它可以在 O(logn) 时间内做查找,插入和删除操作。

红黑树 是每个节点都带有颜色属性的二叉查找树,颜色为红色或黑色。它有以下性质和要求:

  • 1.树内的每个节点非红即黑。
  • 2.根节点和leaf节点(NIL节点,非叶节点)均为黑色。
  • 3.红色节点的子节点均为黑色。
  • 4.任一节点到其每个leaf节点的简单路径都包含相同数量的黑色节点。

Red-black_tree_example

红黑树的每个节点都必须满足上述的条件,其中叶节点的下一层—- NIL节点,称之为 外部节点,其余的称之为 内部节点

正因为 红黑树 拥有上述严格的条件,才能使得其 插入删除查找的操作在最坏的情况下的时间复杂度为 O(logn)

红黑树修正

在做 插入删除操作时,可能会违背 红黑树的上述规则,这时候需要对红黑树进行修正。

红黑树的修正方式有 变色左旋右旋

变色

但进行 插入删除操作时,如果违背的规则三,则需要对树内节点进行变换颜色的操作。黑节点 —> 红节点

左旋

进行左旋时,将子树进行逆时针旋转,保证前后的 先序遍历结果一致。

具体操作如下:

  • 1.将x的左子树赋值给h,作为h的右子树,同时指定x的左子树的父亲节点为h。
  • 2.将h的父节点(非空时)赋值给x,同时更新父节点的子节点为x。
  • 3.将h的父节点设置为x,x的左子节点设置为h。

left-rotation

如上所示,左旋之前子树的 先序遍历结果是 h.left –> h –> x.left –> x –> x.right。在经过 左旋操作后,结果仍是 h.left –> h –> x.left –> x –> x.right

伪代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
x = h.right;

// 1.将x的左子树赋值给h,作为h的右子树,同时指定x的左子树的父亲节点为h。
h.right = x.left;

if(x.left != NIL)
    x.left.parent = h;

// 2.将h的父节点(非空时)赋值给x,同时更新父节点的子节点为x。
x.parent = h.parent;

if(h.parent == NIL)
    root = x;
elif(h == h.parent.left)    // 如果父节点不为NIL,则根据h为左右子树的位置,确定x的位置
    h.parent.left = x;
else
    h.parent.right = x;shan chu

// 3.将h的父节点设置为x,x的左子节点设置为h。
h.parent = x;
x.left = h;

右旋

右旋的过程和左旋的正好相反。

  • 1.将x的右子树赋值给h,作为h的左子树,同时指定x的右子树的父节点为h。
  • 2.将h的父节点(非空时)赋值给x,同时更新父节点的子节点为x。
  • 3.将h的父节点设置为x,x的右子节点设置为h。

right-rotation

伪代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
x = h.right;

// 1.将x的右子树赋值给h,作为h的左子树,同时指定x的右子树的父节点为h。
h.left = x.right;

if(x.right != NIL)
    x.right.parent = h;

// 2.将h的父节点(非空时)赋值给x,同时更新父节点的子节点为x。
x.parent = h.parent;

if(h.parent == NIL)
    root = x;
elif(h.parent.left == h)
    h.parent.left = x;
else
    h.parent.right = x;

// 3.将h的父节点设置为x,x的右子节点设置为h。
h.parent = x
x.right = h

插入

找到插入的位置

首先找到插入的位置,然后针对红黑树的规则条件,对插入位置进行修正。

根据key值,查找插入的位置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
template<typename T>
void RBTree<T>::insert(Node<T> *&root, Node<T> *node) {
    Node<T> *y = nullptr;
    Node<T> *x = root;

    // 找到插入父节点
    while (x != nullptr) {
        y = x;

        if (node->key > y->key)
            x = x->rightChild;
        else
            x = x->leftChild;
    }
    node->parent = y;

    // 插入
    if (y == nullptr)
        root = node;
    else {
        if (node->key < y->key)
            y->leftChild = node;
        else
            y->rightChild = node;
    }

    node->color = RED;

    // 修正
    this->insertFixUp(root, node);
}

template<typename T>
void RBTree<T>::insert(T key) {
    Node<T> * z = nullptr;

    if ((z = new Node<T>(RED, key, nullptr, nullptr, nullptr)) == nullptr)
        return;

    this->insert(this->m_root, z);
}

插入后的修正

修正需要根据插入点的及其周边点的情况进行修正。

  • 如果插入的点为根节点,表示红黑树为空,只需把插入点的颜色修改为红色,然后设置根节点指向插入节点即可。
  • 如果插入节点的父节点为黑色,则无需任何操作。
  • 如果插入节点的父节点为红色,则需要考虑三种情况:
    • 插入节点的父节点及其父节点的兄弟节点均为红色;
    • 插入节点的父节点为红色,父节点的兄弟节点为黑色,且插入节点为父节点的右孩子;
    • 插入节点的父节点为红色,父节点的兄弟节点为黑色,且插入节点为父节点的左孩子;

对于第一种情况: 插入节点的父节点及其父节点的兄弟节点均为红色。我们讨论插入节点为左孩子的情况,右孩子类似。

首先 将父节点及父节点的兄弟节点涂黑,祖父节点涂红,然后将当前节点指向其祖父节点,以新的节点从新开始判断。如下图所示:

parent_red_uncle_red

对于第二种情况:插入节点的父节点为红色,父节点的兄弟节点为黑色,且插入节点为父节点的右孩子

将当前节点指向其父亲节点,以父亲节点进行 左旋。如下图所示:

parent_red_uncle_black_rightchild

对于第三种情况:插入节点的父节点为红色,父节点的兄弟节点为黑色,且插入节点为父节点的左孩子

将父节点涂黑,祖父节点涂红,当前节点指向其 祖父节点,以 祖父节点 进行 右旋,并将根结点颜色涂黑。如下图所示:

parent_red_uncle_black_leftchild

修正过程始终会走到情况三!步骤可以简单的记为: 变色->左旋->右旋

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
template<typename T>
void RBTree<T>::insert(Tchild key) {
    Node<T> *z = nullptr;

    if ((z = new Node<T>(RED, key, nullptr, nullptr, nullptr)) == nullptr)
        return;

    this->insert(z);
}

template<typename T>
void RBTree<T>::insert(Node<T> *node) {
    Node<T> *y = nullptr;
    Node<T> *x = this->m_root;

    // 找到插入父节点
    while (x != nullptr) {
        y = x;

        if (node->key > y->key)
            x = x->rightChild;
        else
            x = x->leftChild;
    }
    node->parent = y;

    // 插入
    if (y == nullptr)
        this->m_root = node;
    else {
        if (node->key < y->key)
            y->leftChild = node;
        else
            y->rightChild = node;
    }

    node->color = RED;

    // 修正
    this->insertFixUp(node);
}

template<typename T>
void RBTree<T>::insertFixUp(Node<T> *node) {
    Node<T> *parent, *grandparent;

    while ((parent = node->parent) != nullptr && parent->isred()) {
        grandparent = node->grandParent();  // 这里不可能出现祖父节点为空的情况,因为根结点一定为黑色,所以如果父节点为根节点会跳出循环

        if (parent == grandparent->leftChild) {     // 父节点是祖父节点的左孩子的情况
            Node<T> *uncle = grandparent->rightChild;

            // case1: 父节点和叔父节点都为红色
            if (uncle != nullptr && uncle->isred()) {   // 叔父节点不为空,并且叔父节点为红色
                parent->setblack();     // 将父节点设置为黑色
                uncle->setblack();      // 叔父节点也设置为黑色
                grandparent->setred();  // 祖父节点设置为红色
                node = grandparent;     // 当前节点指向祖父节点
                continue;
            }

            // case2: 叔父节点为黑色,并且当前节点为右孩子
            if (node == parent->rightChild) {
                this->leftRotate(parent);   // 以父节点为中心进行左旋
                Node<T> *tmp = parent;      // 交换当前节点和父节点,为下一步做准备
                parent = node;
                node = tmp;
            }

            // case3: 叔父节点为黑色,并且当前节点为左孩子
            parent->setblack();     // 将父节点设置为黑色
            grandparent->setred();  // 祖父节点设置为红色
            this->rightRotate(grandparent);     // 以祖父节点为中心进行右旋
        } else {    // 父节点是祖父节点的右孩子的情况
            Node<T> *uncle = grandparent->leftChild;

            // case1: 父节点和叔父节点都为红色
            if (uncle != nullptr && uncle->isred()) {   // 叔父节点不为空,并且叔父节点为红色
                parent->setblack();     // 将父节点设置为黑色
                uncle->setblack();      // 叔父节点也设置为黑色
                grandparent->setred();  // 祖父节点设置为红色
                node = grandparent;     // 当前节点指向祖父节点
                continue;
            }

            // case2: 叔父节点为黑色,并且当前节点为左孩子
            if (node == parent->leftChild) {
                this->rightRotate(parent);  // 以父节点为中心进行右旋
                Node<T> *tmp = parent;      // 交换当前节点和父节点,为下一步做准备
                parent = node;
                node = tmp;
            }

            // case3: 叔父节点为黑色,并且当前节点为右孩子
            parent->setblack();     // 将父节点设置为黑色
            grandparent->setred();  // 祖父节点设置为红色
            this->leftRotate(grandparent);      // 以祖父节点为中心进行左旋
        }
    }

    this->m_root->setblack();
}

删除

在插入时,我们需要考虑的是 插入节点及其父亲节点都为红色,即 双红 的情况。在删除时,我们需要考虑的是 删除节点及其父节点都为黑色,即 双黑 的情况。

如果是 删除节点有两个孩子,并且孩子节点不为叶子节点 的情况,可以通过 先序遍历 的方式寻找其 前驱或者后继节点(叶子节点) 进行替换。

假设要删除的节点为 N,根据 先序遍历的结果A < N < B。那么可以用 B 也就是整个树中 大于N的最小的节点N 进行替换,将 节点B 中的key与 N 中的key进行互换,然后删除 节点B。此时,只需要思考删除 节点B 所面临的不符合规则的问题。由于 节点B节点N后继节点,所以 节点B 最多只有一个子节点!!这样就将问题从 两个孩子变为一个孩子

删除的步骤如下:

  • 被删除节点没有子节点 - 直接删除,此时,由于没有子节点,所以以该节点为叶结点的树就不存在了,不会违背红黑树的规则。
  • 被删除节点有一个孩子节点 - 用它的孩子节点对它进行替换,删除孩子节点,然后根据孩子节点的颜色进行修正。
  • 被删除的节点有两个孩子 - 找到被删除节点的 后继节点,使用后继节点的key替换被删除节点的key。删除 后继节点,由于 后继节点最多只有一个孩子节点,问题就转换成了上面的两种情况。

RB_tree_delete_double_child

删除后的修正

和插入的情况一样,删除完节点后,可能会出现违背 红黑树特性 的问题,这时候需要对红黑树进行修正。由于在删除的时候,已经将双孩子节点的情况转变为 最多只有一个孩子节点 的情况,所有需要修正的情况只需考虑 最多只有一个孩子节点

删除后的修正比插入后的修正要复杂很多,分为以下一些情况需要考虑。当然,现在只需要考虑 被删除节点的子节点最多有一个孩子 的情况。

在做删除后的修正之前,先假设真正要删除的节点为 X,被代替删除的节点为 NX 的父节点为 P,兄弟节点为 S,兄弟节点的孩子节点分别为 SlSr

RB_Tree_delete_replace

注意:以下情况都是在 X 为黑色,最多有一个孩子,并且 NX 的后继节点的情况下讨论的。

情况一:N 为新的根结点

X 为根结点,此时整棵树只有一条分支,所以直接替换即可。

情况二:S 为红色,其他节点为黑色

P 为中心右旋,并交换 SP 的颜色。此时,由于 X 为黑色,所以 S -> P -> N 这条路径上的黑阶较另外几条路径上的黑阶要少一个,整棵树并不平衡。接下来就变成了 情况四情况五情况六 了。

RB_Tree_delete-Delete_2

情况三:所有节点都为黑色

S 涂成红色,此时以 P 为根的子树处于平衡状态,但是由于 黑阶 减少了一个,所以通过 P 的路径上的 黑阶都减少了。需要将 N 指向 P,以 P 为对象从 情况一 开始进行删除后的修正。

RB_Tree_delete-Delete_3

情况四:P 为红色,其他节点为黑色

PS 的颜色互换,此时 P -> S -> Sl(Sr) 的路径上的 黑阶不变P -> N 路径上的 黑阶增加一,通过 P 的路径上的 黑阶并没有改变,整棵树达到平衡状态,结束。

RB_Tree_delete-Delete_4

情况五:Sr 为红色,P可黑可红,其他节点为黑色

S 为中心进行左旋,并交换 SSr 的颜色。由于之前通过 P -> S 的子树已经是平衡的了,此时通过 P -> Sr 的子树也是平衡的。接下来进行 情况六 的分析。

RB_Tree_delete-Delete_5

情况六:Sl 为红色,P可黑可红,其他节点为黑色

我们用 情况五 的结果来进行考虑。首先要了解的是,节点1、2、5、6的黑阶相同,节点3、4的黑阶要比前面四个节点的黑阶多一,黑阶的数量与节点 SlN 相同!

假设情况五变换前,S 的黑阶为 Lps,那么 Sr的黑阶为 LpsNSl 的黑阶为 Lps - 1,节点3、4的黑阶为 Lps - 1,节点1、2、5、6的黑阶为 Lps - 2

P 为中心进行右旋,并将 S 涂黑。

经过上述的转换,在新的子树中, SP 的黑阶为 Lps,达到平衡,结束。

RB_Tree_delete-Delete_6

删除修正总结

删除修正部分,分析起来确实有点头晕,需要静下心来。最后将各个情况串起来,会好很多。

RB_Tree_delete-Delete_all

参考