红黑树简介,以及ConcurrentHashMap如何平衡红黑树

4 篇文章 0 订阅

 

ConcurrentHashMap基础

1,ConcurrentHashMap维护了一个Node数组(JDK1.8),保存了各节点链表的头节点。

2,当链表长度超过8时,ConcurrentHashMap会考虑把链表转为红黑树,但不一定真的转。

3,当链表长度超过8,但Node数组长度小于64时,优先考虑数组扩容。如果Node数组长度大于64,则把链表转为红黑树。

 

红黑树基础

红黑树是一种近似平衡的二叉查找树,它并非绝对平衡,但是可以保证任何一个节点的左右子树的高度差不会超过二者中较低的那个的一倍。

红黑树有这样的特点:

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

2,根节点必须是黑色。叶子节点必须是黑色NULL节点。

3,红色节点不能连续。

4,对于每个节点,从该点至叶子节点的任何路径,都含有相同个数的黑色节点。

5,能够以O(log2(N))的时间复杂度进行搜索、插入、删除操作。此外,任何不平衡都会在3次旋转之内解决。

 

ConcurrentHashMap在insert元素时如何平衡红黑树

ConcurrentHashMap在insert元素后,会调用balanceInsertion()方法来让红黑树重新恢复平衡。

balanceInsertion()方法来自ConcurrentHashMap的内部类TreeBin,这个类保有红黑树根节点引用,也记录了锁状态。

下面看一下balanceInsertion()方法的代码,然后总结一下大概的逻辑:

