Java集合框架分析(Map)——红黑树的自平衡机制详解

目录


一.摘要
TreeMap是基于红黑树结构实现的一种Map,要分析TreeMap的实现首先就要对红黑树有所了解。红黑树是一种自平衡二叉查找树[1]。理论上,一颗平衡的二叉查找树的任意节点平均查找效率为树的高度h,即O(log n).但是如果二叉查找树失去平衡(元素全在一侧),搜索的效率就退化为O(n),因此二叉查找树的平衡是搜索效率的关键所在,而红黑树就是靠自平衡机制(红黑规则)来维持二叉查找树的平衡性。

【注1】:二叉查找树满足一下特点:
● 若左子树不为空,则左子树上所有节点的值均小于它的根节点的值;
● 若右子树不为空,则右子树上所有节点的值均大于它的根节点的值
● 左、右子树也分别为二叉查找树
● 没有键值相等的节点


二.红黑树

具体来说,红黑树是满足如下条件的二叉查找树(binary search tree):

1.每个节点要么是红色,要么是黑色。
2.根节点必须是黑色。
3.红色节点不能连续(也就是,红色节点的孩子和父亲都不能是红色)。
4.对于每个节点,从该点至null(树尾端)的任何路径,都含有相同个数的黑色节点。
5.每个叶节点(null)都是黑色的。
6.新加入到红黑树的节点为红色节点。

红黑树也是均衡二叉树,需要具备自动维持平衡的性质,上面的六条就是红黑树给出的自动维持平衡所需要具备的规则。典型的红黑树如下图所示:

红黑树的性质
☞ 从根节点到叶子结点的最长路径不大于最短路径的2倍

最短路径:纯由黑色节点组成的路径就是最短路径。
最长路径:根据规则3和规则4,若有红色节点,则必然有一个连接的黑色节点,当红色节点和黑色节点相同时,就是最长路径,也就是黑色节点(或红色节点)* 2。

☞ 新加入到红黑树的节点为红色节点
根据规则3,当前红黑树从根节点到每个叶子结点的黑色节点数量是一样的,此时加入新的节点是黑色节点的话,必然破坏规则,但加入红色节点却不一定,除非其父节点就是红色节点,因此加入红色节点,破坏规则的可能性小一些。

三.红黑树的自平衡操作

3.1 红黑树插入元素后的自平衡操作

红黑树维持平衡主要通过两种方式【变色】和【旋转】,【旋转】又分【左旋】和【右旋】。红黑树的自平衡操作对应条件如下表所示:

无需调整【变色】即可实现平衡【旋转】+【变色】才可实现平衡
1.当父节点为黑色时插入子节点2.空树插入根节点,将根节点红色变成黑色4.父节点为红色左节点,叔父节点 [2] 为黑色,插入左子节点,通过【左左节点旋转】
-3.父节点和叔父节点都为红色5.父节点为红色左节点,叔父节点为黑色,插入右子节点,那么通过【左右节点旋转】
--6.父节点为红色右节点,叔父节点为黑色,插入左子节点,那么通过【右左节点旋转】
--7.父节点为红色右节点,叔父节点为黑色,插入右子节点,那么通过【右右节点旋转】

【注2】叔父节点指一个节点的父节点的兄弟节点

左旋:
逆时针旋转两个节点,让一个节点被其右子节点取代,而该节点成为右子节点的左子节点。
左旋操作步骤如下:
首先断开节点PL与右子节点G的关系,同时将其右子节点的引用指向节点C2;然后断开节点G与左子节点C2的关系,同时将G的左子节点的应用指向节点PL。

右旋:
顺时针旋转两个节点,让一个节点被其左子节点取代,而该节点成为左子节点的右子节点。
右旋操作步骤如下:
首先断开节点G与左子节点PL的关系,同时将其左子节点的引用指向节点C2;然后断开节点PL与右子节点C2的关系,同时将PL的右子节点的应用指向节点G。

