NO编程

NO编程

爱编程,爱分享,爱生活

数据结构之红黑树-动图演示(下)

节点的插入和删除

我们知道变色和旋转是为了修正被破坏的红黑树,使其符合红黑树的规则,从新达到平衡状态。那么增加或删除节点在具体情况下该如何操作呢?

插入节点

红黑树的节点插入与二叉查找树的插入的过程是一样的,只是最后多了一步平衡调整操作。

新插入的节点默认为红色节点,所以新节点插入到黑色节点下时不需要对平衡调整,当插入到红色节点下时才需要调整,因为插入到红色节点下违反了两个红色节点不能相邻规则。

插入节点的平衡调整有如下几种情况:

  1. 父节点的兄弟节点(也就是叔叔节点)为红色
  2. 父节点的兄弟节点为NULL节点(NULL默认为黑色),且祖父节点、父节点和新节点在一条直线
  3. 父节点的兄弟节点为NULL节点,且祖父节点、父节点和新节点不在一条直线

Case 1:

新插入节点的父节点是红色,其父节点的兄弟节点也是红色时,通过变色操作调整平衡。

此时将父节点和父节点的兄弟节点(叔叔节点)变为黑色,将祖父节点(父节点的父节点)变为红色,接下来以祖父节点为基础继续向上验证修复,但最后根节点必须为黑色。

如图: 依次插入节点[200,100,300,50]

Case 1

Case 2:

新插入节点的父节点为红色,父节点的兄弟节点为黑色时,且父节点、祖父节点和新节点3个节点在同一方向上(一条直线),如果为偏左的一条直线(如:和这个“/”方向一致),此时需要右旋(对祖父节点)。

如果为偏右的一条直线(如:和这个“\”方向一致),此时需要左旋。然后新节点的父节点变为黑色,其兄弟节点(原始的祖父节点)变为红色。

如图:依次插入节点[200,100,300,50,20] - 以右旋情况为例

Case2

Case 3:

该种情况需要旋转2次。

新插入节点的父节点为红色,父节点的兄弟节点为黑色时,且父节点、祖父节点和新节点3个节点不在同一方向上(不是一条直线),如果新节点是父节点的右子节点(此时,父节点在祖父节点的左侧,此时这三个节点构成这样“<”方向非直线),此时需要先左旋(变为一条直线)再右旋,两次旋转。

如果新节点是父节点的左子节点(此时,父节点在祖父节点的右侧,此时这三个节点构成这样“>”方向非直线),此时需要先右旋(变为一条直线)再左旋,两次旋转。

可以发现,经过第一次旋转后,其实是被转变成了Case 2 的情况。

如图: 依次插入节点[200,100,300,50,80] - 以先左再右旋为例

Case3

伪代码- 插入元素平衡修正(来源Java TreeMap):

void fixAfterInsertion(Node<K,V> x) {
    // 新插入节点默认红色
    x.color = RED;
    // 不是根节点且父节点为红色
    while (x != null && x != root && x.parent.color == RED) {
        //父节点在祖父节点的左侧
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            // 叔叔节点
            Node<K,V> y = rightOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                // 父节点设为黑色
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                // 祖父节点设为红色
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);
                    // 左旋
                    rotateLeft(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                // 右旋
                rotateRight(parentOf(parentOf(x)));
            }
        } else { // 对称的
            Node<K,V> y = leftOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateRight(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }
    root.color = BLACK;
}

删除节点

红黑树的节点删除和二叉搜索树的节点删除过程是一样的,只是最后多了一步平衡纠正操作。

只有当删除黑色节点时需要平衡调整(删除红色节点时不需要平衡调整,没破坏红黑树规则),因删除黑色节点后,会违反从任意节点到叶子节点的每条路径包含的黑色节点数目相同的规则。

删除节点要比插入节点复杂些,插入节点整个过程是先找到插入节点位置 -> 插入节点 -> 平衡调整。而删除节点整个过程是先找到删除节点的位置 -> 节点的位置变换 -> 删除节点 -> 平衡调整,且调整的情况也相对较多。

前面说过,红黑树与二叉搜索树的删除节点过程一样,当删除节点存在两个子节点,会被转换为删除有一个子节点或不存在子节点的节点。
对于有一个子节点的节点删除,比较简单,不需要平衡调整,只需要将其子节点改为黑色即可。

如图: 依次插入节点[200,100,300,50]

Case 0

对于删除不存在子节点的节点,这种情况最为复杂,需要平衡调整。
删除节点平衡调整有如下几种情况:

  1. 待删除节点的兄弟节点是红色
  2. 待删除节点的兄弟节点是黑色,且兄弟节点的子节点都是黑色
  3. 待删除节点的兄弟节点是黑色,如果兄弟节点在右侧,且兄弟节点的左子节点是红色,右子节点是黑色。如果兄弟节点在左侧,就是兄弟节点的右子节点是红色,左节点是黑色
  4. 待删除节点的兄弟节点是黑色,如果兄弟节点在右侧,且兄弟节点的右子节点是红色,左子节点是任意颜色。如果兄弟节点在左侧,则就是兄弟节点的左子节点是红色,右子节点是任意色