static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,    //root是根节点
                                            TreeNode<K,V> x) {     //x是要插入的节点
    x.red = true;    //节点插入时默认为红色

    /* 变量说明:
     * xp:父节点
     * xpp:祖父节点
     * xppl:祖父节点的左子节点
     * xppr:祖父节点的右子节点
     */
    for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
        if ((xp = x.parent) == null) {    //父节点是null,说明当前节点是根节点,设为黑色,并返回
            x.red = false;
            return x;
        }
        else if (!xp.red || (xpp = xp.parent) == null)    //父节点是黑色,或者祖父节点为空,可以直接插入并返回
            return root;
        if (xp == (xppl = xpp.left)) {    //父节点是祖父节点的左子节点(逻辑到这父节点只能是红色了)
            if ((xppr = xpp.right) != null && xppr.red) {    //祖父节点的右子节点(也就是叔叔节点,因为左子节点是父节点)不为空且为红色,直接交换颜色
                xppr.red = false;    //叔叔节点设为黑色
                xp.red = false;      //父节点设为黑色
                xpp.red = true;      //祖父节点设为红色
                x = xpp;             //祖父节点赋值给当前节点,准备下一循环
            }
            else {    //祖父节点的右子节点为空或为黑色
                if (x == xp.right) {    //当前节点为右子节点(也就是祖父节点的左子节点的右子节点,此为内侧插入),先左旋,后面右旋
                    root = rotateLeft(root, x = xp);    //以父节点为中心左旋
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                if (xp != null) {    //当前节点是左子节点(也就是祖父节点的左子节点的左子节点,此为外侧插入),或者前面左旋了,这里右旋
                    xp.red = false;
                    if (xpp != null) {
                        xpp.red = true;
                        root = rotateRight(root, xpp);    //以祖父节点为中心右旋
                    }
                }
            }
        }
        else {    //这里是父节点是祖父节点的右子节点的情况,和上面逻辑差不多
            if (xppl != null && xppl.red) {    //祖父节点的左子节点(也就是叔叔节点,因为右子节点是父节点)不为空且为红色,直接交换颜色
                xppl.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            else {    //祖父节点的左子节点为空或为黑色
                if (x == xp.left) {    //当前节点为左子节点(也就是祖父节点的右子节点的左子节点,此为内侧插入),先右旋,后面左旋
                    root = rotateRight(root, x = xp);    //以父节点为中心右旋
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                if (xp != null) {    //当前节点是右子节点(也就是祖父节点的右子节点的右子节点,此为外侧插入),或者前面右旋了,这里左旋
                    xp.red = false;
                    if (xpp != null) {
                        xpp.red = true;
                        root = rotateLeft(root, xpp);    //以祖父节点为中心左旋
                    }
                }
            }
        }
    }
}

总结一下的话,大概是这样的:

循环开始

if 是根节点:直接置黑,返回

if 父节点为黑色或祖父节点为空:直接插入,返回

if 父节点为红

    if 父节点为祖父节点的左子节点

        if 叔父节点存在且为红:直接变色(父节点和叔父节点变黑,祖父节点变红),下一循环将处理祖父节点

        else if 当前节点为右节点(此为内侧插入):左旋(后面右旋),下一循环将处理父节点

             else 当前节点为左节点(此为外侧插入)或前面左旋了:先变色(父节点变黑,祖父节点变红),然后右旋

    else 父节点为祖父节点的右子节点

        if 叔父节点存在且为红:直接变色,父节点和叔父节点变黑,祖父节点变红,下一循环将处理祖父节点

        else if 当前节点为左节点(此为内侧插入):右旋(后面左旋),下一循环将处理父节点

             else 当前节点为右节点(此为外侧插入)或前面左旋了:先变色(父节点变黑,祖父节点变红),然后左旋

循环的出口:当前节点为根节点,或父节点为黑色,或祖父节点为空

 

一个例子

如果我们要依次往红黑树中插入12,1,9,2,图示如下:

注意:本文的图中都没画出叶子节点

 

关于左旋的逻辑

1,当前节点X是右子节点时才需要左旋

2,父节点也是红色时才需要左旋

3,左旋后,父节点成为X的左子节点,X的左子节点成为父节点的右子节点

画个图来看就是这样的:

从图上看,逻辑并不复杂,左旋也就是逆时针旋转的含义也很形象,但是代码写出来就让人眼花缭乱,因为代码要考虑各处null的判断,而且要完成父子关系的双向指向,即显式指定A的左(或右)子节点是B,且B的父节点是A。

方法代码是这样的:

static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
                                      TreeNode<K,V> p) {
    TreeNode<K,V> r, pp, rl;
    if (p != null && (r = p.right) != null) {    //1 r是p的右子节点,如果r是null就不用左旋了
        if ((rl = p.right = r.left) != null)     //2 rl是r的左子节点,把p的右子节点设为rl,如果rl不是null,说明r存在非叶子左子节点
            rl.parent = p;                           //把p设为rl的父节点
        if ((pp = r.parent = p.parent) == null)  //3-1 pp是p的父节点,把pp设为r的父节点,如果pp是null,表示p是root
            (root = r).red = false;                  //把r设为root,改成黑色
        else if (pp.left == p)                   //3-2 如果pp不是null,而且p是pp的左子节点
            pp.left = r;                             //把r设为pp的左子节点
        else					 //3-3 如果pp不是null,而且p是pp的右子节点
            pp.right = r;                            //把r设为pp的右子节点
        r.left = p;                              //4 把p设为r的左子节点
        p.parent = r;                            //5 把r设为p的父节点 
    }
    return root;                                 //返回root,有可能是最开始的root,也有可能是顶替了原root(也就是p)的r
}

 

关于右旋的逻辑

1,当前节点X是左子节点时才需要右旋

2,父节点也是红色时才需要右旋

3,右旋后,父节点成为X的右子节点,X的右子节点成为父节点的左子节点

画个图来看就是这样的:

方法代码是这样的:

static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
                                       TreeNode<K,V> p) {
    TreeNode<K,V> l, pp, lr;
    if (p != null && (l = p.left) != null) {     //1 l是p的左子节点,如果l是null就不用右旋了
        if ((lr = p.left = l.right) != null)     //2 lr是l的右子节点,把p的左子节点设为lr,如果lr不是null,说明l存在非叶子右子节点
            lr.parent = p;                           //把p设为lr的父节点
        if ((pp = l.parent = p.parent) == null)  //3-1 pp是p的父节点,把pp设为l的父节点,如果pp是null,表示p是root
            (root = l).red = false;                  //把l设为root,改成黑色
        else if (pp.right == p)                  //3-2 如果pp不是null,而且p是pp的右子节点
            pp.right = l;                            //把l设为pp的右子节点
        else                                     //3-3 如果pp不是null,而且p是pp的左子节点
            pp.left = l;                             //把l设为pp的左子节点
        l.right = p;                             //4 把p设为l的右子节点
        p.parent = l;                            //5 把l设为p的父节点
    }
    return root;                                 //返回root,有可能是最开始的root,也有可能是顶替了原root(也就是p)的l
}

 

本文结束

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值