3.1.1 当父节点为黑色时插入子节点
在左下图所示红黑树中插入59时,根据规则6,无须进行自平衡调整,结果如右下图所示:

添加59后的二叉查找树满足六条规则,故该树为红黑树。

3.1.2 空树插入根节点
根据规则2和规则6,需要将新插入红色节点变色为黑色的根节点。

3.1.3 父节点和叔父节点都为红色
在左下图所示红黑树中插入49,根据规则6,结果如右下图所示:

易知,此二叉查找树不满足规则3,新插入节点的父节点和叔父节点都为红色节点,需要对该树进行变色操作,如下图所示:

♦ 首先解决结构不遵守规则3(红色节点不能连续,节点50-49),需要将50改为黑色。
♦ 此时结构不遵守4(53-51-50-49-null路径上黑色节点个数为3,53-54-null路径上黑色节点个数为2),将51改为红色。
♦ 此时结构不遵守规则3(红色节点不能连续,节点53-51-52),将53改为黑色,52改为黑色。
♦ 此结构满足红黑树规则。

最终调整完成后的树为:

3.1.4 父节点为红色左节点,叔父节点为黑色,插入左子节点
旋转原始图如左下所示,在该树中插入65,规则: 以祖父节点【右旋】,搭配【变色】(左左节点旋转),自平衡过程如下图所示:

3.1.5 父节点为红色左节点,叔父节点为黑色,插入右子节点
旋转原始图如下所示,在该树中插入67,规则: 先父节点【左旋】,然后祖父节点【右旋】,搭配【变色】(左右节点旋转),自平衡过程如下图所示:


3.1.6 父节点为红色右节点,叔父节点为黑色,插入左子节点
旋转原始图如下所示,在该树中插入68,规则: 先父节点【右旋】,然后祖父节点【左旋】,搭配【变色】(左右节点旋转),自平衡过程如下图所示:
在这里插入图片描述

3.1.7 父节点为红色右节点,叔父节点为黑色,插入右子节点
旋转原始图如下所示,在该树中插入70,规则: 先祖父节点【左旋】,搭配【变色】,自平衡过程如下图所示:


3.2 红黑树删除元素后的自平衡操作

红黑树删除的情况比较多,存在以下情况:
序号条件自平衡调节
1删除的是根节点直接将根节点置为null
2待删除节点的左右子节点都为null删除时将该节点置为null
3待删除节点的左右子节点有一个有值用有值的节点替换该节点
4待删除节点的左右子节点都不为null找前驱或者后继,将前驱或者后继的值复制到该节点中,然后删除前驱或者后继

3.2.1 删除的是根节点
直接将根节点置为null。

3.2.2 待删除节点的左右子节点都为null
当待删除节点为红色子节点,且该节点左右子节点都为null,如下图所示:(待删除元素为红色子节点69)

当待删除节点为黑色子节点,且该节点左右子节点都是null,如下图所示:(待删除元素为黑色子节点67)

3.2.3 待删除节点的左右子节点有一个有值
当待删除节点的左右子节点有且仅有一个有值,将有用的节点替换该节点,如下图所示:(待删除元素为69)

3.2.4 待删除节点的左右子节点都是非null节点
第一步:找到该节点的前驱或者后继
前驱:左子树中值最大的节点(可得出其最多只有一个非null子节点,可能都为null);
后继:右子树中值最小的节点(可得出其最多只有一个非null子节点,可能都为null);

前驱和后继都是值最接近该节点值的节点。

第二步:将前驱或者后继的值复制到该节点中,然后删掉前驱或者后继
如果删除的是左节点,则将前驱的值复制到该节点中,然后删除前驱;如果删除的是右节点,则将后继的值复制到该节点中,然后删除后继;

1.使用前驱替换

分析:
因为要删除的是左节点64,找到该节点的前驱63;
然后用前驱的值63替换待删除节点的值64,此时两个节点(待删除节点和前驱)的值都为63;
删除前驱63,此时成为上图过程中间环节,但我们发现其不符合红黑树规则4,因此需要进行自动平衡调整;

