平衡二叉树和红黑树

本文主要介绍了平衡二叉树(AVL)和红黑树(rbtree)的特点以及实现,具体而言在树的插入删除平衡修复过程介绍篇幅较大。

一、平衡二叉树和红黑树

平衡二叉树(AVL): 作为二叉查找树而言,平衡二叉树具备快速查找、插入、删除的特点,并且在平衡二叉树中任何节点的左右子树高度之差不超过1,这一特点使得平衡二叉树具备非常好的平衡性。

红黑树(RB): 红黑树的特点是利用节点的颜色来控制树的平衡,根节点为黑,其余的节点可以为红或者黑,并且不能够存在父子节点同时为红的情况。最重要的一点是,每个节点到其叶节点的路径上的黑色节点的个数相等,这保证了红黑树具备平衡性。

红黑树广泛应用于Linux内核、STL关联容器及nginx中,具备非常好的查找、插入删除特性。

二、为什么红黑树比平衡二叉树应用更广泛

相对于红黑树,平衡二叉树具备更好的平衡性,因而在查找操作频繁的场景中,使用平衡二叉树比较好,但是实际上红黑树应用更加广泛。首先就查找而言,红黑树也具备相当程度的平衡性,所以查找的性能也是很好的,其次二者的差别主要体现在插入和删除后的平衡操作上。

为了保证树本身的特点,在插入和删除操作完成后,平衡二叉树和红黑树都需要进行rebalance操作调整树形。虽然二者插入和删除操作本身的复杂度都是O(n),但是在调整时红黑树更加高效。因为AVL树在平衡了当前节点之后要不断回溯可能要到根节点才能完成平衡,因而需要O(n)的复杂度,而红黑树在平衡时最多只需要三次就能够完成调整,复杂度是O(1)。

三、实现

这里介绍平衡二叉树和红黑树的C++实现

1. 节点的设计

为了方便,平衡二叉树和红黑树共用一个数据结构作为节点,主要保存左右子节点、父节点以及红黑颜色。

struct tree_node_base {
    color_type color;
    tree_node_base * parent;
    tree_node_base *left;
    tree_node_base *right;
};

2. 左旋和右旋

无论是AVL树还是红黑树在插入删除之后的平衡操作中,都离不开对节点的旋转操作。节点的左旋和右旋操作具有对称性,这里以节点的左旋为主。

左旋操作如下所示,要保证旋转完之后仍然有序,左旋如果以y为核心的话可以看作将y旋转成其父节点x的父节点,也就是将y向上提升了一层,相应的其父节点x往下挪了一层

     x                      y
    / \        左旋        / \
   x1  y       ===>       x   y2
      / \                / \
     y1 y2              x1 y1

具体实现的话,左旋操作主要需要关注x、y和y的左节点,其余节点不会发生变化,另外还需要考虑树的根节点,因为如果x刚好是根节点的话还需要更新根节点为y

void rotate_left(tree_node_base *x, tree_node_base *& root) {
    auto y = x->right;
    x->right = y->left;
    if (y->left) y->left->parent = x;

    y->parent = x->parent;

    if (x == root)
        root = y;
    else if (x == x->parent->left)
        x->parent->left = y;
    else
        x->parent->right = y;

    y->left = x;
    x->parent = y;
}

3. 插入和删除

二叉查找树节点的插入比较简单,从根节点开始往下查找,找到相同关键字的节点就直接返回,直到找到一个合适的空节点可以插入该节点即可。

删除操作则比较麻烦,首先进行二叉查找找到相应的节点,然后删除对应的节点。首先这一过程不是单纯的删除该节点就行了,还需要找到一个节点来顶替该节点,如果被删除的节点只有一个子节点的话,那直接用z的子节点顶替z就行了。但是如果两个子节点都存在的话,就需要找到z的后继节点y,用这个y去顶替z的位置。

另外考虑到在删除节点之后还要进行树的平衡调整,所以有必要记录那个地方的节点被移除了一个,当用y来顶替z的位置时,z处的树形其实并没有发生变化,反而y原来所处的位置应该要进行调整。基于此,用x来保存需要进行调整的位置。

void remove(Value v) {
    auto z = find(root, v);
    if (!z) return;

    auto y = z;
    // 删除z节点

    // 找到节点来顶替z,x节点保存的信息为
    if (!y->left) x = y->right;
    else if (!y->right) x = y->left;
    else { y = left_most(y->right); x = y->right; } // 后继节点

    if (y == z) { // 直接用z的独子x来顶替z的位置
        // 更新x和z->parent的相关信息
    }
    else { // z存在两个子节点,找到后继节点y,用y来顶替
        z->left->parent = y, y->left = z->left;
        if (y == z->right) // 处理y刚好为z的右子节点的情况
        else // 处理y为不相邻的子节点

        y->parent = z->parent;
        swap(y->color, z->color);
    }
    delete z;
}

