红黑树(Red Black Tree)的五个性质
- 节点是红色或黑色。
- 根节点是黑色。
- 每个叶节点(NIL节点,空节点)是黑色的。
- 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
注:红黑树本质是平衡树,所以对于每个节点,还满足左子树节点小于该节点,同时右子树节点大于该节点。
基本操作-左旋和右旋
在进入本节之前请先看下面两个小提示:
2. 旋转的前提:增加节点导致出现了两个连续的红色节点,删除黑色节点导致黑色节点所在分支深度减少。
左旋
对着下面问题看上面图片(答案仅供参考):
对x旋转前后有哪些变化?
- x,x.left深度加1;
- right,right.right减1;
- papa,right.left深度不变。
代码如下:
private void rotateLeft(Node x) {
if(x!=null&&x.right!=null) {
Node right=x.right,papa=x.parent;
right.parent=papa;
if (papa != null) {
if (papa.left == x)
papa.left = right;
else
papa.right = right;
}else {
root=right;
}
x.right=right.left;
if(right.left!=null)
right.left.parent=x;
right.left=x;
x.parent=right;
}
}
右旋
对着下面问题看上面图片(答案仅供参考):
对x旋转前后有哪些变化?
- x,x.right加1;
- left,left.left减1;
- papa,left.right不变。
代码如下:
private void rotateRight(Node x) {
if(x!=null&&x.left!=null) {
Node left=x.left,papa=x.parent;
left.parent=papa;
if (papa != null) {
if (papa.left == x)
papa.left = left;
else
papa.right = left;
}else {
root=left;
}
x.left=left.right;
if(left.right!=null)
left.right.parent=x;
left.right=x;
x.parent=left;
}
}
增加节点
小提示:默认红黑树插入的是红色节点。如果插入的是黑色,必然每次插入都会导致树不平衡。插入红色却不会每次都导致树不平衡。
想想如果增加一个节点该如何增加?
可以分两步来做:
- 找个合适的位置插入,这一步和二叉查找树的插入过程一样;
代码如下:
/**
* @Description: 插入节点
* 1.空树
* 2.如果添加到黑色节点下,无需调整,结束;
* 3.如果添加到红色节点下,需调增,调用调增方法
* @param key
* @param value
* @author wjc920
* @date 2018年6月28日
*/
public void put(K key, V value) {
Node cur=root;
Node x=new Node(key, value);
if(cur==null) {
root=x;
root.red=false;
return;
}
while(true) {
int cmp = key.compareTo(cur.key);
if (cmp < 0) {
if(cur.left==null) {
cur.left=x;
x.parent=cur;
if(cur.red)
balanceAfterInsert(cur.left);
break;
}
cur=cur.left;
}else if (cmp > 0) {
if(cur.right==null) {
cur.right=x;
x.parent=cur;
if(cur.red)
balanceAfterInsert(cur.right);
break;
}
cur=cur.right;
} else {
cur.value = value;
break;
}
}
}
- 如果插入导致红黑树不平衡了,就调整。
首先我们来看看插入之后的结果:
图片说明:x为插入节点,cur表示x的父节点,uncle表示x的叔叔节点。
上图中各种情形的分析如下:
- 第一排的情形可以不做任何处理(这种插入完美符合红黑树的性质,这就是插入代码中为何有个if(cur.red)判断语句,就是排除这种完美插入的);
- 第二排的情形,虽然有4种,但通过对cur左旋将22转21,右旋将23转24。旋转之后就剩下21和24两种情形了,21和24处理完后,树成为了一个完美的红黑树,平衡操作也就圆满完成了;
- 第三排的4种情形尽管长相五花八门,但只需一个操作就统统干掉,将cur和uncle变黑,cur.parent变红,这样就相当于在cur.parent处插入了一个红色的节点,处理方式简单粗暴,一般就会处理的不干净,所以我们还需要继续卖苦力从cur.parent开始继续向树顶做平衡。
对21情况做平衡处理(24可参考21做对称操作即可):
小提示:细看一下图中的21情况,我们是不是可以将左边cur删除,移到右边加在uncle和cur.parent中间?如下图(左):
对于上图左的情况,违反了二叉平衡树的性质(红黑树属于二叉平衡树),cur.parent大于右边的cur,那交换cur和cur.parent的值就可以解决问题了(如上图右),交换的内容包括key和value。
交换后,我们惊奇的发现,对于cur及其子树,完美满足了红黑树的所有性质,我们的红黑树平衡任务就这样完成啦。
补漏:上面操作忘记了包扎一下cur.right小兄弟的伤口了,很简单,对照上图左,他和cur的父子关系不变,只是从cur的右儿子变成左儿子了。可以动动小手画一画,帮助cur.right小兄弟包扎一下
代码如下:
/**
* @Description: 对插入的红色点做平衡处理
* 3.1 grandpa.left==papa.left==x (==表示引用指向的意思)
* 3.1.1 grandpa.right的颜色为黑色
* 3.1.2 grandpa.right的颜色为红色
* 3.2 grandpa.left==papa.right==x
* 3.2.1 grandpa.right的颜色为黑色
* 3.2.2 grandpa.right的颜色为红色
* 3.3 grandpa.right==papa.left==x
* 3.3.1 grandpa.left的颜色为黑色
* 3.3.2 grandpa.left的颜色为红色
* 3.4 grandpa.right==papa.right==x
* 3.4.1 grandpa.left的颜色为黑色
* 3.4.2 grandpa.left的颜色为红色
* @param x
* @author wjc920
* @date 2018年6月30日
*/
public void balanceAfterInsert(Node x) {
Node papa,grandpa,uncle;
papa=parentOf(x);
grandpa=parentOf(papa);
while(x!=null&&x!=root&&isRed(papa)) {
if(papa==leftOf(grandpa)) {
uncle=rightOf(grandpa);
if(isRed(uncle)) {//3.1.2&3.2.2 这种情况无需判别x是papa的左还是右,将papa和uncle变黑,grandpa变红
setColor(uncle, BLACK);
setColor(papa, BLACK);
setColor(grandpa, RED);
x=grandpa;
papa=parentOf(x);
grandpa=parentOf(papa);
}else {
if(x==rightOf(papa)) {//3.2.1 这种情况通过左旋papa,然后交换x和papa即可变3.1.1
rotateLeft(papa);
x=papa;
papa=parentOf(x);
}
if(x==leftOf(papa)) {//3.1.1 右旋grandpa之前先将papa变为黑,grandpa变红,目的是将红节点变到右边
setColor(grandpa, RED);
setColor(papa, BLACK);
rotateRight(grandpa);
x=root;//由于papa已经为black,下次while条件判断将会跳出
}
}
}else {//和上面的情况类似,对称的
uncle=leftOf(grandpa);
if(isRed(uncle)) {
setColor(uncle, BLACK);
setColor(papa, BLACK);
setColor(grandpa, RED);
x=grandpa;
papa=parentOf(x);
grandpa=parentOf(papa);
}else {
if(x==leftOf(papa)) {
rotateRight(papa);
x=papa;
papa=parentOf(x);
}
if(x==rightOf(papa)) {
setColor(grandpa, RED);
setColor(papa, BLACK);
rotateLeft(grandpa);
x=root;//由于papa已经为black,下次while条件判断将会跳出
}
}
}
}
setColor(root, BLACK);
}
代码观看温馨小提示:对照代码中的if逻辑,依次画出每种情形的图,有助帮小脑袋理清思路哦。
删除节点
小提示:红黑树删除节点后的平衡操作,就是借兄为父,目的是将黑色的兄弟节点借为自己的父节点,以弥补因为删除导致的深度下降,如下图:
经过上图借兄为父之后,发现树不平衡,所以还需要交换papa和sib的值。
还有个问题需要注意,借兄为父之后,sib.right的深度减一了,这个在处理第四排情况时有处理。
删除也和插入一样两步走,首先你得找到他(要删除的节点),然后才能干掉他。
- 找到他(可结合二叉查找树的删除看)
public V remove(K key) {
V resultV = null;
Node cur = get(root, key);
if (cur != null) {
resultV = cur.value;
if (cur.right != null && cur.left != null) {
Node next = nextNodeForBinTree(cur);
cur.key = next.key;
cur.value = next.value;
cur = next;
}
Node replace = cur.left != null ? cur.left : cur.right;
if (replace != null) {
if (cur.parent == null) {
root = replace;
replace.parent = null;
} else {
replace.parent = cur.parent;
if (cur.parent.left == cur) {
cur.parent.left = replace;
} else {
cur.parent.right = replace;
}
cur.left = cur.right = cur.parent = null;
if (!cur.red) {
balanceAfterRemove(replace);
}
}
} else {
if (cur == root) {
root = null;
} else {
if (!cur.red) {
balanceAfterRemove(cur);
}
if (cur.parent != null) {
if (cur.parent.left == cur)
cur.parent.left = null;
else
cur.parent.right = null;
cur.parent = null;
}
}
}
}
return resultV;
}
- 干掉他,在干掉他之前,首先观察他:
上图如果cur左右子树均不空,那我们就不好删他了,删完左右子树放哪儿都不合适,那我们就找一个节点替换他吧,一般我们选他的后继节点(大于cur的最小节点)。
替换之后(其中也包含不经过替换cur本身的左子树或者右子树其一为空的情况):
找到替换节点的代码,在上面代码的if (cur.right != null && cur.left != null)里面。
现在我们来结合上图分析一下,该如何搞定:
- 对于第一排的情况,我们可以直接用他儿子代替他的位置,树依然是完美的红黑树,结束战斗;
- 对于第二排,我们无法借兄为父,兄弟节点为红色,即使借过来,也无法增加cur的子树深度。然后我们可以对papa节点左旋,并交换sib和papa的颜色,左旋之后sib.left节点就变为cur的兄弟节点了,如下图:
经过从作图到右图的变换之后,成功将问题转化为第三四五排的情况了。
- 对于第三排我们很容易通过右旋的同时交换sib.left和sib的颜色,转化为第四排41、42情况;
重点是第四排的处理,x借兄为父即可,但是借兄为父后发现红色sib.right及其子节点深度减一,只需将红色的sib.right变为黑色即可完成平衡,结束;
对于第五排虽然是黑色兄弟,但是如果借过去发现无法保证sib.right的平衡,所以我们将sib变为红色,保证papa的左右子树深度相同,然后将papa设置为x继续平衡操作。
private void balanceAfterRemove(Node x) {
Node papa, sib;
while (x != null && x != root && !isRed(x)) {
papa = parentOf(x);
if (leftOf(papa) == x) {
sib = rightOf(papa);
if (isRed(sib)) { //对应删除节点图片的第三排
setColor(papa, RED);
setColor(sib, BLACK);
rotateLeft(papa);
papa = parentOf(x);
sib = rightOf(papa);
}
if (!isRed(leftOf(sib)) && !isRed(rightOf(sib))) {//对应删除节点图片的第五排
setColor(sib, RED);
x = papa;
} else {
if (isRed(leftOf(sib))) {//对应删除节点图片的第二排
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
rotateRight(sib);
sib = rightOf(papa);
}
setColor(rightOf(sib), BLACK);//对应删除节点图片的第四排
setColor(sib, isRed(papa));
setColor(papa, BLACK);
rotateLeft(papa);
x = root;
}
} else {
sib = leftOf(papa);
if (isRed(sib)) {
setColor(papa, RED);
setColor(sib, BLACK);
rotateRight(papa);
papa = parentOf(x);
sib = leftOf(papa);
}
if (!isRed(leftOf(sib)) && !isRed(rightOf(sib))) {
setColor(sib, RED);
x = papa;
} else {
if (isRed(rightOf(sib))) {
setColor(rightOf(sib), BLACK);
setColor(sib, RED);
rotateLeft(sib);
sib = leftOf(papa);
}
setColor(leftOf(sib), BLACK);
setColor(sib, isRed(papa));
setColor(papa, BLACK);
rotateRight(papa);
x = root;
}
}
}
setColor(x, BLACK);
}