平衡二叉树之红-黑树学习

1、红黑树

平衡二叉树(最早的AVL树)的劣势在于:

  • 删除:对于平衡二叉树来说,在最坏情况下,需要维护从被删节点到根节点这条路径上所有节点的平衡性,旋转的量级是OlogN。但是红黑树就不一样了,最多只需3次旋转就会重新平衡,旋转的量级是O(1)。
  • 保持平衡:平衡二叉树高度平衡,这也就意味着在大量插入和删除节点的场景下,平衡二叉树为了保持平衡需要调整的频率会更高。
    所以在大量查找的情况下,平衡二叉树的效率更高,也是首要选择。在大量增删的情况下,红黑树是首选。

就是因为平衡ALV树每次维护结点的平衡执行的旋转频率过高,不适合于大量增删的需求,才提出了红黑树。红黑树相对于AVL树来说,牺牲了部分平衡性以换取插入/删除操作时少量的旋转操作,整体来说性能要优于AVL树。
在这里插入图片描述

1.1、什么是红黑树(5个性质)

红黑树是每个节点都带有颜色属性的二叉查找树,颜色为红色或黑色(只要是两种不同的状态即可)。在满足二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求

  • 1、节点是红色或黑色。
  • 2、根是黑色。
  • 3、所有叶子都是黑色(叶子是NIL节点)。(java里边是用null表示NIL节点)
  • 4、每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
  • 5、从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

黑色高度
从根节点到叶节点的路径上黑色节点的个数,叫做树的黑色高度。所以性质5也可以描述为从根节点到叶节点路径的黑色高度必须相同。

小结性质
性质 4 的意思是:从每个根到节点的路径上不会有两个连续的红色节点,但黑色节点是可以连续的。
因此若一条路径上给定黑色节点的个数 N,最短路径的情况是连续的 N 个黑色,树的高度为 N - 1;最长路径的情况为节点红黑相间,树的高度为 2(N - 1) 。
性质 5 是成为红黑树最主要的条件,红黑树的插入、删除操作都是为了遵守这个规定。
红黑树并不是标准平衡二叉树,它以性质 5 作为一种平衡方法,使自己的性能得到了提升。

红黑树例子:
在这里插入图片描述
它的统计性能要好于平衡二叉树(AVL树),因此,红黑树在很多地方都有应用。比如在 Java 集合框架中,很多部分(HashMap, TreeMap, TreeSet 等)都有红黑树的应用,这些集合均提供了很好的性能。

1.2、红黑树的实现

我们知道平衡二叉树最关键的是保持其平衡,那么平衡是要通过旋转来实现的。而红黑树不仅要实现自平衡还有遵循红黑规则(5个性质),那么我们就不难推出:红黑树是通过旋转和节点的颜色变换来完成自平衡的

1.2.1、旋转

同AVL树一样,分左旋右旋。下面我们查看以下TreeMap左旋右旋的源码:

1、左旋
在这里插入图片描述

 /** From CLR */
    private void rotateLeft(Entry<K,V> p) {//左旋,对指定结点p的左旋
        if (p != null) {
            Entry<K,V> r = p.right;//获取失衡点A的右孩子c
            p.right = r.left;//失衡点A的右孩子变更为原来右孩子c的左孩子D
            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;//失衡点A原来的右孩子C变为根节点(相对来说),其左孩子变更为失衡点A
            p.parent = r;
        }
    }

2、右旋
在这里插入图片描述

    /** From CLR */
    private void rotateRight(Entry<K,V> p) {//右旋,对指定结点p的右旋
        if (p != null) {
            Entry<K,V> l = p.left;//获取失衡点A的左孩子B
            p.left = l.right;//失衡点A的左孩子变更为其原来的左孩子B的右孩子E
            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;//失衡点A原来的左孩子B变为根节点,其右孩子变为失衡点A
            p.parent = l;
        }
    }

1.2.2、颜色变换

我们插入节点势必要考虑如何插入能保证红黑树的5个性质不变,也就是说我们插入必须遵循这5个规则。有些插入情况必须要对之前的节点进行颜色变换。

1.3、红黑树的插入

