java数据结构和算法06(红黑树)

  这一篇咱们来看看红黑树,首先说一下我啃红黑树的一点想法,刚开始的时候比较蒙,what?这究竟是什么鬼啊?还有这种操做?有很久的时间我都缓不过来,直到我玩了两把王者以后回头一看,好像有点儿意思,因此有的时候碰到一个问题困扰了好久能够先让本身的头脑放松一下,哈哈!html

  不瞎扯咳,开始今天的正题;java

  前提:看红黑树以前必定要先会搜索二叉树node

1.红黑树的概念算法

  红黑树究竟是个什么鬼呢?我最开始也在想这个问题,你说前面的搜索二叉树多牛,各类操做效率也不错,用起来很爽啊,为何忽然又冒出来了红黑树啊?数据结构

  确实,搜索二叉树通常状况下足够了,可是有个很大的缺陷,向搜索二叉树中插入的数据必须是随机性比较强大的;若是你是插入的顺序是按照必定的顺序的,好比十、九、八、七、六、五、四、三、二、1,你把这十个数据插入到搜索二叉树中你就会看到一个比较有趣的现象;玛德,这二叉树竟然变成链表了(此时的链表也能够说是不平衡树),这就意味着变成链表以后就丧失了身为搜索二叉树的全部特性,这就很可怕,并且当这种有顺序的数据不少的时候,就特别坑爹,查询的效率贼慢;数据结构和算法

  因此就出现了红黑树这种数据结构,能够说这是一种特殊的搜索二叉树,是对搜索二叉树进行改进以后的一种很完美的二叉树,这种数据结构最厉害的就是能够自动调整树的结构,就好比上面这种有顺序的数据插入到红黑树以后,红黑树就会自动的啪啪啪给你一顿调节最后仍是一棵正常的搜索二叉树,不会变成链表就对了;ide

  那么就有人要问了,要怎么样才能将一个搜索二叉树变成红黑树呢?函数

  答:这很容易回答,字如其名,你把搜索二叉树的每一个节点要么涂成红色要么涂成黑色,使得最后这个二叉树中全部节点只有红黑两种颜色,这就是一个红黑树;this

  这时还有人要问了,是否是能够随意把搜索二叉树中的节点涂成红色或者黑色呢?spa

  答:emmmm.....你以为有这么容易么?哪有这么随便的!确定是要符合一些规则你才能涂啊,并且大佬们已经把这些规则总结出来了,咱们只须要记好这些笔记就行了!

  下面咱们就看看红黑树要知足的规则:

  (1):每一个节点不是红色就是黑色;

  (2):根节点老是黑色;

  (3):不能有两个连续的红色节点;

  (4):从根节点到每个叶节点或空子节点的黑色节点的数量必定要相同,这个黑色节点的数量叫作黑色高度,因此这个规则换句话来讲就是根节点到每个叶节点或空子节点的黑色高度相等;

  这四个规则很重要,任何红黑树都必须同时知足这四个规则,不然就不是红黑树,前三个很容易,话说第四个的空子节点是什么意思呢?字如其名,就是一个空的节点,里面什么都没有,能够看成一个null节点,好比下图所示,这个其实理解就好,不用在乎;

  第四条规则为了好理解才从根节点开始的,其实从任意一个节点开始也是同样的;能够拆分为两条,某个节点到该节点每个叶节点的黑色高度要同样,同时还要该节点到该节点的每个空子节点的黑色高度要同样;

  

  空子节点的定义为:非叶节点能够接子节点的位置;(注意,有的版本没有这个空子节点这个说法,只是说每个叶节点(NIL)都是黑色的。。。。并且这里的叶节点和以前咱们理解的叶节点还不同,看看下图,但本篇咱们仍是按照空子节点的这个说法,参考《java数据结构和算法第二版》),理解了以后实际上是同样的

 

  咱们再看看下面这个我截的图,假如不看那两个空子节点,看起来好像是符合红黑树规则的,可是咱们还要判断根节点到每一个空子节点的黑色高度是否是同样,结果不同,因而下图其实违背了规则四;

 

  这里继续说一点东西:

  新插入的节点必须是红色的,为何呢?你想啊,你往一个正常的红黑树中插入一个黑色节点,确定就会百分之百违反第四规则,这就比较坑,每插入一个节点你都要想办法去调整整个树的颜色和结构,这很影响效率;可是假如你插入的节点是红色的,并且这个红色节点还恰好是插入在一个黑色的叶节点那里,诶呀,舒服,什么都不用动;固然还有可能插入到另外一个红色节点下面,因此插入红色节点违反规则的几率是百分之五十,用脚趾头都能想到新插入的节点确定要是红色的啊!

 