4. AVL的树形调整

平衡二叉树需要保证树的左右子树高度之差不超过1,所以在插入和删除操作之后需要对树进行调整,调整时主要就是根据左右子树的高度来进行左旋右旋。

void avl_tree::rebalance(tree_node_base *x) {
    while(x != header) {
        int balance = height(x->left) - height(x->right); // 高度差
        if (balance > 1) { // 左边高了
            if (x->left->right) rotate_left(x->left, root);
            rotate_right(x, root);
        } else if (balance <  -1) { // 处理右边高了 }
        else x = x->parent // 回溯到根节点
    }
}

5. 红黑树树形调整

红黑树在插入和删除时的调整分别是不同的,因为插入时是增加了一个红色节点,需要满足相邻的父子节点不会同时为红色,因此需要调整到父节点不为红色的情况。而删除时则是因为可能会少了黑色节点而需要调整,也就是说,如果删除时删除了红色节点其实是不需要再平衡的。

先看看红黑树插入的时候如何进行平衡,首先区分x是左节点还是右节点。找到x的伯父节点y,如果y和parent都为红的话,可以同时调整为黑色,然后将爷爷改为红,这样只需要继续调整爷爷就行了。

其次,如果伯父节点为黑色的话,通过一系列的旋转操作,可以调整平衡。

void rb_tree::rebalance(tree_node_base *x) {
    x->color = red;
    while(x != root & &  is_red(x->parent)) {
        if (x->parent是左节点) {
            y = // x的parent的兄弟节点
            if (is_red(y)) { // y 和 x->parent都为红色可以合并
                y->color = x->parent->color = black
                x->parent->parent->color = red;
                x = x->parent->parnet // 继续调整爷爷
            } else {
                // 伯父y为黑,需要旋转
                if (x是右节点) rotate_left(x->parent, root);
                else x = x->parent;
                
                swap(x->color, x->parent->color); // 红x,黑x->parent 交换
                rotate_right(x->parent, root);
            }
        } else {
            // 对称处理x为右节点的情况
        }
    }
    root()->color = black;
}

红黑树删除之后的平衡比较麻烦,x节点表明了该条路经上需要增加一个黑色节点,主要需要x的兄弟节点w的配合,首先如果兄弟节点w为红色,先通过旋转将其调整为黑色

然后判断w的两个孩子节点,如果都为黑色的话就让w变为红色,然后继续调整x->parent,也就是x的父节点的路径上需要增加一个黑色节点

最后如果w的两个孩子中有一个红色节点,如果左节点为红色,先通过调整让其为黑色,然后将w旋转成为新的x_parent,这样原来的x的路径上现在既有x_parent也有w,也就是增加了一个黑色节点,但是问题是旋转之后w->right路径上面现在少了一个黑色节点,所以直接将w->right从红色变为黑色即可。

void rb_tree::rebalance_for_erase(tree_node_base *x, tree_node_base *x_parent) {
    while(x != root & &  is_black(x)) {
        if (x 是左节点) {
            auto w = x_parent->right; // 兄弟节点
            if (w->color == red) { // 这一步能确保x的兄弟节点为黑色
                swap(w->color, x_parent->color);
                rotate_left(x_parent, root);
                w = x_parent->right; 
            }

            if (is_black(w->left) & &  is_black(w->right)) {
                w->color = red;
                x = x_parent, x_parent = x_parent->parent;
            } else {
                // w至少有一个孩子为红  
                if (is_red(w->left)) { // 这一步骤能够确保,w->left为黑,而w->right为红
                    swap(w->left->color, w->color);
                    rotate_right(w, root);
                    w = x_parent->right;
                }
                swap(w->color, x_parent->color);
                rotate_left(x_parent, root);
                if (w->right) w->right->color = black;
                break; // 已经增加了一个黑色节点,可以退出了
            }

        } else {
            // 对称处理x为右节点的情况
        }
    }
}

红黑树在调整的过程中经常要用到旋转操作,一个比较方便的技巧就是在旋转两个节点的位置之前,先交换他们的颜色,这样就能够保证旋转之后的红黑颜色特性保持不变。

6. 完整代码

平衡二叉树和红黑树完整源码请参考:https://github.com/wxggg/algorithm/blob/master/include/tree.hh

参考

[1] 侯捷. STL源码剖析[M]. 华中科技大学出版社, 2002.

2] https://github.com/wxggg/algorithm