红黑树学习笔记(Java)

红黑树

记录的是个人学习过程,难免有些冗长,如果有理解有误的地方希望各位大大指正。

首先,红黑树是一种特殊的二叉查找树,他实现了自平衡的功能,这种功能实现的平衡不是绝对的平衡是一种相对平衡,他解决了,二叉查找树在极端情况下退化成链表的问题,提高了查找性能。

关于二叉查找树可以点击这里

性质

红黑树既然能比二叉查找树多实现自平衡的功能,那么肯定在二叉查找树的基础上多了一些规则。红黑树的规则如下:

  1. 每一个节点都有非黑即红的颜色。(反正就两种颜色,不过大家公知黑色红色的关系所以一般用黑色红色。)
  2. 根节点一定是黑色。
  3. 红节点的两个子节点一定是黑色节点。(不能有两个连续的红色节点)
  4. 每个节点到他所有的叶子节点的路径,经过的黑色节点数相同。
  5. 每个叶子节点(NIL)一定为黑色。(null节点)

红黑树的性质就如上了,红黑树的这些性质其实就是为了维护性质4,即黑色层数平衡。

假设左子树的黑色层数为lbh = n,那么右子树的黑色层数rbn = n;

那么左右子树的最大红色层数rhMax = n,最小红色层数为rhMin = 0;

树高为h = bh+rh,假设在最极端情况下,左子树高度为最大 lh = lbh + rhMax = 2n,右子树高度为最小rh = rbh + rhMin = n;

所以在最极端情况下左右子树高度之差也不会高于小的那边的一倍。

关于红黑树还有一个定理:一个含有n个节点的红黑树高度最多为2log(n+1);

最少节点时红黑树是一个黑色节点的完全二叉树,则最少节点个数为2^n - 1;

设黑色高度为bh,节点数为n,根据上式可得 n >= 2^bh - 1;
又设树高为h,根据红黑树性质可得 h <= 2bh,h/2 <=bh;
根据上两式可得 n >= 2^(2h) - 1;
n+1 >= 2^(2h);
log(n+1) >= 2h;
2log(n+1) >= h;
h <= 2log(n+1);

动作

上面提到了红黑树的规则,正是由于要保持这些规则所以红黑树才实现了自平衡的效果,而在我们操作红黑树时(如增加删除节点)一定会破坏红黑树的这些规则,而当我们破坏这些规则时红黑树会进行一系列的动作来维持自身的规则。一般有变色和旋转两种动作。

这里先说一下单纯的动作怎么操作,再讨论在那种情况下该怎么做,由于我学习是在Java语言的环境下学习的所以我们可以结合Java提供的红黑树基础实现TreeMap源码来学习。

变色

变色其实没什么好说的,就是将红色变为黑色,黑色变为红色。实质意义是当节点变为红色时那么黑色层数就相当于减少了1。

旋转

旋转可以分为左旋(逆时针)和右旋(顺时针),这里的左右旋转是以当前节点为支点来说的。旋转的实质意义就是将目标节点上升层数。

左旋

个人感觉口述很难描述清楚所以直接看如下图:

在这里插入图片描述

Java中TreeMap关于左旋的具体代码实现如下

    private void rotateLeft(Entry<K,V> p) {
        if (p != null) {
            Entry<K,V> r = p.right; //获取支点节点的右子节点,后面称他为前右子节点
            p.right = r.left; // 将前右子节点的左子树设置为支点节点的右子节点
            if (r.left != null) //如果前右子节点的左子节点不是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;
        }
    }
右旋

如图:

在这里插入图片描述

Java中TreeMap关于右旋的具体代码实现如下:

    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实现的修复代码来看看具体逻辑是怎么样的:

在TreeMap中修复操作分为了两类,一种插入后的修复,一种是删除后的修复

插入后的修复代码

    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;
    }

具体分析里面的分支结构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rtOzMCLK-1594670332476)(C:/Users/Administrator/Desktop/RedBlackTree/fixAfterInsert.png)]

再用图来理解下:

在这里插入图片描述

在这里插入图片描述

