红黑树

博主前面的文章 Java HashMap实现原理2——HashMap详解中提到,JDK8中,对HashMap有优化,当某个位置的冲突元素达到8个时,改用红黑树而不是链表存储。为什么是红黑树呢?且看下文慢慢道来。
说红黑树之前,不得不把我们的数组、链表、还有上文的二叉搜索树“批判“一番。数组的优点在于查找快,而插入删除麻烦。链表正好相反,插入删除方便,但是查找麻烦。这样可就呵呵哒了,聪明的前辈们就想着折中,查找、插入和删除都不能慢。于是乎有了树这种结构,树的结构也有很多种,其中的二叉搜索树就很nice,删除和插入很快自不必说,查找也很快,时间复杂度O(logn)。这样一看啦,二叉搜索树就够用了,为啥还要整个红黑树呢?问题就在于,如果构建二叉树的时候,由一组随意的数据构建还好,要是由一组基本有序的数构建出来的基本是个链表,查找的时间复杂度又退化到O(n)。为了能保证在O(logn)的时间复杂度内查找,需要保证二叉树尽可能是平衡的,即每个结点的左右子树的结点个数尽可能的相等。一旦因为插入和删除操作破环了平衡就要进行调整,保证平衡。
可以知道,红黑树是在二叉搜索树进行了改进,至于如何才能思考出使用怎样的结构才能达到平衡的目的,这个博主也没思考过,我们就站在巨人的肩膀直接来学习红黑树的性质和用法。

性质

红黑树在二叉搜索树的基础上增加了一个存储位来表示结点的颜色,可以是红色或者黑色,通过对任意一条从根结点出发到叶子结点的路径上各个结点的颜色进行约束,确保最大路径也不会比最长路径大过两倍。红黑树满足如下五条性质:

  1. 根结点是黑色的
  2. 红色结点的子结点都是黑色的
  3. 每个结点不是黑色的就是红色的
  4. 叶子(NIL)结点都是黑色的
  5. 任意结点出发的到所有后代叶结点的路径上包含的黑色结点数相同

对于第四条规则,可以暂时忽略尾部增加的NIL叶子结点,关注内部结点即可。

修正

上面说红黑树的魔力在于对二叉树的修正,而它修正的方式包括:图色和旋转。
对于图色比较好理解,如果新插入的结点或者刚删除一个结点,导致现在的二叉树不满足以上原则,则需要通过重新图色使得符合红黑树性质。具体的用法在后面插入删除操作中介绍。
旋转包括两种:左旋和右旋。左旋和右旋也很好理解,分别给大家看一个直观的动态图。


看完效果图,就是上代码了
对于左旋,先处理s和e结点,再处理s的左子树和e的右子树。

void leftRotate(Node root,Node e){
    if(root!=null&&e!=null){
        Node s=e.rChild;
        if(e.p==null){//如果e是根结点
            root=s;//s为根结点
        }else if(e.p.lChild==e){
            s.p=e.p;
            e.p.lChild=s;//e原来的父结点左子树指向s
        }else{
            s.p=e.p;
            e.p.rChild=s;//e原来的父结点的右子树指向s
        }
        e.p=s;//e的父结点指向s
        s.lChild=e;//s的左子树指向e
        e.rChild=s.lChild;//e的右子树指向s的左子树
        if(s.lChild!=null){//如果s的左结点不为空,则e指向s的左子树结点
            s.lChild.p=e;
        }
    }
}

注释已经很详细了,可以看出,右旋和左旋很像,只是改动的子树不同,看代码:

void rightRotate(Node root,Node s){
    if(root==null||s==null){
        return;
    }
    Node e=s.lChild;
    if(s.p==null){//s是根结点
        root=e;
    }else{//调整e到原来s的位置
        e.p=s.p;
        if(s.p.lChild==s){//修改e和父结点的指针
            s.p.lChild=e;
        }else{
            s.p.rChild=e;
        }   
    }
    s.lChild=e.rChild;//修改e的右子树到s的左子树
    if(e.rChild!=null){
        e.rChild.p=s.lChild;
    }
    e.rChild=s;//修改s到e的右子树
    s.p=e;

}

差别不大,应该都能看懂。

插入

红黑树是基于二叉搜索树的,所以插入操作是在二叉搜索树的插入的基础上增加了修正操作。
二叉搜索树的插入,就是找到对应的结点位置,然后插入,比较简单,不明白的同学可以参看博主的文章二叉搜索树
插入结点一般有两种颜色选择:红色或者黑色,一般选择插入红色结点,为什么呢?因为插入黑色结点的话,一定会违背上文提到的红黑树的性质5,但是插入红色的话不会违背性质5,但是有可能违背性质2(概率1/2,父结点红黑都有可能),而且违背性质2比违背性质5更容易调整,所以插入结点的颜色都设为红色。
插入可分为如下五种情形:

  1. 原树为空,只需重新上色为黑色即可
  2. 父结点为黑色,不违背规则,直接插入即可
  3. 父结点和叔叔结点均为红色
  4. 父结点红色,叔叔结点黑色,插入父结点的右子树
  5. 父结点红色,叔叔结点黑色,插入父结点的左子树