红黑树的插入主要分两步:

  • 1、首先和二叉查找树的插入一样,查找、插入,利用了递归,可以参考查找算法
  • 2、然后调整结构,保证满足红黑树状态
    • 2.1、对结点进行重新着色(颜色变换
    • 2.2、以及对树进行相关的旋转操作

红黑树的插入在二叉查找树插入的基础上,为了重新恢复平衡,继续做了插入修复操作。
下面我们考虑以下插入时如何解决着色的问题。

类型 分析
①红黑树为空,插入为根节点 由规则1,直接染色为黑色
②红黑树不空,插入节点为子节点
1、插入节点的父节点为红色 如果插入红色违背规则5;插入黑色,违背规则4、5
2、插入节点的父节点为黑色 如果插入红色,不违背规则5;插入黑色,违背规则5

从上表我们可以发现,无论父节点是红色还是黑色,插入黑色都会违背规则5,可能违背规则4,必然要进行修复。而插入红色有可能违背规则5,也有可能不违背而不需要进行修复。那么从逻辑上我们肯定是优先考虑插入红色,然后把问题简化为——什么情况下插入红色节点会破坏红黑树结构性质而需要修复?怎么修复?

因此我们的插入规则是:插入节点是根节点,则插入节点为黑色。不是,则插入节点颜色为红色。这个时候我们只需要关心父节点是否为红色。

下面约定一下我习惯的叫法(不喜勿喷。。):
双亲Parent我喜欢叫父节点,父节点兄弟叫Uncle,父节点的孩子我叫Son,父节点的父节点我用G表示爷爷。

1.4、左子树的插入节点

情况1、父节点为黑色,插入红色节点

情况1
我们插入节点为红色,不违背5个规则。

情况2、父节点为红色,插入红色节点

  • 是在左子树插入
  • 是在右子树插入
    因为二叉树是对称的。执行的操作是相对的。我们搞定了右边,必然能搞定左边。

我喜欢从左边开始考虑,那就先学左边的吧。

情况2.1 父节点的兄弟,Uncle也是红色,祖父必为黑色

情况2.1
在这里插入图片描述
红色节点的孩子不能是红色,这时不管 Son 是 父节点F 的左孩子还是右孩子,只要同时把 父节点F 和 Uncle节点U 染成黑色,爷爷G 染成红色即可。这样这个子树左右两边黑色个数一致,也满足特征 4。

但是这样改变后 G 染成红色,G 的父亲如果是红色岂不是又违反特征 4 了?
因此需要从插入节点往上,一直检查,如以 节点 G 为新的调整节点,再次进行调整操作,以此循环,直到父亲节点不是红的,就没有问题了。

情况2.2 父节点的兄弟,Uncle是黑色,祖父必为黑色

第一种 插入的是左孩子

插入红色节点Son,违背规则4,但是单纯依靠颜色变换,发现将Uncle节点U变为红色,也就是把该条路径的黑色高度减少了1,违背了规则5。那如果把F涂成黑色,又会导致F所在路径黑色高度加一。那么该怎么办呢?
在这里插入图片描述

这个时候,我们需要用到旋转了。在这里插入图片描述从而满足了规则。

第二种 插入的是右孩子(先转变为第一种)

当插入的节点Son是F的右孩子时,同平衡二叉树AVL类似,需要先左旋,转化为上述的情况,然后进行同样的操作。
在这里插入图片描述
这个时候,就相当于Son变为了父节点,插入节点为F,插入节点为左孩子的情况。即第一种情况。

1.5、右子树的插入节点(与左子树操作相对即可)

右子树的插入则执行相反操作即可。

1.6、代码实现(TreeMap源码)

  /** From CLR */
    private void fixAfterInsertion(Entry<K,V> x) {//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)));//获取Uncle
                if (colorOf(y) == RED) {//判断Uncle的颜色,红色就是第一种情况,只需要将父亲和Uncle设置为黑色。祖父变为红色,然后一直向上检测
                    setColor(parentOf(x), BLACK);//父节点黑色
                    setColor(y, BLACK);//Uncle黑色
                    setColor(parentOf(parentOf(x)), RED);//爷爷节点黑色
                    x = parentOf(parentOf(x));//将当前节点更新为爷爷节点,继续检测
                } else {//Uncle是黑色,先得判断x是左孩子插入还是右孩子插入
                    if (x == rightOf(parentOf(x))) {//右孩子插入,则额外需要对父亲左旋一次,化简为第一种情况:Son变为父亲,父亲变为Son;
                        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)));//获取Uncle
                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;//根节点保证为黑色。
    }

1.7 红黑树的删除节点

我们先来回顾以下二叉查找树是如何删除节点的:

情况类型 处理方法
情况1:要删除的节点P的子树都是空 直接删除P节点即可
情况2:要删除的节点P的子树只有一颗子树是空的 将删除节点P的位置替换为其非空子树
情况3:要删除的节点P的两颗子树都不空 先找到P节点的后继结点S,交换S和P节点,转化为了情况1或者2.

回顾以下知识点就开始了。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
TreeMap删除节点源码

