一、红黑树

红黑树也是BST树的一种,它和AVL树不同,它不要求各个节点的高度都是完全一致,是一个近似平衡的树。

红黑树通过黑树节点增加颜色,对每一条根节点到叶子节点上面的各个节点颜色进行约束,确保没有一条路径会比其他路径长出2倍。

红黑树的性质:

  1. 每个节点的颜色要么是红色,要么是黑色
  2. 根节点是黑色的
  3. 每个叶子节点(NIL)是黑色
  4. 如果一个节点是红色的,则它的两个子节点都是黑色的
  5. 对于每个节点,其到任意后代的叶子节点的路径上,都包含相同数量的黑色节点

和普通树不同的是,红黑树的叶子节点都是NIL节点,它是一个特殊的节点,颜色是黑色。

定理:一个有n各内部节点的红黑树的高度至多为2lg(n+1)。

下面就是一棵红黑树示例,省去了末尾的NIL叶子节点:

红黑树的操作和BST/AVL树相差不大,搜索、旋转也都是一模一样的代码。红黑树的操作:

  • RB_INSERT: 插入节点到红黑树
  • RB_DELETE: 从红黑树中删除节点

二、红黑树的添加操作

红黑树每个新插入的节点都是红色,节点添加操作分为以下两步:

  1. 按照BST的插入节点操作插入一个节点
  2. 调整节点颜色,以满足红黑树性质

过程中负责的步骤是第二部,也就是插入之后的调整,调整的规则有如下几种。

2.1 节点颜色调整规则

假设待插入节点为z,设其叔叔节点为y。插入节点后,有以下几种情况:

情况一:父节点是红色,叔叔节点也是红色

这种情况下,插入节点z会违反性质4,并且z的叔父节点也是红色,所以为了维持性质,需要修改其父亲节点和叔叔节点的颜色为黑色,修改其祖父节点的颜色为红色。并把z设置为祖父节点,继续判断z的属性,如果修改后z的叔叔节点还是红色,继续当前的操作。直到叔叔节点y是黑色了,情况一就转换成情况二,向下看。

情况二:父节点是红色,叔叔节点是黑色,当前节点是其父节点的右儿子

叔叔节点是黑色,判断当前节点是其父亲的左儿子还是右儿子,如果是左儿子,将满足情况三,按照情况三来处理。如果是右儿子,设置z指向其父节点,并对新的z节点左旋,把情况二转换成情况三。

情况三:父节点是红色,叔叔节点是黑色,当前节点是其父节点的左儿子

这种情况下,为了保持平衡,修改z的父颜色为黑色,z的祖父节点为红色,然后对祖父节点右旋。得到满足条件的红黑树:

情况四:父节点是黑色

如果父节点是黑色,无需调整,插入节点后树依旧是一棵红黑树,性质不会破坏。

2.2 插入节点代码

红黑树的插入代码前部分和BST插入一模一样,最后添加完成之后执行操作FIXUP对红黑树进行调整:

void rb_tree_insert(struct rb_tree_st *tree, struct rb_tree_node_st *node) {
    struct rb_tree_node_st *x = tree->root;
    struct rb_tree_node_st *y = NULL;
    struct rb_tree_node_st *z = node;

    while (x != NULL) {
        y = x;
        if (z->key < x->key)
            x = x->lchild;
        else
            x = x->rchild;
    }

    z->parent = y;
    if (y == NULL)
        tree->root = z;
    else if (y->key > z->key)
        y->lchild = z;
    else
        y->rchild = z;

    z->lchild = NULL;
    z->rchild = NULL;
    node->color = NODE_COLOR_RED;

    rb_tree_fixup(tree, node);
}

红黑树调整代码:

void rb_tree_fixup(struct rb_tree_st *tree, struct rb_tree_node_st *node) {
    struct rb_tree_node_st *z = node;
    struct rb_tree_node_st *y = NULL;
    
    while (z->parent && z->parent->color == NODE_COLOR_RED) {
        // 如果父节点是红色,那么必定有祖父节点,根据父节点的位置得到叔父节点
        if (z->parent == z->parent->parent->lchild)
            // 父节点是祖父节点的左子节点
            y = z->parent->parent->rchild;
        else
            y = z->parent->parent->lchild;

        if (y && y->color == NODE_COLOR_RED) {
            // 叔父节点是红色,把叔父节点和父节点全部置黑,不区分叔父节点是左儿子还是右儿子
            y->color = NODE_COLOR_BLACK;
            z->parent->color = NODE_COLOR_BLACK;
            // 祖父节点置红,然后重新处理祖父节点
            z->parent->parent->color = NODE_COLOR_RED;
            z = z->parent->parent;
            continue;
        }

        // 此时叔父节点必定是黑色,把y设为祖父节点,需要区分叔父节点是左儿子还是右儿子
        y = z->parent->parent;
        if (z->parent == y->lchild) {
            if (z == z->parent->rchild) {
                z = z->parent;
                left_rotate(tree, z);
            }
            z->parent->color = NODE_COLOR_BLACK;
            z->parent->parent->color = NODE_COLOR_RED;
            right_rotate(tree, z->parent->parent);
        } else {
            if (z == z->parent->lchild) {
                z = z->parent;
                right_rotate(tree, z);
            }
            z->parent->color = NODE_COLOR_BLACK;
            z->parent->parent->color = NODE_COLOR_RED;
            left_rotate(tree, y);
        }
    }
    // 设置根节点是黑色
    tree->root->color = NODE_COLOR_BLACK;
}

三、删除节点

和AVL中一样,删除节点总是比增加节点麻烦一些。因为红黑树也是一颗BST树,并且也是一个接近平衡的二叉树,因此它的删除过程也和AVL树一样。先删除节点,然后调整,步骤为:

  1. 删除或者替换节点,这一步有可能导致红黑树性质不满足
  2. 调整节点颜色,以满足红黑树性质

在AVL树中,删除节点并不是把这个节点直接删除,而是找到这个节点的后继节点来替代该节点。红黑树中的删除也是按照这种办法,假设删除节点为z,设定y表示替换z位置的节点,x表示实际上被删除的节点(即逻辑上被删除的叶子节点),wx的兄弟节点。删除节点x点有可能是z的儿子,也有可能是y的儿子,也有可能是NIL。

当删除节点是红色,删除时不会影响红黑树的性质,需要调整的直有删除节点是黑色的场景。

3.1 删除节点调整颜色规则

情况一:删除节点x的兄弟节点w是红色

操作:修改w和父节点的颜色,并对父节点左移。此时x的新兄弟节点是原w节点的左子节点,必定为黑色。情况一可以转换成下面的情况来处理。

情况二:x的兄弟节点w是黑色,且w的两个子节点都是黑色

操作:去掉w上的黑色,并使x为其父节点,继续循环处理新x节点。

情况三:x的兄弟节点w是黑色,w的左孩子是红色,w的右孩子是黑色

操作:修改w和其左儿子的颜色,并对w右旋,然后把w设置为其原来的左儿子。将情况三转换成情况四。

情况四:x的兄弟节点w是黑色,且w的右孩子是红色

第四种情况是我们最终想要转换的情况,因为只有这一种情况能通过旋转来彻底解决平衡。

操作:设置w的颜色为其父节点的颜色,w父节点设置为黑色,w的右儿子设置为黑色,对父节点进行左旋。

上面的描述过于抽象,可以用一个例子来理解。下面是一棵红黑树,想要删除节点60(60原先是黑色):

删除的步骤:

  1. 设置80的颜色为其父节点的颜色(红色)
  2. 设置80的父节点70颜色为黑色,设置80右儿子85的颜色为黑色
  3. 对70左旋,去掉节点60,得到右边的红黑树,此时红黑树的性质依旧保持

3.2 删除代码实现

删除红黑树节点的伪代码(来自算法导论):

修复节点伪代码:

最后修改:2019 年 05 月 04 日
如果觉得我的文章对你有用,请随意赞赏