Case 1:

对于 Case 1,首先改变父节点和兄弟节点颜色(兄弟节点变为黑色,父节点变为红色),再对父节点做一次旋转(红色的兄弟节点在右侧,左旋。红色的兄弟节点在左侧,右旋),操作后,红黑的规则没有被破坏,树的黑色高度没变化,原兄弟节点的一个子节点变成删除节点的新兄弟节点。

所以,Case 1 转化成了 Case 2 或 Case 3、Case 4 几种情况。

如图: 依次插入节点[200,100,300,400,500,600] - Case1 转换为 Case2,3,4 等情况

Case 1

Case 2:

对于Case 2,删除节点后其父节点的左子树比右子树黑高度小1,此时需要把兄弟节点改为红色,则左右子树黑高度就相同了。此时将删除节点的父节点变为新的节点,然后继续向上迭代调整,修正平衡。

如图: 依次插入节点[200,100,300,400,删除400] 得到下图结构

Case 2

Case 3:

对于Case 3,兄弟节点在右侧时,交换兄弟节点和其左子节点(红色)的颜色,并对兄弟节点进行右旋,于是被删除节点的新兄弟是一个有红色右子节点的黑色节点。相反,兄弟节点在左侧时,交换兄弟节点和其右子节点(红色)的颜色,并对兄弟节点进行左旋,于是被删除节点的新兄弟是一个有红色左子节点的黑色节点。
所以,Case 3 转化成了Case 4。

如图: 依次插入节点[200,100,300,250]

Case 3

Case 4:

对于Case 4,兄弟节点在右侧时,兄弟节点和父节点交换颜色,把兄弟节点的右子节点设为黑色,并对删除节点的父节点做一次左旋转。
兄弟节点在左侧时,兄弟节点和父节点交换颜色,把兄弟节点的左子节点设为黑色,并对删除节点的父节点做一次右旋转。

最后将删除节点直接指向根节点,循环结束。

如图: 依次插入节点[200,100,300,400]

Case 4

伪代码 - 删除元素平衡修正(来源Java TreeMap):

void fixAfterDeletion(Node<K,V> x) {
    while (x != root && colorOf(x) == BLACK) {
        if (x == leftOf(parentOf(x))) {
            Node<K,V> sib = rightOf(parentOf(x));
            // Case 1
            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rotateLeft(parentOf(x));
                sib = rightOf(parentOf(x));
            }
            // Case 2
            if (colorOf(leftOf(sib))  == BLACK &&
                colorOf(rightOf(sib)) == BLACK) {
                setColor(sib, RED);
                x = parentOf(x);
            } else {
                // Case 3
                if (colorOf(rightOf(sib)) == BLACK) {
                    setColor(leftOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateRight(sib);
                    sib = rightOf(parentOf(x));
                }
                // Case 4
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(rightOf(sib), BLACK);
                rotateLeft(parentOf(x));
                x = root;
            }
        } else { // 对称的
            Node<K,V> sib = leftOf(parentOf(x));

            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rotateRight(parentOf(x));
                sib = leftOf(parentOf(x));
            }

            if (colorOf(rightOf(sib)) == BLACK &&
                colorOf(leftOf(sib)) == BLACK) {
                setColor(sib, RED);
                x = parentOf(x);
            } else {
                if (colorOf(leftOf(sib)) == BLACK) {
                    setColor(rightOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateLeft(sib);
                    sib = leftOf(parentOf(x));
                }
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(leftOf(sib), BLACK);
                rotateRight(parentOf(x));
                x = root;
            }
        }
    }

    setColor(x, BLACK);
}

文章中的代码片段全部出自Java的TreeMap集合(略有改动),这是个权威的参考。

为何很多应用选择红黑树作为平衡树的实现

插入或删除节点时,平衡二叉树为了维持平衡,需要对其进行平衡纠正,这需要较大的开销。

例如,有种AVL树, 是高度平衡树,结构比红黑树更加平衡,这也表示其增删节点时也更容易失衡,失衡就需要纠正,增删节点时开销会比红黑树大。但不可否认的是AVL树搜索的效率是非常稳定的。

因此在大量数据需要插入或者删除时,AVL需要平衡调整的频率会更高。因此,红黑树在需要大量插入和删除节点的场景下,效率更高。自然,由于AVL高度平衡,因此AVL的Search效率略高。

红黑树不是高度平衡树,但平衡的效果已经很好了。所以,可以认为红黑树是一种折中的选择,其综合性能较好。

最后

红黑树的介绍到此结束,学习过程中可以参考 TreeMap 源码来学习,会有意想不到的效果!


推荐:
数据结构之红黑树-动图演示(上)

参考:
JDK TreeMap源码
https://www.zhihu.com/question/20545708

posted @ 2019-08-21 09:19  陆十三  阅读(2195)  评论(3编辑  收藏  举报