理解红黑树(下)删除操作

1. 引言

红黑树的结点增删改查效率非常优良,都为logn,其应用十分广泛:
1. Linux内核进程调度由红黑树管理进程控制块。
2. Epoll用红黑树管理事件块。
3. nginx服务器用红黑树管理定时器。
4. C++ STL中的map和set的底层实现为红黑树。
5. Java中的TreeMap和TreeSet由红黑树实现。
6. Java8开始,HashMap中,当一个桶的链表长度超过8,则会改用红黑树。

红黑树的操作比较复杂,实现起来比较麻烦,看了很多篇别人写的关于红黑树的博客,插入操作比较容易理解,但是删除操作都非常乱,本文不会讲解二叉排序树,左旋右旋等基础知识,将直接从红黑树的插入操作和删除操作开始讲解,并附上源码,算是自己对红黑树的一个总结。

2. 红黑树的性质

  1. 结点不是红色就是黑色。
  2. 根结点一定为黑色。
  3. 所有的NULL指针被认为是黑色结点。
  4. 红色结点的两个孩子一定是黑色。
  5. 从任一节点到其每个叶子的所有简单路径上的黑色结点数都相同。

想要理解红黑树的操作,以上性质必须滚瓜烂熟。
先定义红黑树这个类的成员属性:

public class RBTree<T extends Comparable<T>> {
    private RBNode root;//根结点指针
    private static final boolean RED = false;
    private static final boolean BLACK = true;
    class RBNode<T extends Comparable<T>> {//树的结点
        boolean color;//红或黑
        T key;
        RBNode left;//左孩子指针
        RBNode right;//右孩子指针
        RBNode parent;//父结点指针,红黑树经常涉及到兄弟,叔叔,侄子,有个父结点指针方便操作。
        public RBNode(boolean color, T key, RBNode left, RBNode right, RBNode parent) {
            this.color = color;
            this.key = key;
            this.left = left;
            this.right = right;
            this.parent = parent;
        }
    }
    //左旋
    private void leftRotate(RBNode x) {
        RBNode y = x.right;
        x.right = y.left;
        if(y.left != null)
            y.left.parent = x;
        y.parent = x.parent;
        if(x.parent != null) {
            if(x.parent.left == x)
                x.parent.left = y;
            else 
                x.parent.right = y;
        }else {
            this.root = y;
        }
        y.left = x;
        x.parent = y;
    }
    //右旋
    private void rightRotate(RBNode x) {
        RBNode y = x.left;
        x.left = y.right;
        if(y.right != null)
            y.right.parent = x;
        y.parent = x.parent;
        if(x.parent != null) {
            if(x.parent.left == x)
                x.parent.left = y;
            else 
                x.parent.right = y;
        }else {
            this.root = y;
        }
        y.right = x;
        x.parent = y;
    }
}

3. 红黑树的删除操作

  看了N篇博客,只感觉下面这篇的删除操作讲得非常好,先把链接贴出来再说:红黑树删除操作
  然后是我自己的理解。
  首先红黑树是二叉排序树,按排序树的方式删除一个结点,有三种情况:
  (1)如果待删除的结点是叶子结点,则直接删除即可。
  (2)如果待删除的结点只有一个孩子,则将父结点的指针指向它的孩子。
  (3)如果待删除的结点有两个孩子,则可以找它的后继,将值覆盖过来,之后情况转变成删除前驱或者后继结点,回到(1)和(2)。
那么再根据颜色的不同,接下来情况就非常多了,这就是红黑树删除操作麻烦的地方,切忌浮躁,务必静下心来仔细分析。

第三种情况:
  此情况因为可以转变成前面两种情况,相当于已经解决了。
第二种情况(一个孩子):
  此情况是个软柿子,可以先捏掉:
根据性质5,可以推断出的是,待删除结点必定是黑色(红色结点不可能只有一个孩子),且唯一的子树一定是一个红色孩子。所以这种情况可以在删除时就处理掉,用红色孩子顶替待删除结点,再将其涂成黑色。
理解红黑树(下)删除操作_第1张图片
变成:
理解红黑树(下)删除操作_第2张图片