可以列表如下:

第一条件第二条件第三条件执行操作
当插入节点是根节点或NULL或插入节点的父节点是黑色时///
当插入节点的叔叔节点和父节点都是红色时//将叔叔节点和父节点变色为黑色。将祖父节点变为红色,并且再以祖父节点为判断中心进行判断
当插入节点的叔叔节点不是红色时当插入节点的父节点是右子节点时当插入节点是右子节点时将插入节点的父节点变为黑色,将祖父节点变为红色,以祖父节点为支点左旋,再以当前节点进行下次判断。
当插入节点的叔叔节点不是红色时当插入节点的父节点是右子节点时当插入节点是左子节点时将原父节点设置为当前节点,后续操作都以父节点为当前节点,再以当前节点为支点右旋,右旋后再将当前节点父节点设置为黑色,祖父节点设置为红色,再以祖父节点为支点左旋。以当前节点进入下次判断。
当插入节点的叔叔节点不是红色时当插入节点的父节点是左子节点时当插入节点是左子节点时将插入节点的父节点变为黑色,将祖父节点变为红色,以祖父节点为支点右旋,再以当前节点进行下次判断。
当插入节点的叔叔节点不是红色时当插入节点的父节点是左子节点时当插入节点是右子节点时将原父节点设置为当前节点,后续操作都以父节点为当前节点,再以当前节点为支点左旋,左旋后再将当前节点父节点设置为黑色,祖父节点设置为红色,再以祖父节点为支点右旋。以当前节点进入下次判断。

  总之当添加新节点后,当父节点为黑色时说明当前树的红色空位还没有满,(因为添加红色节点在没有冲突的情况下,不会影响红黑树要维护的黑色层数平衡。)且空位就在添加位置则不需要改变。如果父节点为黑色说明新节点位置没有空位此时需要到其他地方去寻找是否有空位。

  如果叔叔节点也为红色那么可以直接改变父节点和叔叔节点为黑色将寻找空位的节点上升到祖父节点(因为此时至少能保证以祖父节点为根的子树黑色层数时平衡的)。

  如果叔叔节点不为红色,则不能改变颜色因为会破坏原本的平衡此时需要用对应的旋转操作将新节点上升直至寻找到对应的空位,或者到根节点增加黑色层数。

删除后的修复代码

个人理解如果说添加节点后的修复是向上寻找空位,那么删除节点后的修复其实是将通过改节点的路径+1但又不改变其他路径黑色数。想了解删除后的修复,不能只看修复的代码,还要看看删除的代码。

删除代码

删除代码就不用多说了直接看代码就行。

    public V remove(Object key) {
        Entry<K,V> p = getEntry(key); //找到要删除的节点
        if (p == null)
            return null;

        V oldValue = p.value;
        deleteEntry(p);//删除节点
        return oldValue;
    }
    
    //寻找后继节点(将树压扁成数组,数组状态该节点的后一个就是后继节点,就是说树中大于本身的节点中最小的一个)
    static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
        if (t == null)//如果删除节点为空,那直接返回空就行了
            return null;
        else if (t.right != null) {//如果删除节点存在右子节点,寻找右子树中最小的节点作为后继节点即可。
            Entry<K,V> p = t.right;
            while (p.left != null)
                p = p.left;
            return p;
        } else {//如果不存在右子节点,则从当前节点开始向上找,找到一个最大的左子树的根节点,以这个根节点为后继节点。
            Entry<K,V> p = t.parent;
            Entry<K,V> ch = t;
            while (p != null && ch == p.right) {
                ch = p;
                p = p.parent;
            }
            return p;
        }
    }
    
    private void deleteEntry(Entry<K,V> p) {
        modCount++;
        size--;

        if (p.left != null && p.right != null) {//p有两个孩子的时候
            Entry<K,V> s = successor(p);
            p.key = s.key;
            p.value = s.value;
            p = s;
            //寻找p的后继节点,并将p指向后继节点。
            //删除的根本操作就是找到后继节点与本身交换后删除后继节点。(颜色不变)
            //所以当存在后继节点时实际上后继节点才是要删除的节点。            
        }

        //寻找替代节点,替代节点优先左子节点。(删除了一个节点后替代他的节点)
        Entry<K,V> replacement = (p.left != null ? p.left : p.right);
        //如果找到了替代节点,说明删除节点至少有一个子节点。
        if (replacement != null) {
            //先将替代节点与父节点链接上
            replacement.parent = p.parent;
            if (p.parent == null)
            //如果p为根节点,直接设置根节点就行。
            //此处p不可能存在两个节点,看寻找后继节点那里的逻辑
                root = replacement;
            else if (p == p.parent.left)//当删除节点时左子节点时,替换对应位置
                p.parent.left  = replacement;
            else
                p.parent.right = replacement;

            // 清空删除节点的链接,方便后面修复
            p.left = p.right = p.parent = null;

            // 当删除的节点是黑色节点时我们从他以前的位置开始修复
            //红色不用修复
            if (p.color == BLACK)
                fixAfterDeletion(replacement);
        } else if (p.parent == null) {
        //找不到替代节点说明没有孩子,父节点为空说明时根节点,此时直接删除就行
            root = null;
        } else {
        //如果没有孩子,但又不时根节点,我们用他自身作成一个幻影替代节点
            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;
            }
        }
    }    