这里直接通过**【变色】**即可完成。

2.使用后继替换

分析:
因为要删除的左节点64,找到该节点的后继节点65;
然后用后继的值65替换待删除节点的值64,此时两个节点(待删除节点和后继)的值都为65;
删除后继65,此时成为上图过程中间环节,但我们发现其不符合红黑树规则4,因此需要进行自动平衡调整;


四.方法分析
在TreeMap中,使用 fixAfterInsertion() 方法来实现插入后红黑树的自平衡操作;
使 fixAfterDeletion() 方法来实现删除后红黑树的自平衡操作。

fixAfterInsertion源码分析
    private void fixAfterInsertion(Entry<K,V> x) {
        //设置结点的初始化颜色为红色
        x.color = RED;
        //如果当前结点x不为null,并且不为根结点root,并且当前结点的父结点是红色结点,则进行循环
        while (x != null && x != root && x.parent.color == RED) {
            //如果当前结点x的父结点是x的祖结点的左结点
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                //获取结点x的叔叔结点y,也就是x祖结点的右结点
                Entry<K,V> y = rightOf(parentOf(parentOf(x)));
                //如果叔结点y是红色
                if (colorOf(y) == RED) {
                    //设置x的父结点为黑色
                    setColor(parentOf(x), BLACK);
                    //设置x的叔结点y为黑色
                    setColor(y, BLACK);
                    //设置x的祖结点为红色
                    setColor(parentOf(parentOf(x)), RED);
                    //设置祖结点为x
                    x = parentOf(parentOf(x));
                //如果叔结点y是黑色
                } else {
                    //判断当前结点x是否是父结点的右结点
                    if (x == rightOf(parentOf(x))) {
                        //x的父结点作为x
                        x = parentOf(x);
                        //将原来的父结点进行左旋操作
                        rotateLeft(x);
                    }
                    //设置x的父结点为黑色
                    setColor(parentOf(x), BLACK);
                    //设置x的祖结点为红色
                    setColor(parentOf(parentOf(x)), RED);
                    //x的祖结点进行右旋操作
                    rotateRight(parentOf(parentOf(x)));
                }
            //如果当前结点x的父结点是x的祖结点的右结点
            } else {
                //获取x结点的祖结点的左结点,也就是x的叔叔结点赋值给y
                Entry<K,V> y = leftOf(parentOf(parentOf(x)));
                //如果叔结点y是红色的
                if (colorOf(y) == RED) {
                    //设置x结点的父结点为黑色
                    setColor(parentOf(x), BLACK);
                    //设置x结点的叔结点y为黑色
                    setColor(y, BLACK);
                    //设置x的祖结点为红色
                    setColor(parentOf(parentOf(x)), RED);
                    //获取x结点祖结点赋值给x
                    x = parentOf(parentOf(x));
                //如果叔结点y是黑色的
                } else {
                    //如果x结点是x父结点的左结点
                    if (x == leftOf(parentOf(x))) {
                        //取x结点的父结点赋值为x
                        x = parentOf(x);
                        //将x结点右旋
                        rotateRight(x);
                    }
                    //设置x结点的父结点为黑色
                    setColor(parentOf(x), BLACK);
                    //设置x结点的祖结点为红色
                    setColor(parentOf(parentOf(x)), RED);
                    //将x结点的祖结点进行左旋
                    rotateLeft(parentOf(parentOf(x)));
                }
            }
        }
        //始终让根结点保持黑色,这是红黑树的特性
        root.color = BLACK;
    }
fixAfterDeletion源码分析

为注释方便,特做此规定:
N: 当前待删除节点
P: N的父节点
S: N的兄弟节点,P的右子节点
Sl: S的左子节点
Sr: S的右子节点

对应关系如下图所示:

private void fixAfterDeletion(Entry<K,V> x) {
    while (x != root && colorOf(x) == BLACK) { // N节点是黑色节点并且不是根节点就一直循环
        if (x == leftOf(parentOf(x))) { // 如果N是P的左子节点
            Entry<K,V> sib = rightOf(parentOf(x)); // sib就是N节点的兄弟节点S
 
            if (colorOf(sib) == RED) { // 如果S节点是红色节点,满足删除冲突3.2,对P节点进行左旋操作并交换P和S的颜色
                // 交换P和S的颜色,S原先为红色,P原先为黑色(2个红色节点不能相连)
                setColor(sib, BLACK); // S节点从红色变成黑色
                setColor(parentOf(x), RED); // P节点从黑色变成红色
                rotateLeft(parentOf(x)); // 删除冲突3.2中P节点进行左旋
                sib = rightOf(parentOf(x)); // 左旋之后N节点有了一个黑色的兄弟节点和红色的父亲节点,S节点重新赋值成N节点现在的兄弟节点。接下来按照删除冲突3.4、3.5、3.6处理
            }
 
            // 执行到这里S节点一定是黑色节点,如果是红色节点,会按照冲突3.2交换成黑色节点
            // 如果S节点的左右子节点Sl、Sr均为黑色节点并且S节点也为黑色节点
            if (colorOf(leftOf(sib))  == BLACK &&
                colorOf(rightOf(sib)) == BLACK) {
                // 按照删除冲突3.3和3.4进行处理
                // 如果是冲突3.3,说明P节点也是黑色节点
                // 如果是冲突3.4,说明P节点是红色节点,P节点和S节点需要交换颜色
                // 3.3和3.4冲突的处理结果S节点都为红色节点,但是3.4冲突处理完毕之后直接结束,而3.3冲突处理完毕之后继续调整
                setColor(sib, RED); // S节点变成红色节点,如果是3.4冲突需要交换颜色,N节点的颜色交换在跳出循环进行
                x = parentOf(x); // N节点重新赋值成N节点的父节点P之后继续递归处理
            } else { // S节点的2个子节点Sl,Sr中存在红色节点
                if (colorOf(rightOf(sib)) == BLACK) { // 如果S节点的右子节点Sr为黑色节点,Sl为红色节点[Sl如果为黑色节点的话就在上一个if逻辑里处理了],满足删除冲突3.5
                    // 删除冲突3.5,对S节点做右旋操作,交换S和Sl的颜色,S变成红色节点,Sl变成黑色节点
                    setColor(leftOf(sib), BLACK); // Sl节点变成黑色节点
                    setColor(sib, RED); // S节点变成红色节点
                    rotateRight(sib); // S节点进行右旋操作
                    sib = rightOf(parentOf(x)); // S节点赋值现在N节点的兄弟节点
                }
                // 删除冲突3.5处理之后变成了删除冲突3.6或者一开始就是删除冲突3.6
                // 删除冲突3.6,P节点做左旋操作,P节点和S接口交换颜色,Sr节点变成黑色
                setColor(sib, colorOf(parentOf(x))); // S节点颜色变成P节点颜色,红色
                setColor(parentOf(x), BLACK); // P节点变成S节点颜色,也就是黑色
                setColor(rightOf(sib), BLACK); // Sr节点变成黑色
                rotateLeft(parentOf(x)); // P节点做左旋操作
                x = root; // 准备跳出循环
            }
        } else { // 如果N是P的右子节点,处理过程跟N是P的左子节点一样,左右对换即可
            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); // 删除冲突3.4循环调出来之后N节点颜色设置为黑色 或者 删除节点只有1个红色子节点的时候,将顶上来的红色节点设置为黑色
}

六.参考资料

面试常问:什么是红黑树?
关于红黑树(R-B tree)原理,看这篇如何
我的jdk源码(十九):TreeMap类 红黑树实现的map结构
红黑树之删除节点
红黑树旋转变色规则(最全面详细版)

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值