第一种情况(叶子):
  首先,如果待删除结点为红色,则直接结束了。
  如果待删除结点为黑色,这种情况下,此条分支不可能通过涂色的方式弥补缺少的黑色,所以要判断其兄弟和侄子的状况,希望通过兄弟那边分支的旋转,来保持黑色的数量:
(1)如果兄弟是黑色。
理解红黑树(下)删除操作_第3张图片
这种情况下,一旦deleted被删除,左边就比右边少了一个黑色结点,parent红色或者黑色都有可能,那两个侄子要么为null,要么为红色。

同样先捏软柿子:

  • 如果两个侄子都为null,parent为红色
    理解红黑树(下)删除操作_第4张图片
    deleted结点被删除之后
    理解红黑树(下)删除操作_第5张图片
    此时只要将parent涂黑,brother涂红即可。
    理解红黑树(下)删除操作_第6张图片
  • 如果两个侄子都为null,parent为黑色
    理解红黑树(下)删除操作_第7张图片
    deleted结点被删除之后
    理解红黑树(下)删除操作_第8张图片
    这时,不论如何涂色,都无法让弥补左边缺少的一个黑色,所以只能先把brother涂红,让左右黑色数量相同,但是这样parent这棵树一定会少一个黑色,所以需要对parent进行又一轮的调整。
    理解红黑树(下)删除操作_第9张图片
  • 左侄子为红色(在网上见过两种调整方法,其中一种是将其转换成另一种情况,这里选择一步调整到位)
    理解红黑树(下)删除操作_第10张图片
    红色结点是不会影响红黑性质的,所以希望能够利用一下这个红结点。
    删除deleted结点之后,经过一系列变换:
    理解红黑树(下)删除操作_第11张图片
    可以看到,左侄子被涂上了parent的颜色,目的是为了能够在后面顶替parent,而parent有自己的任务,就是去弥补左子树缺少的黑色。
  • 右侄子为红色
    理解红黑树(下)删除操作_第12张图片
    brother涂上了parent的颜色为了顶替parent,parent的任务是去弥补左子树缺少的黑色,而右侄子则涂黑保持黑色数量相等。

(2)如果兄弟是红色,那么可以先转换成黑兄的情况。
理解红黑树(下)删除操作_第13张图片
可以肯定的是,那两个侄子肯定不为null。
parent左旋一下,然后parent和brother交换颜色,之后brother指向最下面那个结点。这样一来,就变成了上面的黑兄情况。
理解红黑树(下)删除操作_第14张图片

到这里为止,删除操作就全部分析完,这当然只是左孩子位置的情况,对称情况的分析是一样的。

4. 实现源码  

//删除结点
private void deleteNode(RBNode node) {
    //replace表示删除之后顶替上来的结点
    //parent为replace结点的父结点
    RBNode replace = null, parent = null;
    // 如果删除的结点左右孩子都有
    if (node.left != null && node.right != null) {
        RBNode succ = null;
        for (succ = node.right; succ.left != null; succ = succ.left);//找到后继
        node.key = succ.key;//覆盖值
        deleteNode(succ);//递归删除,只可能递归一次
        return;
    } else {// 叶子或只有一个孩子的情况
        // 如果删除的是根,则root指向其孩子(有一个红孩子或者为nil)
        if (node.parent == null) {
            // 如果有左孩子,那根就指向左孩子,没有则指向右孩子(可能有或者为NIL)
            this.root = (node.left != null ? node.left : node.right);
            replace = this.root;
            if (this.root != null)
                this.root.parent = null;
        } else {// 非根情况
            RBNode child = (node.left != null ? node.left : node.right);
            if (node.parent.left == node)
                node.parent.left = child;
            else
                node.parent.right = child;

            if (child != null)
                child.parent = node.parent;
            replace = child;
            parent = node.parent;
        }
    }
    //如果待删除结点为红色,直接结束
    if (node.color == BLACK)
        deleteFixUp(replace, parent);
}

