我终于想明白了红黑树为啥要标成红色和黑色

2,590 阅读5分钟

红黑树为什么要标成红色和黑色,这个问题我想了很久,我之所以想很久,是因为我从来没看过什么是红黑树。今天我看了一篇文章(致敬作者,一个画了近百张图的男人,讲的很详细,如果你还不知道什么是红黑树的话,建议先看看这篇文章《我画了近百张图来理解红黑树》)。

看了我明白了,红色和黑色是为了更快的判断此树是不是满足平衡的条件。

先来看一下红黑树的五大特性:

  • 性质1:根节点永远是黑色的。
  • 性质2:每个节点要么是红色,要么是黑色。
  • 性质3:所有的叶子节点都是空节点(即null),并且是黑色的。
  • 性质4:每个红色节点的两个子节点都是黑色。(从每个叶子到根的路径上不会有两个连续的红色节点。)
  • 性质5:从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点(我叫他黑色路径)。

说明为啥要标颜色,其实只用4和5两个性质,再结合一点点代码。这个算法实现的时候,每次假定插入的新节点是红色的,因为红色的不会影响路径上黑色节点的数量,也就不会改变黑色路径的长度。这时候就会出现两种情况,它的父亲节点是黑色的话,皆大欢喜啊,完美的满足了插入一个节点,又不影响黑色路径的长度这个条件,插入结束。但如果它的父亲是红色的呢?条件4说不能有两个连续的红色节点啊,那好吧,我把它父亲改成黑色的就可以了,但是,这一改,路径上凭空多了一个黑色的节点,那么黑色路径也就加长了,可能会引起失衡啊。为了判断是否失衡,就要扩大范围多看一下了。

假设,我们现在真的要改父亲节点的颜色了,去看一下它父亲节点的兄弟节点(我叫他叔叔节点)。如果叔叔节点和父亲节点同为红色,那就把这哥俩都变成黑色,左右子树同时增加黑色路径长度,那么新增节点的爷爷节点就是平衡的,这时候就可以放心的去看爷爷节点的父亲(祖爷爷节点)的两个儿子是不是平衡就可以了,这是一个递归的过程。但如果,叔叔节点为黑色呢?这个时候只是单方面的把父亲节点变成了黑色,那么父亲节点这边的子树就比叔叔节点那边高了一层,这个时候就失衡了,会让爷爷节点无法接受,并且能肯定的是,此时是无法通过改变颜色来实现平衡的,所以这个时候就要进行旋转了。

总结成颜色的语言就是,爸爸是黑色的,皆大欢喜。爸爸和叔叔是红色的,只用改成黑色。爸爸是红色的,叔叔是黑色的,需要进行旋转。以上,就是把树标成红色和黑色的意义,只用简单的判断颜色,就能确定应该怎么调整。

扩展来看,红色和黑色只有两种颜色,其最糟糕的情况就是一边子树全是黑色,而另一边子树是红黑相间的,由性质1和性质3可知,这棵树的两头都是黑色的,所以红黑相间的子树只可能比全黑的子树高一倍(黑色路径是另一边的两倍),这也是红黑树所追求的近似平衡。

接下来,上代码,节选自Java8中TreeMap的调整,写的很容易读,就是不容易懂。

//取颜色
private static <K,V> boolean colorOf(Entry<K,V> p) {
    return (p == null ? BLACK : p.color);
}
//取父亲
private static <K,V> Entry<K,V> parentOf(Entry<K,V> p) {
    return (p == null ? null: p.parent);
}
//设置颜色
private static <K,V> void setColor(Entry<K,V> p, boolean c) {
    if (p != null)
        p.color = c;
}
//取左子树
private static <K,V> Entry<K,V> leftOf(Entry<K,V> p) {
    return (p == null) ? null: p.left;
}
//取右子树
private static <K,V> Entry<K,V> rightOf(Entry<K,V> p) {
    return (p == null) ? null: p.right;
}

//插入后调整,内含的左旋右旋代码不贴了
private void fixAfterInsertion(Entry<K,V> x) {
    //先把当前插入节点设置成红色
    x.color = RED;
    //条件:当前节点不为空,不是根,并且爸爸是红色的,如果是黑色的就是皆大欢喜的情况
    while (x != null && x != root && x.parent.color == RED) {
        //父亲是爷爷的左子树,左子树和右子树会影响到旋转操作,也会影响到取叔叔子树的方法
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            //取叔叔子树
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            //红色的,好,跟爸爸一个颜色,直接把他们改成黑色,进入下一个循环
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                //把爷爷节点改成红色(因为爸爸节点是红色,根据条件4,爷爷节点一定是黑色)
                //父辈变黑,祖辈变红,实质上没有增加黑色路径的长度
                //接下来就要看看爷爷节点和祖爷爷节点之间有没有破坏条件4了,好吧,进入下一层循环
                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 {
            Entry<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)));
            }
        }
    }
    //最后要记得 rule No.1 : 根只能是黑色的
    root.color = BLACK;
}