private void deleteEntry(Entry<K,V> p) {
        modCount++;
        size--;

        // If strictly internal, copy successor's element to p and then make p
        // point to successor.
        if (p.left != null && p.right != null) {//情况3,左右子树不空则需要转化
            Entry<K,V> s = successor(p);//获取后继结点
            p.key = s.key;//设置键、值
            p.value = s.value;
            p = s;//交换
        } // p has 2 children

        // Start fixup at replacement node, if it exists.
        Entry<K,V> replacement = (p.left != null ? p.left : p.right);//判断情况1还是情况2

        if (replacement != null) {//情况2,有一颗子树不空,替代要删除的节点即可。
            // Link replacement to parent
            replacement.parent = p.parent;
            if (p.parent == null)
                root = replacement;
            else if (p == p.parent.left)
                p.parent.left  = replacement;
            else
                p.parent.right = replacement;

            // Null out links so they are OK to use by fixAfterDeletion.
            p.left = p.right = p.parent = null;//删除节点

            // Fix replacement
            if (p.color == BLACK)
                fixAfterDeletion(replacement);
        } else if (p.parent == null) { // return if we are the only node.只有一个节点
            root = null;
        } else { //  No children. Use self as phantom replacement and unlink.情况1,没有子树
            if (p.color == BLACK)
                fixAfterDeletion(p);

            if (p.parent != null) {
                if (p == p.parent.left)
                    p.parent.left = null;
                else if (p == p.parent.right)
                    p.parent.right = null;
                p.parent = null;
            }
        }
    }

1.8、删除节点以后重新调整颜色?(超重要的理解)

首先我们先利用枚举:

  • 删除的节点是红色,情况1、2下——>不会违背规则,不需要调整;而情况3下,会转化为情况1或者情况2进行删除,也不违背规则,不需要调整。
  • 删除的节点是黑色——>违背了规则5,减少了某些路径上的黑色高度,需要调整

所以我们只需要考虑当删除的节点是黑色时,如何调整即可。

调整策略分析

为了保证删除节点父节点左右两边黑色节点数一致,需要重点关注父节点没删除的那一边节点是不是黑色(即考虑删除节点的兄弟节点那边的树)。如果删除后父节点另一边比删除的一边黑色节点多,就要想办法搞到平衡,具体的平衡方法有如下几种方法:

  • 把父节点另一边(即删除节点的兄弟树)其中一个节点弄成红色,实现让兄弟树也少一个黑色
  • 或者把另一边多的黑色节点转过来一个

删除节点在父节点的左子树还是右子树,调整方式都是对称的,这里以当前节点为父节点的左孩子为例进行分析。

第一步

如果删除节点(黑色节点如12)以后的X的兄弟是红色,则兄弟的儿子都是黑色,执行图中操作:
在这里插入图片描述
进入第二步。

第二步

1、如果X现在的兄弟是黑色,且兄弟的两个孩子都是黑色,执行如图操作。然后跳到第三步。
在这里插入图片描述

注意这一类型需要将X更新为其父节点。

2、如果X现在的兄弟Y是黑色,兄弟节点Y的孩子至多有一个是黑的,执行如图操作,然后跳到第三步。(这一步必然让X指向根节点
在这里插入图片描述
这是2类的实例图片,不同于上面的图。
在这里插入图片描述

第三步

  • 如果研究的不是根节点并且是黑的,重新进入第一种情况,研究上一级树;
  • 如果研究的是根节点或者这个节点不是黑的,就退出
    把研究的这个节点涂成黑的。
    在这里插入图片描述
    流程图实现理解:
    在这里插入图片描述

1.9、TreeMap的删除后调整的源码:

  /** From CLR */
    private void fixAfterDeletion(Entry<K,V> x) {
        while (x != root && colorOf(x) == BLACK) {//删除节点以后的当前节点为黑色才需要调整
            if (x == leftOf(parentOf(x))) {//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);//更新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)));//兄弟节点与X节点的父节点一致
                    setColor(parentOf(x), BLACK);//设置X节点为黑色
                    setColor(rightOf(sib), BLACK);//兄弟右孩子设置为黑色
                    rotateLeft(parentOf(x));//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);//根节点或者研究节点设置为黑色。
    }

完全和分析一致。很棒。

1.10、对比AVL树、一般的二叉查找树

来吧,让我们来看一下知乎大佬说的:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

参考博客:
https://juejin.im/entry/58371f13a22b9d006882902d#comment
维基百科
https://www.jianshu.com/p/e136ec79235c
https://blog.csdn.net/u012142247/article/details/80250166

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章