2.红黑树调整的方式

  对了,知不知道计算机中红黑树怎么区分成黑色节点啊?咱们不可能真的去给计算机中的节点涂颜色吧。。。。其实咱们只须要在节点类中添加一个Boolean color的属性便可,color为true表示黑色,false为红色;

  咱们在插入红色的节点的时候有两种可能:(1)恰好把这个红色节点插入到一个黑色节点下面,这个时候直接添加就好;(2)比较不幸,插入到一个红色节点下面,这个时候就违反规则3,连续两个节点为红色,这个时候咱们要对红黑树的颜色和进行必定调整;

  咱们对不符合规则的红黑树进行调整操做主要是分为两个步骤:改变颜色和旋转

  改变颜色就很少说了,看名字就知道,咱们重点就看看旋转究竟是什么鬼?经过改变一些节点的颜色使得知足红黑树规则;

  一般状况下旋转分为左旋(逆时针)和右旋(顺时针),咱们就简单看看右旋吧,左旋差很少;注意下图这里的右旋可不是绕着节点A旋转啊,A叫作顶端,这里至关因而把这两个节点之间的路径进行了一个旋转;(这里很像绕着A节点旋转,很抱歉不是绕着A旋转,下面咱们看看红黑树中的右旋就看的很明显了。。。)

 

  在红黑树中的右旋,在下图中80是“顶端节点”,通过右旋以后顶端节点变为右子节点,而原来的左子节点50变为顶端节点,这样调整了以后70这个节点就没地方放了,因而咱们能够顺着右旋以后的50节点的右子节点找到能够存放的位置,也就是80的左子节点,咱们能够把70这个节点的移动看做横向移动;

  从右往左看就是左旋,这里就不详说了。。。,

  对了,顺便提一下,假如这里的70节点有子节点,那么子节点也会跟着一块儿移动的;咱们把70这个节点叫作80这个顶端节点的内侧子孙节点,把30叫作顶端节点80的外侧子孙节点,这个仍是很好理解的,30这个节点在树的靠外面,70这个节点始终都是在中里面。。。。

 

   网上找到两张动态的图能够看看右旋和左旋,能够好好理解这旋转,旋转真的很重要!!!

 3.添加红黑树节点

  下面咱们经过慢慢的添加一个一个节点,看看红黑树当遇到问题的时候是怎么调整的;

  (1)

 

  (2)

 

  (3)右图这种状况下根节点左边两层,右边一层,稍微有点不平衡,可是没有违反红黑树规则,因而咱们没必要在乎什么;可是假如一条路径比另一条路径多两层或者两层以上,这个确定是会违反红黑树规则的,为何呢?我也不是怎么清楚多是通过大佬们无数次试验得出来的结论吧!

 

  (4)

 

  此时咱们碰到这种状况,第一感受是改变10和25的颜色,下图所示,看起来貌似是符合红黑树结构的;可是咱们要记住当一条路径多于另一条路径两层及以上的时候确定会违反红黑树规则,咱们再仔细看看这个图就会发现违反了第四规则中:根节点到空子节点的黑色长度要同样

                  

 

  因此咱们能够知道只是单纯的改变颜色确定是不能知足红黑树规则的,咱们还要再进行旋转,咱们以25为顶端节点进行右旋,变成了下图,知足条件,ok!

 

  (5).对上面(4)中进行的完善  

  当咱们觉得(4)这就完美解决的时候,很抱歉还有另一种状况,当咱们新添加的红色节点在10的右子节点上,下图所示:

 

  这种状况就比较坑爹,确定不能像(4)中那样右旋,好比我就不信这个邪,我就要右旋,因而结果以下图一所示,我就默默地信了这个邪!

  那么我就要换一个方法了,我就想啊,若是咱们能把这个图变成(4)中的那样的结构那不就能够直接用(4)中的解决方法了吗?基于这个想法,咱们能够先试着以10为顶点节点,和15节点一块儿进行左旋,如图二所示,而后咱们就发现世界原来一切如此美好,后面的就跟(4)中同样了,这里就很少说了;

  可是在这里要注意颜色的变换和上面那个有一点不一样,(4)中是改变父节点和爷爷节点的颜色,而图二是通过旋转以后也是改变父节点和爷爷节点的颜色,就是至关于旋转以前的当前节点和爷爷节点

           

  如今咱们把上面调整方式整理一下(想必你们应该知道爷爷节点的意思吧。。。一般都叫作祖父节点,我就喜欢叫爷爷节点,哈哈):

  第一种:假如咱们添加的红色节点是添加在黑色节点下,完美;

  第二种:假如咱们添加的红色节点不当心添加到红色节点下,这里要分为两种状况:

    假如是左节点(也能够叫作爷爷节点的外侧子孙),那么就改变父节点和爷爷节点的颜色,而且以爷爷节点为顶端节点进行右旋,就ok了;

    假如是右节点(也能够叫作爷爷节点的内侧子孙) ,那么就改变当前节点和爷爷节点的颜色,而后要以父节点为顶端节点进行左旋,再绕爷爷节点右旋;

  小知识:怎么快速的判断一个节点是否是它爷爷节点的外侧子孙仍是内侧子孙呢?你要看当前节点和父节点是否是在同侧,同侧的就是外侧节点,不一样侧就是内侧节点;举个例子,假如当前节点的父节点是左节点,当前节点也是属于左节点,都是左边,那当前节点就是其爷爷节点的外侧节点,若是当前节点是右节点,那就是内侧子孙。。。   

 

  (6).对上面(5)中进一步的完善

  ╮( ̄▽ ̄")╭,是否是以为各类补充的内容啊,哈哈哈,正常!这是最后一个补充了。。。

  说出来大家可能不信,上面的(4)和(5)其实都是针对在节点插入以后致使树不平衡而作出的调整,可是会有点小问题,就好比在(4)中,假如50这个节点不是根节点而是一个普通的红色节点,那么在咱们首先进行颜色变换的时候就会出现问题,例以下图,那么咱们后面的所谓右旋也就没啥用了,因此咱们要解决一下这种隐患,最好是在插入数据以前首先对红黑树中的这种有隐患的节点首先进行颜色调整或者旋转;

 

   那么确定有人要问了,卧槽!这该怎么作啊?我不会呀,怎么办?

  答:不会才正常啊,才能显示那些大佬很牛啊!咱们只须要在插入节点以前对树的一些有隐患的结构进行调整便可(颜色调整和旋转),调整这个有隐患的结构是为了让咱们后续的插入节点更加方便;

  6.1.颜色调整:红黑树中咱们要插入节点,实际上是和搜索二叉树同样,从根节点开始一个一个的比较节点数值大小,小就继续和左子节点比较.....最终确定能够找到肯定的位置,在这个找的过程当中,假如一个节点为黑色,它的两个子节点都为红色,这就是一种有隐患的结构,咱们须要将父节点和两个子节点颜色都改变一下,下图所示:

  6.2.旋转:在6.1中虽然对这样的结构进行了颜色的改变,可是有个小缺陷,假如10节点的父节点是红色的呢?那么咱们这样改变颜色也是不符合红黑树规则三(不能有连续的两个红节点)的,因而咱们还要进行旋转操做,而旋转操做的的话,无非仍是上面说的那两种,外侧子孙和内侧子孙;

  注意:这里的内侧子孙和外侧子孙,不是指新插入的节点,而是两个连续红色节点中的子节点。。。。也就是下面第二个图中的节点10就是爷爷节点50的外侧子孙

  这样提及来比较抽象,咱们来实际看看两个例子就ok了;

  外侧子孙:假如在向下查找插入点的途中找到了以下结构:

 

 

  对红黑树的调整就结束了,有没有发现通过这种调整以后使得后续插入红色节点就容易了不少,并且纵观整棵红黑树,红色节点在慢慢向上运动,直到根节点也被调整成红色,最终咱们只须要把根节点变成黑色就好! 插入节点2就不用多说了吧!

  内侧子孙:这个我是在不想说了,偷个懒,嘿嘿嘿!其实和前面同样的,就是先改变两个连续红节点的子节点和爷爷节点的颜色,而后绕父节点左旋,最后绕爷爷节点右旋,换汤不换药;

 

 4.总结重点

  咱们把这篇的重点提出来,其实就是分为插入前和插入后两步:

  (1).插入前咱们必须调整一下有隐患的结构,具体操做:当一个黑色节点有两个红色节点的时候,咱们就改变这三个节点的颜色,红变黑,黑变红;可是因为这个黑色节点的父节点多是黑色,也多是红色

     当黑色节点的父节点是黑色的时候,那么这个改变颜色不会形成任何影响

       当黑色节点的父节点是红色的时候,改变颜色以后就会违反红黑树规则三,有连续的两个红色节点,咱们就须要进行旋转,对于旋转,咱们有两种状况

        第一种,假如两个连续的红色节点的子节点是外侧子孙,那么就先改变父节点和爷爷节点的颜色,而后以这个外侧子孙的爷爷节点进行右旋

        第二种,假如两个连续的红色节点的子节点是内侧子孙,那么就先改变内侧子孙和爷爷节点的颜色,而后先绕内侧子孙的父节点进行左旋,最后绕爷爷节点右旋;

  (2)插入节点以后,假如插入的是黑色节点下面,那没有什么改变;假如是插入在红色节点之下,那么就会违反红黑树规则三,两个连续的红色节点,此时就会有两种调整方式:

        第一种,假如这个新插入的节点是外侧子孙,那么改变父节点和爷爷节点的颜色,而后绕着爷爷节点进行右旋

        第二种,假如这个新插入的节点是内侧子孙,那么改变当前插入节点和爷爷节点的颜色,再绕着父节点左旋,再绕着爷爷节点右旋

 

5.代码

  看看前面的逻辑贼多,因此代码的话最好内心准备,下面咱们就用java代码来看一下红黑树添加节点的过程;

  为何暂时不说删除红黑树节点呢?由于删除节点有点儿复杂,后面有时间再说吧!并且删除的分为真正的删除和伪删除,真正的删除就是慢慢研究每个删除的步骤每一步代码,从树中删除节点;而伪删除其实就是在节点类中加一个boolean变量,标识该节点是否为已删除节点,伪删除其实避免了删除红黑树的所有复杂的逻辑,很容易,可是缺陷也很大,由于删除的节点还保存在树中。。。

  emmm....原本想本身实现一下的,然而看到一些大佬的博客实现代码,瞬间感受本身的代码很丑陋,就借用一下大佬的代码;

节点类

public class RBTree<T extends Comparable<T>> {

    private RBTNode<T> mRoot;    // 根结点

    private static final boolean RED   = false;
    private static final boolean BLACK = true;

    public class RBTNode<T extends Comparable<T>> {
        boolean color;        // 颜色
        T key;                // 关键字(键值)
        RBTNode<T> left;    // 左孩子
        RBTNode<T> right;    // 右孩子
        RBTNode<T> parent;    // 父结点

        public RBTNode(T key, boolean color, RBTNode<T> parent, RBTNode<T> left, RBTNode<T> right) {
            this.key = key;
            this.color = color;
            this.parent = parent;
            this.left = left;
            this.right = right;
        }

    }

    ...
}
View Code

 

 右旋

/* 
 * 对红黑树的节点(y)进行右旋转
 *
 * 右旋示意图(对节点y进行左旋):
 *            py                               py
 *           /                                /
 *          y                                x                  
 *         /  \      --(右旋)-.            /  \                     #
 *        x   ry                           lx   y  
 *       / \                                   / \                   #
 *      lx  rx                                rx  ry
 * 
 */
private void rightRotate(RBTNode<T> y) {
    // 设置x是当前节点的左孩子。
    RBTNode<T> x = y.left;

    // 将 “x的右孩子” 设为 “y的左孩子”;
    // 若是"x的右孩子"不为空的话,将 “y” 设为 “x的右孩子的父亲”
    y.left = x.right;
    if (x.right != null)
        x.right.parent = y;

    // 将 “y的父亲” 设为 “x的父亲”
    x.parent = y.parent;

    if (y.parent == null) {
        this.mRoot = x;            // 若是 “y的父亲” 是空节点,则将x设为根节点
    } else {
        if (y == y.parent.right)
            y.parent.right = x;    // 若是 y是它父节点的右孩子,则将x设为“y的父节点的右孩子”
        else
            y.parent.left = x;    // (y是它父节点的左孩子) 将x设为“x的父节点的左孩子”
    }

    // 将 “y” 设为 “x的右孩子”
    x.right = y;

    // 将 “y的父节点” 设为 “x”
    y.parent = x;
}
View Code

  

左旋

/* 
 * 对红黑树的节点(x)进行左旋转
 *
 * 左旋示意图(对节点x进行左旋):
 *      px                              px
 *     /                               /
 *    x                               y                
 *   /  \      --(左旋)-.           / \                #
 *  lx   y                          x  ry     
 *     /   \                       /  \
 *    ly   ry                     lx  ly  
 *
 *
 */
private void leftRotate(RBTNode<T> x) {
    // 设置x的右孩子为y
    RBTNode<T> y = x.right;

    // 将 “y的左孩子” 设为 “x的右孩子”;
    // 若是y的左孩子非空,将 “x” 设为 “y的左孩子的父亲”
    x.right = y.left;
    if (y.left != null)
        y.left.parent = x;

    // 将 “x的父亲” 设为 “y的父亲”
    y.parent = x.parent;

    if (x.parent == null) {
        this.mRoot = y;            // 若是 “x的父亲” 是空节点,则将y设为根节点
    } else {
        if (x.parent.left == x)
            x.parent.left = y;    // 若是 x是它父节点的左孩子,则将y设为“x的父节点的左孩子”
        else
            x.parent.right = y;    // 若是 x是它父节点的左孩子,则将y设为“x的父节点的左孩子”
    }
    
    // 将 “x” 设为 “y的左孩子”
    y.left = x;
    // 将 “x的父节点” 设为 “y”
    x.parent = y;
}
View Code

  

插入节点

/* 
 * 将结点插入到红黑树中
 *
 * 参数说明:
 *     node 插入的结点        // 对应《算法导论》中的node
 */
private void insert(RBTNode<T> node) {
    int cmp;
    RBTNode<T> y = null;
    RBTNode<T> x = this.mRoot;

    // 1. 将红黑树看成一颗二叉查找树,将节点添加到二叉查找树中。
    while (x != null) {
        y = x;
        cmp = node.key.compareTo(x.key);
        if (cmp < 0)
            x = x.left;
        else
            x = x.right;
    }

    node.parent = y;
    if (y!=null) {
        cmp = node.key.compareTo(y.key);
        if (cmp < 0)
            y.left = node;
        else
            y.right = node;
    } else {
        this.mRoot = node;
    }

    // 2. 设置节点的颜色为红色
    node.color = RED;

    // 3. 将它从新修正为一颗二叉查找树
    insertFixUp(node);
}

/* 
 * 新建结点(key),并将其插入到红黑树中
 *
 * 参数说明:
 *     key 插入结点的键值
 */
public void insert(T key) {
    RBTNode<T> node=new RBTNode<T>(key,BLACK,null,null,null);

    // 若是新建结点失败,则返回。
    if (node != null)
        insert(node);
}

/*
 * 红黑树插入修正函数
 *
 * 在向红黑树中插入节点以后(失去平衡),再调用该函数;
 * 目的是将它从新塑形成一颗红黑树。
 *
 * 参数说明:
 *     node 插入的结点        // 对应《算法导论》中的z
 */
private void insertFixUp(RBTNode<T> node) {
    RBTNode<T> parent, gparent;

    // 若“父节点存在,而且父节点的颜色是红色”
    while (((parent = parentOf(node))!=null) && isRed(parent)) {
        gparent = parentOf(parent);

        //若“父节点”是“祖父节点的左孩子”
        if (parent == gparent.left) {
            // Case 1条件:叔叔节点是红色
            RBTNode<T> uncle = gparent.right;
            if ((uncle!=null) && isRed(uncle)) {
                setBlack(uncle);
                setBlack(parent);
                setRed(gparent);
                node = gparent;
                continue;
            }

            // Case 2条件:叔叔是黑色,且当前节点是右孩子
            if (parent.right == node) {
                RBTNode<T> tmp;
                leftRotate(parent);
                tmp = parent;
                parent = node;
                node = tmp;
            }

            // Case 3条件:叔叔是黑色,且当前节点是左孩子。
            setBlack(parent);
            setRed(gparent);
            rightRotate(gparent);
        } else {    //若“z的父节点”是“z的祖父节点的右孩子”
            // Case 1条件:叔叔节点是红色
            RBTNode<T> uncle = gparent.left;
            if ((uncle!=null) && isRed(uncle)) {
                setBlack(uncle);
                setBlack(parent);
                setRed(gparent);
                node = gparent;
                continue;
            }

            // Case 2条件:叔叔是黑色,且当前节点是左孩子
            if (parent.left == node) {
                RBTNode<T> tmp;
                rightRotate(parent);
                tmp = parent;
                parent = node;
                node = tmp;
            }

            // Case 3条件:叔叔是黑色,且当前节点是右孩子。
            setBlack(parent);
            setRed(gparent);
            leftRotate(gparent);
        }
    }

    // 将根节点设为黑色
    setBlack(this.mRoot);
}
View Code

  

参考大佬博客:https://www.cnblogs.com/skywang12345/p/3624343.html

相关文章
相关标签/搜索