private void deleteFixUp(RBNode replace, RBNode parent) {
    RBNode brother = null;
    // 如果顶替结点是黑色结点,并且不是根结点。
    //由于经过了上面的deleteNode方法,这里面parent是一定不为null的
    while ((replace == null || replace.color == BLACK) && replace != this.root){
        //左孩子位置的所有情况,
        if (parent.left == replace) {
            brother = parent.right;
            // case1 红兄,brother涂黑,parent涂红,parent左旋,replace的兄弟改变了,变成了黑兄的情况
            if (brother.color == RED) {
                brother.color = BLACK;
                parent.color = RED;
                leftRotate(parent);
                brother = parent.right;
            }
            // 经过上面,不管进没进if,兄弟都成了黑色
            // case2 黑兄,且兄弟的两个孩子都为黑
            if ((brother.left == null || brother.left.color == BLACK) && (brother.right == null || brother.right.color == BLACK)) {
            // 如果parent此时为红,则把brother的黑色转移到parent上
                if (parent.color == RED) {
                parent.color = BLACK;
                brother.color = RED;
                break;
            } else {// 如果此时parent为黑,即此时全黑了,则把brother涂红,导致brother分支少一个黑,使整个分支都少了一个黑,需要对parent又进行一轮调整
                brother.color = RED;
                replace = parent;
                parent = replace.parent;
            }
        } else {
            // case3 黑兄,兄弟的左孩子为红色
            if (brother.left != null && brother.left.color == RED) {
                brother.left.color = parent.color;
                parent.color = BLACK;
                rightRotate(brother);
                leftRotate(parent);
            // case4 黑兄,兄弟的右孩子为红色
            } else if (brother.right != null && brother.right.color == RED) {
                brother.color = parent.color;
                parent.color = BLACK;
                brother.right.color = BLACK;
                leftRotate(parent);
            }
            break;
        }
    } else {//对称位置的情况,把旋转方向反回来
        brother = parent.left;
        // case1 红兄,brother涂黑,parent涂红,parent左旋,replace的兄弟改变了,变成了黑兄的情况
        if (brother.color == RED) {
            brother.color = BLACK;
            parent.color = RED;
            rightRotate(parent);
            brother = parent.left;
        }
        // 经过上面,不管进没进if,兄弟都成了黑色
        // case2 黑兄,且兄弟的两个孩子都为黑
        if ((brother.left == null || brother.left.color == BLACK)
            && (brother.right == null || brother.right.color == BLACK)) {
        // 如果parent此时为红,则把brother的黑色转移到parent上
            if (parent.color == RED) {
                parent.color = BLACK;
                brother.color = RED;
                break;
            } else {// 如果此时parent为黑,即此时全黑了,则把brother涂红,导致brother分支少一个黑,使整个分支都少了一个黑,需要对parent又进行一轮调整
                brother.color = RED;
                replace = parent;
                parent = replace.parent;
                }
            } else {
                // case3 黑兄,兄弟的左孩子为红色,右孩子随意
                if (brother.right != null && brother.right.color == RED) {
                    brother.right.color = parent.color;
                    parent.color = BLACK;
                    leftRotate(brother);
                    rightRotate(parent);
                // case4 黑兄,兄弟的右孩子为红色,左孩子随意
                } else if (brother.left != null && brother.left.color == RED) {
                    brother.color = parent.color;
                    parent.color = BLACK;
                    brother.left.color = BLACK;
                    rightRotate(parent);
                }
                break;
            }
        }
    }
    //这里可以处理到删除结点为只有一个孩子结点的情况,如果是根,也会将其涂黑。
    if (replace != null)
        replace.color = BLACK;
}

5. 总结

  可以看到,红黑树的删除操作非常复杂的,情况较多,但是效率非常好。了解甚至手写红黑树是一种挑战也是一种很好的锻炼,并不光是为了笔试面试(估计也问不了这个),所谓练拳不练功,到老一场空,在繁忙的日常工作中,多去了解一点底层的东西可以加深对各种技术的理解。

你可能感兴趣的:(算法与数据结构)