认真研究HashMap中的平衡插入

关联博文:
数据结构之Map基础入门与详解
认真学习Java集合之HashMap的实现原理
认真研究HashMap的读取和存放操作步骤
认真研究HashMap的初始化和扩容机制
认真研究JDK1.7下HashMap的循环链表和数据丢失问题
认真研究HashMap中的平衡插入
认真研究HashMap的结点移除
认真研究HashMap中的平衡删除

本文是基于Jdk1.8,关于平衡插入这一部分涉及的内容比较多,所以从博文认真学习Java集合之HashMap的实现原理摘取出来单独研究。

下面方法参数中的root为当前root结点,x 为新插入的结点 。

  • xp是x.parent,也就是父亲结点
  • xpp 为xp的parent,也就是祖父结点
  • xppl 为xpp的left,可能是XP也可能是叔叔结点;
  • xppr 为 xpp的right ,可能是XP也可能是叔叔结点
  • 这个过程会涉及到红黑树的左旋和右旋。
// root为当前root结点,x 为新插入的结点 
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                           TreeNode<K,V> x) {
	//插入结点颜色首先默认是红色
   x.red = true;

	//无限循环,直到return
   for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
   		
   		// 如果parent为null,说明是根节点,直接返回x,颜色为黑色
       if ((xp = x.parent) == null) {

       	  //这一步很关键,根节点强制调整为黑色	
           x.red = false;
           return x;
       }
     
       //如果xp颜色为黑色,或者xpp为null,则无需处理,返回root
       else if (!xp.red || (xpp = xp.parent) == null)
           return root;

//xp的颜色是红色且xpp不为null
		//XPP的左侧分支
       if (xp == (xppl = xpp.left)) {
       		
       		//第一种情况
       		//如果xppr.red 也就是祖父结点的右孩子,即叔叔结点为红色
       		//这种情况需要调整xp、xppr的颜色
           if ((xppr = xpp.right) != null && xppr.red) {
               xppr.red = false;//调整为黑色
               xp.red = false;//调整为黑色
               xpp.red = true;//暂时调整为红色
               x = xpp;//X指向XPP,进行下一次循环处理
           }
           
           //xppr也就是叔叔结点不存在或者为黑色
           else {
           		//如果X是右孩子
               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);
                   }
               }
           }
       }
      
       //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;
               }
               //右旋之后改变XP颜色为黑色
               if (xp != null) {
                   xp.red = false;
                   if (xpp != null) {
                   //XPP改为红色,然后进行左旋
                       xpp.red = true;
                       root = rotateLeft(root, xpp);
                   }
               }
           }
       }
   }
}

插入平衡也就是插入结点后进行平衡操作,对于红黑树来说就是为了保证“任意路径上黑色结点个数相同”这一特性。

需要注意,下面我们分析的前提是:

  • XP不为null
  • xp的颜色是红色且xpp不为null

① 第一种情况,位于xpp的左侧分支,且叔叔结点是红色

也就是如下代码分支:

 if (xp == (xppl = xpp.left))
 	if ((xppr = xpp.right) != null && xppr.red)

如下图所示,此时在左侧分支插入结点,那么无论插入结点X是10(左孩子)还是25(右孩子),其叔叔结点是40(红色),这时将会引起颜色发生改变。
在这里插入图片描述

这时就会触发如下逻辑,这里xpp再下次循环处理中会修改为黑色。

if ((xppr = xpp.right) != null && xppr.red) {
    xppr.red = false;//40调整为黑色
    xp.red = false;//20调整为黑色
    xpp.red = true;//暂时调整为红色
    x = xpp;//X指向XPP,进行下一次循环处理
}

最终颜色将改变为如下:
在这里插入图片描述
如果叔叔结点是黑色的呢?

② 第二种情况:位于xpp的左侧分支,且叔叔结点是黑色或叔叔结点不存在

再提醒一下前提,XP是红色且XPP不为null

如下图所示,我们插入结点35(红色),其叔叔结点不存在。
在这里插入图片描述

假设我们插入结点35,其是XP(30)的右孩子,那么会发生左旋。

//插入结点是父节点的右孩子
if (x == xp.right) {
	//进行左旋
    root = rotateLeft(root, x = xp);
    xpp = (xp = x.parent) == null ? null : xp.parent;
}

在这里插入图片描述

左旋之后插入的结点X(35)与原先父节点交换了父子关系,新的XP是35。

if (xp != null) {
//父节点改为黑色-35
    xp.red = false;
    
    //祖父节点存在变为红色
    if (xpp != null) {
    	//40-red
        xpp.red = true;
        //右旋
        root = rotateRight(root, xpp);
    }
}