删除后修复代码
    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 {//镜像
                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.

假设我们其他路径上黑色节点数量 oldBlack = n;

那么此时经过了删除节点的路径上黑色节点就会变成 delBlack = n - 1;

此时我们得想办法在不改变oldBlack的情况下,让delBlack+1从而修复平衡。

而怎么判断该怎么修改呢,就要从兄弟节点开始判断。

根据红黑树性质(每个节点到他所有的叶子节点的路径,经过的黑色节点数相同。),我们可以知道,从父节点开始,经过兄弟节点的路径,和经过删除节点的路径平衡。而删除节点被删除且为黑色节点造成了不平很,这样我们就知道造成不平衡的源头在这里,要解决问题只要在不改变兄弟节点路径的黑色节点个数的情况下,为删除节点路线的黑色数增加1就行了,或者让全部路径都变为n-1;

此时如果兄弟节点是黑色那么我们可以直接让他变为红色,相当于经过兄弟节点的路径也变为了n - 1;

这样我们可以保证子树平衡,再利用同样的道理将判断中心向上迭代。根据判断中心的条件的不同再进行其他操作,知道遇到根节点或者红色节点时可以完成修复,遇到根节点说明说明所有的路径都变成了n - 1;遇到红色节点直接让红色节点变为黑色节点就可以将兄弟节点和删除节点的路径又变为n;

那么当兄弟节点不为黑色为红色了,或者说兄弟节点变红会破坏树平衡(兄弟节点的子节点是红色的),此时不能直接变色来减少兄弟节点的路径让子树平衡了,我们就需要用旋转了。(思路渐渐清晰了)

接着看看具体是怎么做的。

情况四:后继节点为黑色,兄弟节点也是黑色,但兄弟节点的子节点不都为黑

在这里插入图片描述

情况五:后继节点为黑色,兄弟节点是红色

这种情况下,和其他情况相比,只是多执行了一个提前准备的代码,如下

                if (colorOf(sib) == RED) {
                    setColor(sib, BLACK); //即兄弟黑色路径变为n+1
                    setColor(parentOf(x), RED); //即删除黑色路径 n-2;兄弟黑色路径 n;
                    rotateLeft(parentOf(x));//因为这里是删除节点为左子节点时的代码,所以这里其实是将兄弟节点上升作为父节点
                    //效果删除路径变为n-1;兄弟路径变为n;
                    sib = rightOf(parentOf(x));//前兄弟节点的右子节点变为了兄弟节点
                    //由红黑树性质可知,此时的sib一定为黑
                    //所以这段代码的实际效果是通过旋转,
                    //在不改变现有黑色路径的情况下 //改变兄弟节点为黑色的节点
                }

之后的处理步骤就和上述一系列情况一样判断修复了。

至此总算是对红黑树有了一定的了解了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值