下面是一个红黑树的图示:
从任意节点(不含该节点)到达一个叶节点的任意一条简单路径上的黑色节点个数称为该节点的黑高(block-height),计为bh(x)。根节点的黑高为树的黑高。红黑树的树高h(x)与黑高bh(x)的关系是bh(x) >= h(x)/2
引理:一颗有n个内部节点的红黑树的高度至多为2lg(n+1)。(tip:这里的lg和下面的lg都是以2为底数的)。
红黑树的插入删除操作通过旋转和变色可以将时间复杂度控制在O(lgn)。
插入一个红色节点后,有时只通过变色操作就可以保持红黑树的平衡。下面对这种情况进行说明:
当插入的节点x的父节点xp是红色并且x的叔叔节点xppr也是红色的时候,需要将xp和xppr节点变为黑色,将xp的父节点xpp变为红色即可。如图:(xpp并不一定是根节点)
下面看一下hashMap中的代码:
static TreeNode balanceInsertion(TreeNode root,
TreeNode x) {
// 默认新插入的节点为红色
x.red = true;
// x:新插入的节点
// xp:x的父节点(xp==xppl 或者 xp==xppr)
// xpp:x的祖父节点
// xppl:x的祖父节点的左孩子节点,即x的左叔叔节点
// xppr:x的祖父节点的右孩子节点,即x的右叔叔节点
for (TreeNode xp, xpp, xppl, xppr;;) {
// 若xp为空说明x为根节点 所以将x变成黑色并返回
// 因为红黑树是自底向上做平衡的,当x为根节点时说明已经
// 平衡到根节点了可以返回了。
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
// 若xp为黑色或者xpp为空,则不需要调整,直接返回根节点
else if (!xp.red || (xpp = xp.parent) == null)
return root;
// 若不满足上面情况说明:xp为红色而且xpp不为空
// 若xp为xpp的做孩子节点时,说明x插在了xpp的左子树上
// 也就是说x有右叔叔节点
if (xp == (xppl = xpp.left)) {
// 若右叔叔节点不为空 而且 为红色
if ((xppr = xpp.right) != null && xppr.red) {
// 将右叔叔节点和父节点变为黑色,祖父节点变为红色
// 将祖父节点作为新的插入节点回到循环起点
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
// 若右叔叔节点为黑色(为空也是为黑色)
else {
// 若x插在了父节点的右边
if (x == xp.right) {
// 先以xp为中心左旋,同时将xp变为新插入的节点
root = rotateLeft(root, x = xp);
// 调整xp与xpp的值
// 这里对下面的代码做一个变换便于理解
// xp = x.parent;
// xpp = xp == null ? null : xp.parent;
xpp = (xp = x.parent) == null ? null : xp.parent;
}
// 此时x已经是插入到xp的左边了
// 若xp节点不为空
if (xp != null) {
// 将xp节点变为黑色
xp.red = false;
// 如果祖父节点不为空
if (xpp != null) {
// 将祖父节点变为红色
xpp.red = true;
// 在以祖父节点为中心进行右旋
root = rotateRight(root, xpp);
}
}
}
}
// 否则x有左舒舒节点时
else {
// 当左叔叔节点不为空且为红色时
if (xppl != null && xppl.red) {
// 将左叔叔变为黑色,父节点变为黑色,祖父节点变为红色
// 将祖父节点作为新的插入节点回到循环起点
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
// 当左叔叔节点为黑色时(为空也是为黑色)
else {
// 当x为xo的左节点时
if (x == xp.left) {
// 右旋 让xp作为新的插入节点
//
root = rotateRight(root, x = xp);
// 将 xp重新指向x.parent,若x.parent不为空 则 pp == xp.parent
xpp = (xp = x.parent) == null ? null : xp.parent;
}
// 此时x已经是插入到xp的右节点了
if (xp != null) {
//将xp变为黑色节点
xp.red = false;
// 如果xpp不为空 将xpp变为红色节点,然后左旋,回到循环初始位置
if (xpp != null) {
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
代码中26行到33行是处理上面说的这种情况。通过上下文可知当程序走到26行时说明插入节点x的父节点和叔叔节点都为红色。然后执行下面的操作:
// 若右叔叔节点不为空 而且 为红色
if ((xppr = xpp.right) != null && xppr.red) {
// 将右叔叔节点和父节点变为黑色,祖父节点变为红色
// 将祖父节点作为新的插入节点回到循环起点
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
可以看到将xppr,xp变为黑色,xpp变为红色。然后将xpp作为x回到循环开始位置继续做树的平衡操作。
上图只列举了x插入左子树的情况,其实x插入右子树是一样的,这里不再赘述。
当插入节点x的父节点xp为红色并且x的叔叔节点xppr(或xppl)为黑色时(叔叔节点为空也是黑色节点),只通过变色是无法满足平衡条件的。这时就需要旋转了。旋转分为左旋和右旋。不管是左旋还是右旋都是有公式的,只要记住公式就可以了。
当节点x与父节点xp与祖父节xpp点在同一条线上时,将xp变为黑色、xpp变为红色然后在以xp为圆心向倾斜的那一方旋转,比如:
下面结合代码看看hashmap中是如何操作的(节选自上面代码的一部分34行-59行):
// 若右叔叔节点为黑色(为空也是为黑色)
else {
// 若x插在了父节点的右边
if (x == xp.right) {
// 先以xp为中心左旋,同时将xp变为新插入的节点
root = rotateLeft(root, x = xp);
// 调整xp与xpp的值
// 这里对下面的代码做一个变换便于理解
// xp = x.parent;
// xpp = xp == null ? null : xp.parent;
xpp = (xp = x.parent) == null ? null : xp.parent;
}
// 此时x已经是插入到xp的左边了
// 若xp节点不为空
if (xp != null) {
// 将xp节点变为黑色
xp.red = false;
// 如果祖父节点不为空
if (xpp != null) {
// 将祖父节点变为红色
xpp.red = true;
// 在以祖父节点为中心进行右旋
root = rotateRight(root, xpp);
}
}
}
下面看一下左旋rotateLeft()方法的代码:
// 左旋方法
//对p进行左旋操作时,必须满足p不能为而且p的右子节点不能为空,然后进行如下操作:
//1、定义pp = p.parent; r = p.right; rl = r.left;
//2、将r的左子树作为p的右子树,若rl不为空则将p作为rl的父节点
//3、将pp作为r的父节点。当pp不为空时且p为pp的左子节点,
// 则将r变为pp的左子节点,否则将r变为pp的右子节点。若pp为空则将r变为根节点
//4、将p作为r的左子树
//5、将r作为p的父节点
static TreeNode rotateLeft(TreeNode root,
TreeNode p) {
// r:p的右子节点
// pp:p的父节点
// rl:r的左子节点
TreeNode r, pp, rl;
//
// 若p和p的右子节点都不为空才进行左旋操作 同时将r = p.right
if (p != null && (r = p.right) != null) {
// 赋值 p.right = r.left; rl = p.right(即将r的左子节点作为p的右子节点);
// 当 rl 不为空时 将 rl.parent = p;
if ((rl = p.right = r.left) != null)
rl.parent = p;
// 赋值 r.parent = p.parent(将p的父节点作为r的父节点); pp = r.parent
// 当 p.parent为空时说明p为root节点,因为需要左旋
// 所以将root = r ; root.red = false;
if ((pp = r.parent = p.parent) == null)
(root = r).red = false;
// 当pp不为空时且p为pp的左子节点,则将r变为pp的左子节点
else if (pp.left == p)
pp.left = r;
// 否则将r变为pp的右子节点
else
pp.right = r;
// 将p变为r的左子节点
r.left = p;
// 将r作为p的父节点
p.parent = r;
}
return root;
}
在看右旋rotateRight()方法的代码:
// 右旋方法
//对p进行右旋操作时,必须满足p不能为而且p的左子节点不能为空,然后进行如下操作:
//1、定义pp = p.parent; l = p.left; lr = l.right;
//2、将l的右子树作为p的左子树,若lr不为空则将p作为lr的父节点
//3、将pp作为l的父节点。当pp不为空时且p为pp的左子节点,
// 则将l变为pp的左子节点,否则将l变为pp的右子节点。若pp为空则将l变为根节点
//4、将p作为l的右子树
//5、将l作为p的父节点
static TreeNode rotateRight(TreeNode root,
TreeNode p) {
TreeNode l, pp, lr;
// p不能为而且p的左子节点不能为空
if (p != null && (l = p.left) != null) {
// 将l的右子树作为p的左子树,若l的右子树不为空则将l的右子树的父节点变为p
if ((lr = p.left = l.right) != null)
lr.parent = p;
// 将p的父节点变为l的父节点,若p的父节点为空 则将l变为root节点
if ((pp = l.parent = p.parent) == null)
(root = l).red = false;
// 若p的父节点不为空且p为pp的右子节点,则将l变为pp的右子节点
else if (pp.right == p)
pp.right = l;
// 否则 将l变为pp的左子节点
else
pp.left = l;
// 将p作为l的右子节点
l.right = p;
// 将l作为p的父节点
p.parent = l;
}
return root;
}
红黑树的插入是比较简单的情况比较少,一般来说默认新插入的节点为红色。因为从第5条定义可知红黑树的每条简单路径的黑高是相等的,若插入的节点默认是黑色的话那么必定会引起新插入的节点的分支的黑色节点比其他的分支的黑色节点多,从而必定会进行红黑树的自平衡(变色和旋转)操作。而若默认插入的节点是红色的话,只有当父节点为红色时才会进行自平衡操作,为了减少算法要处理的情况,默认插入的节点为红色。