关联博文:
数据结构之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的左孩子。

本文详细解析了HashMap中红黑树平衡插入的过程,包括四种主要情况及其对应的旋转操作,并附带左旋与右旋的具体实现。
4926

被折叠的 条评论
为什么被折叠?



