链表转换为红黑树节点
当往hashMap中添加元素,在同一个hash槽位挂载的元素超过8个后,执行treeifyBin方法。
在treeifyBin方法中,只有当tab数组(hash槽位)的长度不小于MIN_TREEIFY_CAPACITY(默认64)时,才会将(n - 1) & hash位置处的所有节点树化。
先遍历(n-1)&hash处的链表,从头到尾,按顺序,将所有节点转换为treeNode。
树节点重新挂载
在将链表所有节点转换为TreeNode后,再执行treeify方法。
在treeify方法中,顺着头节点遍历链表设置红黑树(this是头节点)。
在遍历节点时,比较大小决定挂在红黑树的哪个节点上(先比hash,再使用Comparable接口的compareTo方法进行比较,小的挂在左边,大的挂在右边)。
红黑树平衡插入
在二叉树上新增节点后,执行balanceInsertion方法对树结构进行调整,以便符合红黑树定义。
下面代码中的注释是我推演加上的,若有错误,请指正。
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
x.red = true;
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
// x is top
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
// 新节点挂在root下
else if (!xp.red || (xpp = xp.parent) == null)
return root;
// 新节点挂在树左边,左子节点
if (xp == (xppl = xpp.left)) {
// 新节点(1)挂在最左的左边,不需旋转,只需改变颜色
// black(3) red(3)
// red(2) red(4) => black(2) black(4)
// red(1) red(1)
if ((xppr = xpp.right) != null && xppr.red) {
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else { // 新节点(2)挂在最左的右边
// black(3) black(3)
// red(1) rotateLeft=> red(2)
// red(2) red(1)
if (x == xp.right) {
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
// black(3) black(3) xpp
// red(2) => black(2) xp
// red(1) red(1) x
xp.red = false;
if (xpp != null) {
xpp.red = true;
// red(3)
// black(2) => black(2)
// red(1) red(1) red(3)
root = rotateRight(root, xpp);
}
}
}
}
else { // 新节点(4)挂在树最右的右边 xp != (xppl = xpp.left)
// black(2) red(2)
// red(1) red(3) => black(1) black(3)
// red(4) red(4)
if (xppl != null && xppl.red) {
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
// 新节点(2)挂在树最右的左边
// black(1) black(1)
// red(3) => red(2)
// red(2) red(3)
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) {
// red(1)
// black(2) => black(2)
// red(3) red(1) red(3)
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
分析下balanceInsertion的思路:二叉树新增节点导致不符合红黑定义时,通过改变颜色和旋转(左旋或右旋)使得红黑树再次平衡,红黑树概念及旋转可参考https://blog.csdn.net/weixin_43790276/article/details/106042360。
根据插入节点是挂在左子节点(xp == (xppl = xpp.left)),还是右子节点,分情况进行了操作,下面是挂在左子节点时的三种情况。
下面为挂在右子节点时的三种情况,这六种情况,覆盖了所有插入时会导致的红黑树结构破坏。
新节点挂载并重新平衡红黑树后,将root节点移动到hash槽位的最前面
rotateLeft左旋方法
简单说下,为什么要旋转节点,因为往二叉树中挂载新节点时,未严格按照红黑树的规定挂载(按照规定挂载会很麻烦),所以先按二叉树的规则挂载,再通过左旋、右旋、改变颜色来使其符合红黑树规定。
什么是左旋,其实主要是为了把旋转节点与它的右子节点进行位置互换,旋转节点相对于右子节点来说,它往左边移动了,所以叫左旋;右旋类似,这样不管二叉树是怎么样的,总可以通过左旋、右旋,挪动它们的位置,再加上改变颜色,使其达到红黑树的形态。
// root:根节点 p:当前旋转节点
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
// r:右子节点 pp:父父节点 rl:右左节点
TreeNode<K,V> r, pp, rl;
// 必须当前节点和右子节点存在才左旋
if (p != null && (r = p.right) != null) {
if ((rl = p.right = r.left) != null)
// 把右左节点挂在当前节点右边,见下面的左旋情况2
rl.parent = p;
if ((pp = r.parent = p.parent) == null)
// 右节点挪到上方,变成根节点,见下面的左旋情况2
(root = r).red = false;
else if (pp.left == p)
// 右子节点移到父父节点左边,见下面的左旋情况5
pp.left = r;
else
// 右子节点移到父父节点右边,见下面的左旋情况6
pp.right = r;
// 右子节点移到旋转节点上方,见下面的左旋情况1
r.left = p;
p.parent = r;
}
return root;
}
上述左旋方法覆盖了6种二叉树左旋情况(注意二叉树有时并不能通过一次左旋就能变为红黑树,有时需要多次左旋或右旋及改变颜色),转换过程覆盖的情况画图演示如下。
左旋情况1,最简单的左旋,旋转节点p为1。
左旋情况2,旋转节点p为1。
左旋情况3,旋转节点p为1。
左旋情况4,旋转节点p为2。
左旋情况5,旋转节点p为4。
左旋情况6,旋转节点p为2。