深入浅出:红黑树(Red Black Tree)

红黑树(Red Black Tree)的五个性质

  1. 节点是红色或黑色。
  2. 根节点是黑色。
  3. 每个叶节点(NIL节点,空节点)是黑色的。
  4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
  5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

注:红黑树本质是平衡树,所以对于每个节点,还满足左子树节点小于该节点,同时右子树节点大于该节点。

基本操作-左旋和右旋

在进入本节之前请先看下面两个小提示:

1. 该部分可以先不看直接跳到,增加节点删除节点

2. 旋转的前提:增加节点导致出现了两个连续的红色节点,删除黑色节点导致黑色节点所在分支深度减少。

左旋

rotate left

对着下面问题看上面图片(答案仅供参考):

对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;
    }

}

右旋

rotate 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;
    }
}

增加节点

小提示:默认红黑树插入的是红色节点。如果插入的是黑色,必然每次插入都会导致树不平衡。插入红色却不会每次都导致树不平衡。

想想如果增加一个节点该如何增加?

可以分两步来做:

  1. 找个合适的位置插入,这一步和二叉查找树的插入过程一样;

代码如下:

/**
 * @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;
        }
    }       
}
  1. 如果插入导致红黑树不平衡了,就调整。

首先我们来看看插入之后的结果:

图片说明:x为插入节点,cur表示x的父节点,uncle表示x的叔叔节点。

insert

上图中各种情形的分析如下:

  • 第一排的情形可以不做任何处理(这种插入完美符合红黑树的性质,这就是插入代码中为何有个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的深度减一了,这个在处理第四排情况时有处理。

删除也和插入一样两步走,首先你得找到他(要删除的节点),然后才能干掉他。

  1. 找到他(可结合二叉查找树的删除看)

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;
}
  1. 干掉他,在干掉他之前,首先观察他:

替换前的通用情形

上图如果cur左右子树均不空,那我们就不好删他了,删完左右子树放哪儿都不合适,那我们就找一个节点替换他吧,一般我们选他的后继节点(大于cur的最小节点)。

替换之后(其中也包含不经过替换cur本身的左子树或者右子树其一为空的情况):

找到替换节点的代码,在上面代码的if (cur.right != null && cur.left != null)里面。

经过替换后的几种情形

现在我们来结合上图分析一下,该如何搞定:

  • 对于第一排的情况,我们可以直接用他儿子代替他的位置,树依然是完美的红黑树,结束战斗;
  • 对于第二排,我们无法借兄为父,兄弟节点为红色,即使借过来,也无法增加cur的子树深度。然后我们可以对papa节点左旋,并交换sib和papa的颜色,左旋之后sib.left节点就变为cur的兄弟节点了,如下图:

image

经过从作图到右图的变换之后,成功将问题转化为第三四五排的情况了。

  • 对于第三排我们很容易通过右旋的同时交换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);
}

源码地址

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值