上文我们根据红黑树的2、3、4节点的插入规律,代码实现了红黑树的插入。本文我们继续通过对于红黑树的删除规律进行红黑树的删除代码实现。下图便是一个红黑树和234数的一个对应关系。
红黑树的删除和红黑树的插入类似,可以分为删除操作、调整。但是假如直接删除红黑树节点时,不光需要断开与其他节点连接还需要调整变色,难度极大。由于红黑树来源于234数,红黑树在删除时就只删除234树对应的叶子节点,这样只需要删除底层的叶子节点,降低了删除的复杂度。其中对应方法就是去寻找节点的前驱节点或者后继节点进行删除。比如要删除8,其后继节点为9,则将9,8的值在树上互换,然后删除8节点即可。这样便完成了节点删除。
但是存在某节点删除了,红黑树的规则被打破,导致不合法的情况。现在我们便根据红黑树与234数的对应情况来进行讨论。
情况1:删除后,节点能自己保持合法性
4节点:9,11,12 删除9节点,能够其他节点能为此黑色平衡,保持合法
3节点:11,12 删除11,12能恢复其黑色平衡,保持合法性
2节点:12 删除节点为null,无法保证合法性
情况2:自己无法保证,向兄弟节点借,兄弟节点有得借 (删除7 向 9,11,12借)
查找自己真正的兄弟节点(下文情况3有说明)
删除节点7黑色,需要向11兄弟借,父亲8下去顶替,兄弟11替父亲位置
操作:父节点变黑,兄弟节点变红,兄弟右节点变黑,对父亲向左旋转
最后效果如图所示:
情况3:自己无法保证,向兄弟节点借,兄弟节点有得接没得借(删除5 向7借)
通过234树可以看到 5,7是兄弟节点,但是对应到红黑树上,5,8才是正真的兄弟节点,这里与红黑树不一致,所以在删除前需要进行调整,让兄弟节点关系正确。
此时删除7时,其实11也不是7的正真的兄弟节点,所以情况2删除之前也需要找自己的真正的兄弟节点
调整后5,7已经是正真的兄弟节点,但是删除5后,7无法删除。目前的解决方法是,7也进行变红色进行自损,然后判断父亲是否是红色,红色便能完成黑色补充。假如父亲是黑色,则自损父亲的兄弟,直到找到父节点为红色完成黑色补充。。。。这里要使用逆向思维,假定5以下还有分支且黑色平衡,7以下还有分支且黑色平衡。5删除后黑色-1,那么7分支同样要黑色-1才能达到 5,7分支的黑色平衡。但是11分支黑色此时会多1个,那么就需要5,7的父亲及其以上节点有一个黑色补充,达到新的黑色平衡即可。
2.代码实现
1.删除操作,找到真正需要删除的点
/**
* 删除叶子节点:直接删除
* 有一个子节点,用字节的去代替自己 替代节点是红色
* 有2个节点,使用前去或者后继代替删除
*
* 删除非黑色节点直接删除,无需旋转和变色
* @param
*/
public void deleteCurrentNode(RBNode current){
//找到需要删除的节点
//处理两个节点不为空
if (current.right != null && current.left!=null){
RBNode precursor = getPrecursorNode(current);
current.key = precursor.key;
current.value = precursor.value;
current = precursor;
}
//处理一个节点不为空(这里包含了一个节点的情况和前驱节点替代主节点删除的情况)
RBNode replaceNode = current.left !=null? current.left : current.right;
if (replaceNode !=null){
//替代节点与删除节点的父亲连接,然后当前节点进行脱钩
replaceNode.parent = current.parent;
if (current.parent ==null){//root为根节点
root = replaceNode;
}else if (current == current.parent.left){
current.parent.left = replaceNode;
}else {
current.parent.right = replaceNode;
}
//没有字节的,数据脱钩
current.parent = null;
current.left = null;
current.right = null;
//先删除在调整,黑色才会调整 替代节点肯定是红色只需要将替代节点染黑(前驱,双节点,上黑下红)
if (current.color == BLACK){
fixAfterRemove(replaceNode);
}
}else if (current.parent == null){
root = null;
}else {
//没有叶子节点
//先调整,有节点用于删除了,在删除 (黑色才会调整)
if (current.color == BLACK){
fixAfterRemove(replaceNode);
}
if (current == current.parent.left){
current.parent.left = null;
}else {
current.parent.right = null;
}
current.parent = null;
}
}
调整
/**
* deleteNode 是真正要删除的接口,只会有一个子节点或者没有子节点
*
* 单位三节点时,可以自己解决,则可以用儿子来补充
*
* 当为2节点时,不能自己解决,则需要兄弟节点帮忙
*
* 此时兄弟无子节点, 则需要兄弟节点自损,此时黑色平衡收到破坏,deleteNode 指向 deleteNode的父节点,直到deleteNode的父节点为红色节点,然后变成黑色节点保存黑色平衡
*
* 和兄弟子节点
*
* 情况2:自己搞定不,兄弟有得接
*情况3:自己搞定不,兄弟没得接
* @param deleteNode
*/
private void fixAfterRemove(RBNode deleteNode){ //此时改节点为替代节点,红色
while (deleteNode != root && colorOf(deleteNode) == BLACK){ //删除节点是黑色
if (deleteNode == leftOf(parentOf(deleteNode))){//父亲的左还在
RBNode rbNode = rightOf(parentOf(deleteNode));//兄弟
//这里是搞不定,向兄弟节点借
//想想对于5,6,7,8的图 ,红黑树 5 8是兄弟,红黑树5 7是兄弟 所以需要找到正真的兄弟节点
//由于一个二叉树可以对应多个红黑树,当兄弟节点和自己颜色不一致时,则说明不是正真的兄弟节点
if (colorOf(rbNode)==RED){
setBlackColor(rbNode);
setRedColor(parentOf(rbNode));
rotateLeft(parentOf(rbNode));//沿着父亲旋转
rbNode = rightOf(parentOf(deleteNode));
}
if (colorOf(leftOf(rbNode)) ==BLACK && colorOf(rightOf(rbNode)) == BLACK){ //情况三:找兄弟接,兄弟没得接
//兄弟没得接 自己变红 则兄弟需要损, 降级成红色,但是黑色平衡被破坏。 一直到 删除节点的父节点为红色位置 红色就不进行循环 最后一步变成黑色 保存黑色平衡
setRedColor(rbNode);
deleteNode = parentOf(deleteNode); //红色就不进行循环 最后一步变成黑色 保存黑色平衡
}else {
//情况2:兄弟有得接
//目前的操作是 父亲节点下去 兄弟节点上去作为父节点,
//理想状态是右旋一次性ok, 但是假如右孩子的右孩子为空 则旋转后打破了原有平衡
//右还在为空,需要左旋一下 然后2节点位置转换一下
//想想5,6,6.5,7的情况
if (colorOf(rightOf(rbNode))==BLACK){
setRedColor(rbNode);
setBlackColor(leftOf(rbNode));
rotateRight(rbNode);
rbNode =rightOf(parentOf(deleteNode)) ;
}
//右孩子为空调整结束
//这里包含了3节点和4节点的借节点
setRedColor(rbNode);
setBlackColor(parentOf(deleteNode));
setBlackColor(rightOf(rbNode));
rotateLeft(parentOf(deleteNode));//沿着父亲旋转
deleteNode = root;
}
}else {//父亲的右还在
RBNode lbNode = leftOf(parentOf(deleteNode));//兄弟
//由于一个二叉树可以对应多个红黑树,当兄弟节点和自己颜色不一致时,则说明不是正真的兄弟节点
if (colorOf(lbNode)==RED){
setBlackColor(lbNode);
setRedColor(parentOf(lbNode));
rotateLeft(parentOf(lbNode));
lbNode = leftOf(parentOf(deleteNode));
}
if (colorOf(leftOf(lbNode)) ==BLACK && colorOf(rightOf(lbNode)) == BLACK){
//兄弟没得接 自己变红 则兄弟需要损, 降级成红色,但是黑色平衡被破坏。 一直到 删除节点的父节点为红色位置 红色就不进行循环 最后一步变成黑色 保存黑色平衡
setRedColor(lbNode);
deleteNode = parentOf(deleteNode); //红色就不进行循环 最后一步变成黑色 保存黑色平衡
}else {
//目前的操作是 父亲节点下去 兄弟节点上去作为父节点,
//理想状态是右旋一次性ok, 但是假如右孩子的右孩子为空 则旋转后打破了原有平衡
//右还在为空,需要左旋一下 然后2节点位置转换一下
if (colorOf(leftOf(lbNode))==BLACK){
setRedColor(lbNode);
setBlackColor(rightOf(lbNode));
rotateRight(lbNode);
lbNode =leftOf(parentOf(deleteNode)) ;
}
setRedColor(lbNode);
setBlackColor(parentOf(deleteNode));
setBlackColor(leftOf(lbNode));
rotateLeft(parentOf(deleteNode));
deleteNode = root;
}
}
}
//情况1 自己可以处理,自己为黑色 则 替代节点是红色
setBlackColor(deleteNode);
}
3.HashMap源码
经过自己实现后,现在来膜拜一下大神的代码。
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab, boolean movable) {
......
//p是待删除节点,replacement用于后续的红黑树调整,指向的是p或者p的继承者。
//如果p是叶子节点,p==replacement,否则replacement为p的右子树中最左节点
if (replacement != p) {
//若p不是叶子节点,则让replacement的父节点指向p的父节点
TreeNode<K,V> pp = replacement.parent = p.parent;
if (pp == null)
root = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
p.left = p.right = p.parent = null;
}
//若待删除的节点p时红色的,则树平衡未被破坏,无需进行调整。
//否则删除节点后需要进行调整
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
//p为叶子节点,则直接将p从树中清除
if (replacement == p) { // detach
TreeNode<K,V> pp = p.parent;
p.parent = null;
if (pp != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
}
}
}
麻烦的地方就在删除节点后的调整了,所有逻辑都在balanceDeletion函数里,两个参数分别表示根节点和删除节点的继承者,来看看它的具体实现:
static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root, TreeNode<K,V> x) {
for (TreeNode<K,V> xp, xpl, xpr;;) {
//x为空或x为根节点,直接返回
if (x == null || x == root)
return root;
//x为根节点,染成黑色,直接返回(因为调整过后,root并不一定指向删除操作过后的根节点,如果之前删除的是root节点,则x将成为新的根节点)
else if ((xp = x.parent) == null) {
x.red = false;
return x;
}
//如果x为红色,则无需调整,返回
else if (x.red) {
x.red = false;
return root;
}
//x为其父节点的左孩子
else if ((xpl = xp.left) == x) {
//如果它有红色的兄弟节点xpr,那么它的父亲节点xp一定是黑色节点
if ((xpr = xp.right) != null && xpr.red) {
xpr.red = false;
xp.red = true;
//对父节点xp做左旋转
root = rotateLeft(root, xp);
//重新将xp指向x的父节点,xpr指向xp新的右孩子
xpr = (xp = x.parent) == null ? null : xp.right;
}
//如果xpr为空,则向上继续调整,将x的父节点xp作为新的x继续循环
if (xpr == null)
x = xp;
else {
//sl和sr分别为其近侄子和远侄子
TreeNode<K,V> sl = xpr.left, sr = xpr.right;
if ((sr == null || !sr.red) &&
(sl == null || !sl.red)) {
xpr.red = true; //若sl和sr都为黑色或者不存在,即xpr没有红色孩子,则将xpr染红
x = xp; //本轮结束,继续向上循环
}
else {
//否则的话,就需要进一步调整
if (sr == null || !sr.red) {
if (sl != null) //若左孩子为红,右孩子不存在或为黑
sl.red = false; //左孩子染黑
xpr.red = true; //将xpr染红
root = rotateRight(root, xpr); //右旋
xpr = (xp = x.parent) == null ?
null : xp.right; //右旋后,xpr指向xp的新右孩子,即上一步中的sl
}
if (xpr != null) {
xpr.red = (xp == null) ? false : xp.red; //xpr染成跟父节点一致的颜色,为后面父节点xp的左旋做准备
if ((sr = xpr.right) != null)
sr.red = false; //xpr新的右孩子染黑,防止出现两个红色相连
}
if (xp != null) {
xp.red = false; //将xp染黑,并对其左旋,这样就能保证被删除的X所在的路径又多了一个黑色节点,从而达到恢复平衡的目的
root = rotateLeft(root, xp);
}
//到此调整已经完毕,进入下一次循环后将直接退出
x = root;
}
}
}
//x为其父节点的右孩子,跟上面类似
else { // symmetric
if (xpl != null && xpl.red) {
xpl.red = false;
xp.red = true;
root = rotateRight(root, xp);
xpl = (xp = x.parent) == null ? null : xp.left;
}
if (xpl == null)
x = xp;
else {
TreeNode<K,V> sl = xpl.left, sr = xpl.right;
if ((sl == null || !sl.red) &&
(sr == null || !sr.red)) {
xpl.red = true;
x = xp;
}
else {
if (sl == null || !sl.red) {
if (sr != null)
sr.red = false;
xpl.red = true;
root = rotateLeft(root, xpl);
xpl = (xp = x.parent) == null ?
null : xp.left;
}
if (xpl != null) {
xpl.red = (xp == null) ? false : xp.red;
if ((sl = xpl.left) != null)
sl.red = false;
}
if (xp != null) {
xp.red = false;
root = rotateRight(root, xp);
}
x = root;
}
}
}
}
}
嗯。。。。。。终于完成了对于红黑树的实现分析。。。。。。成就感满满。