数据结构及算法基础--树(Tree)(四)红黑树(Red Black Tree)

上篇文章我们学习了二叉排序树,而二叉排序树的性能取决于二叉树的层数:

  • 最好的情况是 O(logN),存在于完全二叉排序树情况下,其访问性能近似于折半查找;
  • 最差时候会是 O(N),比如插入的元素是有序的,生成的二叉排序树就是一个链表,这种情况下,需要遍历全部元素才行

为了改变二叉树的不足,我们开始使用红黑树。

对红黑树的定义其实是在二叉搜索树的规定进行了扩展,扩展内容为:

1、每个节点要么是黑色,要么是红色;

2、根节点永远是黑色;

3、所有的叶节点都是黑色(注意,java中实现的红黑树中的叶节点其实是null,在红黑树中表示为NIL,为空);

4、每个红色节点的两个子节点一定是黑色;

5、任意一个节点到其每个叶子节点的路径包含相同的黑色节点;

 

 

黑色高度指的是:从根节点到叶子节点路径中包含的黑色节点个数。

 

红黑树的左旋和右旋。这在红黑树中是非常重要的一环,我们在进行插入或者删除的时候为了保障红黑树的结构,我们都会使用一定的左旋和右旋操作来维持。

左旋和右旋的概念通过一张图便可以理解:

shixinzhang

 

我们在java结合框架的treemap中可以查看到左旋右旋操作的具体过程:

/** From CLR */
    private void rotateLeft(Entry<K,V> p) {
        if (p != null) {
            Entry<K,V> r = p.right;
            p.right = r.left;
            if (r.left != null)
                r.left.parent = p;
            r.parent = p.parent;
            if (p.parent == null)
                root = r;
            else if (p.parent.left == p)
                p.parent.left = r;
            else
                p.parent.right = r;
            r.left = p;
            p.parent = r;
        }
    }

    /** From CLR */
    private void rotateRight(Entry<K,V> p) {
        if (p != null) {
            Entry<K,V> l = p.left;
            p.left = l.right;
            if (l.right != null) l.right.parent = p;
            l.parent = p.parent;
            if (p.parent == null)
                root = l;
            else if (p.parent.right == p)
                p.parent.right = l;
            else p.parent.left = l;
            l.right = p;
            p.parent = l;
        }
    }

 

这是一段很简单的代码,相信即便不了解treemap和java的Entry类的同学仍然能够明白这段代码的过程。

 

接下来就是红黑树最重要的操作了。就是在插入或者删除过程后,通过调整来让这个树仍然满足红黑树的条件。

 

1)插入:

我们其实主要需要考虑的是规则4和规则5,但是如果我们插入红色节点,我们就只需要根据规则4进行调整。所以我们将插入元素染红。

如果插入节点的父节点是黑色,我们则不需要进行其他的变化变可以满足条件。如果父节点是红色,我们则需要通过一系列变化来进行调整。

 

注:插入后我们主要关注插入节点的父亲节点的位置,而父亲节点位于左子树或者右子树的操作是相对称的,这里我们只介绍一种,即插入位置的父亲节点为左子树。

 

其实这里面只有两种情况

  1、父节点的兄弟节点是红色

  2、父节点的兄弟节点是黑色

 

情况1、父节点的兄弟节点是红色:

shixinzhang

  假设插入的是节点 N,这时父亲节点 P 和叔叔节点 U 都是红色,爷爷节点 G 一定是黑色。红色节点的孩子不能是红色,这时不管 N 是 P 的左孩子还是右孩子,只要同时把 P 和 U 染成黑色,G 染成红色即可。这样这个子树左右两边黑色个数一致,也满足特征 4。但是这样改变后 G 染成红色,G 的父亲如果是红色岂不是又违反特征 4 了?

  我们这个时候可以使用向树的高层循环前进来进行变换,当判断的节点等于根节点或者当前节点的父节点为黑色的时候,我们跳出循环,完成调整。

 

情况2、父节点的兄弟节点是黑色:

shixinzhang

通过把 爷爷节点 G 右旋,P 变成了这个子树的根节点,G 变成了 P 的右子树。

右旋后 G 跑到了右子树上,这时把 P 变成黑的,多了一个黑节点,再把 G 变成红的,完成调整

 

当然还有一种情况,如果 N 是 P 的右孩子,就需要多进行一次左旋,把情况化解成上述情况。

shixinzhang

 

在代码中,这一切情况都有很好的考量:

/** From CLR */
    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);
                    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)));
                }
            }
        }
        root.color = BLACK;
    }

 

 

2)删除调整

 

好吧,其实删除调整的原理是非常非常复杂的,要考虑很多种情况。本人能力有限还未理解彻底,也没有找到没有错误并且完整的资料,我只能通过jdk文档中的代码进行分析,然而我也只是将整个过程梳理了一遍,对每个情况判断的情况原理和机制还是不太了解,将来我学习更多知识过后,我会来细细讲解这部分内容:

private void fixAfterDeletion(Entry<K,V> x) {
        while (x != root && colorOf(x) == BLACK) {
            if (x == leftOf(parentOf(x))) {
                Entry<K,V> sib = rightOf(parentOf(x));

                if (colorOf(sib) == RED) {
                    setColor(sib, BLACK);
                    setColor(parentOf(x), RED);
                    rotateLeft(parentOf(x));
                    sib = rightOf(parentOf(x));
                }

                if (colorOf(leftOf(sib))  == BLACK &&
                    colorOf(rightOf(sib)) == BLACK) {
                    setColor(sib, RED);
                    x = parentOf(x);
                } else {
                    if (colorOf(rightOf(sib)) == BLACK) {
                        setColor(leftOf(sib), BLACK);
                        setColor(sib, RED);
                        rotateRight(sib);
                        sib = rightOf(parentOf(x));
                    }
                    setColor(sib, colorOf(parentOf(x)));
                    setColor(parentOf(x), BLACK);
                    setColor(rightOf(sib), BLACK);
                    rotateLeft(parentOf(x));
                    x = root;
                }
            } else { // symmetric
                Entry<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);
    }

 

 

红黑树的运用在java1.8种是非常广泛的,很多集合类型中都包含了红黑树的运用。感兴趣的同学可以通过treemap中的相关代码进行学习。源码永远是最好的老师。即便某些过程的原理不了解,我们也可以加深对某段代码或者某个数据结构的理解。

posted @ 2017-10-09 05:10  霄十一郎  阅读(583)  评论(0编辑  收藏  举报