在这里插入图片描述

③ 第三种情况:位于xpp的右侧分支且叔叔结点是红色

这种情况与第一种情况不同的就是“左右”。再提醒一下前提,XP是红色且XPP不为null

if (xppl != null && xppl.red) {
	//30-black
    xppl.red = false;
    //50-black
    xp.red = false;
    
    //暂时设置为红色,下次循环设置为黑色
    xpp.red = true;
    x = xpp;
}

在这里插入图片描述

④ 第四种情况:位于xpp的右侧分支且叔叔结点是黑色或者叔叔结点不存在

再提醒一下前提,XP是红色且XPP不为null

如下图所示,我们插入结点45(红色),其叔叔结点不存在。
在这里插入图片描述

如下图所示,假设我们插入的是45(红色),那么发生右旋。

if (x == xp.left) {
//如果是左孩子,那么先右旋
    root = rotateRight(root, x = xp);
    xpp = (xp = x.parent) == null ? null : xp.parent;
}

在这里插入图片描述

右旋之后,父子结点会发生交换。新的XP为45,旧的父结点50作为其右孩子。

//右旋之后改变XP颜色为黑色
if (xp != null) {
	// 45-black
    xp.red = false;

    // xpp-40
    if (xpp != null) {
    	//XPP-40改为红色,然后进行左旋
        xpp.red = true;
        root = rotateLeft(root, xpp);
    }
}

在这里插入图片描述
上述就是四种情况,其实总结起来就是六种情况:

  • 左侧分支,叔叔是红色,此时不发生结构调整,只需要改变父亲与叔叔结点颜色为黑色;
  • 右侧分支,叔叔是红色,此时不发生结构调整,只需要改变父亲与叔叔结点颜色为黑色;
  • LL调整
  • LR调整
  • RL调整
  • RR调整

后四种均需要考虑螺旋与颜色改变。OK,最后我们再看看左旋和右旋的代码。

⑤ rotateLeft(root, x = xp)

在这里插入图片描述
这里 p 是30,root是40。

static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
                                      TreeNode<K,V> p) {
    TreeNode<K,V> r, pp, rl;
    // r=35
    if (p != null && (r = p.right) != null) {
    	
    	//rl=null 这句话的意思是,如果r有左孩子,那么将成为p的右孩子
        if ((rl = p.right = r.left) != null)
            rl.parent = p;
       
        //r.parent=40,pp=40  不为null,
        //这句话的意思是如果p是根节点,那么左旋后 r 必然是黑色
        if ((pp = r.parent = p.parent) == null)
            (root = r).red = false;
        else if (pp.left == p)
        	// 40.left=35  也就是右孩子作为(祖)父节点的左孩子
            pp.left = r;
        else
            pp.right = r;
       
        // r成了新的父节点,p 作为父节点的左孩子    
        r.left = p;
        
        p.parent = r;//p 与 r 形成父子双向绑定
    }
    return root;
}

左旋的本质就是右孩子成为祖父结点的左孩子,旧父节点成为右孩子(新父节点)的左孩子。可以看到其实在上图实例中,root没有发生改变。

⑤ rotateRight(root, x = xp)

在这里插入图片描述
如上图所示,这里 p 是50,root是40。

static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
                                       TreeNode<K,V> p) {
    TreeNode<K,V> l, pp, lr;
    // l=45 左孩子,否则无需右旋
    if (p != null && (l = p.left) != null) {
    	
    	//上图这里为null,这里核心是P的左孩子指向P的左孩子的右孩子
    	//即p.left -> p.left.right	
        if ((lr = p.left = l.right) != null)
            lr.parent = p;
         //l.parent -> p.parent,即 p.left.parent -> p.parent   
        if ((pp = l.parent = p.parent) == null)
            (root = l).red = false;
        else if (pp.right == p) //40.right=50
            pp.right = l;// 40.right=45
        else
            pp.left = l;
        
        //45.right=50    
        l.right = p;
        // 50.parent=45
        p.parent = l;
    }
    return root;
}

右旋的本质是节点P(50)的左孩子(45)作为新的父节点,节点P(50)作为其左孩子(45)的右孩子。

如果左孩子有右孩子,那么左孩子的右孩子作为结点P的左孩子。也就是下面这两行代码:

//下面两行等价于:p.left=l.right, lr=p.left, lr.parent=p 
 if ((lr = p.left = l.right) != null)
      lr.parent = p;

如下图所示,假设结点P是35,l=p.left=27,那么执行完上面这两行代码后,28就成为了结点35的左孩子。
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流烟默

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值