前两种情况比较简单,重点看后面三种情况:
情况3,考虑这样一种情况,如图:
这里写图片描述
图(a)中,当前结点为4,父结点5和叔叔结点8都是红色,对应的调整到图(b)。调整操作为将父结点5和叔叔结点8涂黑,爷结点涂红,将当前结点指向爷结点,状态由情况3转到了情况4,我们来看情况4。
先将图(b)中的当前结点7的父结点2作为新的当前结点,对2做左旋操作,得到图(c)
这里写图片描述
由于当前结点是2,所以左旋后得到的红黑树结构对应于情况5,父结点7红色和叔结点14黑色,2位于结点7点左子树。这个情况下我们该如何操作呢?父结点7涂黑,爷结点11涂红,再以爷结点11做右旋操作,涂黑根结点,如图(d)。这样插入结点4的调整操作就完成了,红黑树从图(a)调整到图(d)。当然,不是每次插入操作都需要这样调整,调整前的状态可能位于情况3,也可能是情况4,甚至直接是情况5。
我们刚刚以当前结点的父结点位于爷结点右子树的情况举例,如果位于爷结点的左子树呢?左右子树的选取以及旋转略有不同,我们直接看代码:

void rbInsert(Node root,Node z){
    Node  uncle;
    while(z.p!=null&&z.p.color==RED){
        if(z.p==z.p.p.lChild){//z的父结点位于爷结点的左子树
            uncle=z.p.p.rChild;
            if(uncle.color==RED){//第三种情况
                uncle.color=BLACK;//叔结点变黑
                z.p.color=BLACK;//父结点变黑
                z.p.p=RED;//爷结点变红
                z=z.p.p;
            }else if(uncle.color==BLACK&&z.p.rChild==z){//第四种情况
                z=z.p;//当前结点指向父结点
                leftRotate(root,z);//左旋
            }           
            z.p.color=BLACK;
            z.p.p.color=RED;
            rightRotate(root,z.p.p);
        }else{//位于爷结点右子树
            uncle=z.p.p.lChild;
            if(uncle.color==RED){//第三种情况
                uncle.color=BLACK;//叔结点变黑
                z.p.color=BLACK;//父结点变黑
                z.p.p=RED;//爷结点变红
                z=z.p.p;
            }else if(uncle.color==BLACK&&z.p.rChild==z){//第四种情况
                z=z.p;//当前结点指向父结点
                leftRotate(root,z);//左旋
            }           
            /*第五种情况*/
            z.p.color=BLACK;
            z.p.p.color=RED;
            rightRotate(root,z.p.p);
        }
    }
    root.color==BLACK;//根结点设置为黑色,对应于情况1
}

插入操作就说完了,主要考虑的就是情况3、4、5三种,他们的区别在于插入结点的父结点和叔结点的颜色不同,以及当前结点位于父结点的子树位置不同。理清楚这三种情况即可完成插入操作。

删除

删除可以算是红黑树中最复杂的操作了,不是它时间复杂度高,而是操作起来略显麻烦。
与添加一样,红黑树的插入操作是在二叉搜索树插入操作的基础上进行了调整。二叉搜索树的删除操作分三种情况考虑,详情请参考二叉搜索树

  1. 叶子结点,直接删除
  2. 只有一个子结点,叶子结点顶替被删除的结点位置
  3. 两个子结点,找到要删除结点的中继结点,如果中继结点为右结点,移动右子树;如果不是,则进行一系列操作,balabala。

删除某个节点后,被删除位置补位结点会涂成原有的颜色,所以不会出现破坏红黑树平衡的事情。但是补位的结点的子结点和补位结点的父结点构成的新树,可能会破坏红黑树的结构。
为了简述,我们称补位结点的子结点为当前结点,有四种情况需要调整红黑树结构:
1. 当前结点黑色,兄弟结点红色
2. 当前结点黑色,兄弟结点黑色,且兄弟结点的子结点黑色
3. 当前结点黑色,兄弟结点黑色,兄弟结点的左结点红色,右结点黑色
4. 当前结点黑色,兄弟结点黑色,兄弟结点的右结点红色,左结点任意色(红色或黑色)
情况1如图(B表示补位结点的父结点,A是补位结点的右子树,D是补位结点父结点的右子树,下同):
这里写图片描述
对应操作为:父结点涂红,兄弟结点涂黑,对父结点左旋。
情况2如图:
这里写图片描述
对应操作为:兄弟结点涂红,当前结点指向父结点,父结点指向当前结点的祖父结点。
情况3如图:
这里写图片描述
对应操作为:兄弟结点涂红,兄弟结点的左结点涂黑,对兄弟结点进行右旋。
情况4如图:
这里写图片描述
对应操作为:兄弟结点涂红,父结点涂黑,兄弟结点的右结点涂黑,对父结点左旋。
可以看出,如果是从情况1开始发生的,后续可能是情况2,3,4中的一种:如果是情况2开始,就不可能再出现3和4;如果是情况3开始,必然会导致情况4的出现;如果2和3都不是,那必然是4。
除了这四种情况外,其他还有两种简单的情况:当前结点是黑色的根节点,那么不用任何操作,因为并没有破坏树的平衡性(子树中黑色结点的个数没变,红色结点的子结点依然都是黑色的——删除结点的父结点如果是红色,那么新的子结点仍是黑色)。如果当前节点是红色的,说明刚刚移走的节点是黑色的,那么不管替换节点的父节点是啥颜色,我们只要将当前节点涂黑就可以了,规则5可以保证,规则4也可以保证。
删除的代码,本文暂时不贴了,这儿比较复杂,过几天博主补上,有兴趣的同学,可以通过【数据结构和算法05】 红-黑树(看完包懂~) 教你初步了解红黑树两篇文章先了解。

总结

to be continued…
很惭愧,做了一点微小的贡献!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值