数据结构之三-红黑树

概述

  搜索二叉树在插入的数据是有序的时候会非常不平衡,几乎变成了线性结构,如插入数据顺序为10,20,30,40,50,那么该二叉树的结构会如下图所示,那么这样就和链表没啥区别,查找的时间复杂度就为O(n),而不是O(logN),为了以较快的时间搜索一颗树,我们就要保证这颗树的平衡性,也就是树的左右子树节点个数趋近相等,红黑树就是这样一颗平衡树,红黑树对插入的数据项会进行检查,如果插入项破坏了树的平衡,树会自动修复。

1.1红黑树特征

1 树中节点非黑及红

2 插入和删除节点必须遵循红黑规则:

    1.每个节点不是红色就是黑色的;

  2.根节点总是黑色的;

  3.如果节点是红色的,则它的子节点必须是黑色的(反之不一定),(也就是从每个叶子到根的所有路径上不能有两个连续的红色节点);

  4.从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。

注意:新插入的节点颜色总是红色的,这是因为插入一个红色节点比插入一个黑色节点违背红-黑规则的可能性更小,原因是插入黑色节点总会改变黑色高度(违背规则4),但是插入红色节点只有一半的机会会违背规则3(因为父节点是黑色的没事,父节点是红色的就违背规则3)另外违背规则3比违背规则4要更容易修正。当插入一个新的节点时,可能会破坏这种平衡性,那么红-黑树是如何修正的呢?

1.2 红黑树自我修正方法

红黑树节点实体类

public class RBNode<T extends Comparable<T>> {
    boolean color;//颜色
    T key;//关键值
    RBNode<T> left;//左子节点
    RBNode<T> right;//右子节点
    RBNode<T> parent;//父节点
     
    public RBNode(boolean color,T key,RBNode<T> parent,RBNode<T> left,RBNode<T> right){
        this.color = color;
        this.key = key;
        this.parent = parent;
        this.left = left;
        this.right = right;
    }
     
    //获得节点的关键值
    public T getKey(){
        return key;
    }
}

  

1.2.1改变节点颜色

  新插入的节点为15,一般新插入颜色都为红色,那么我们发现直接插入会违反规则3,改为黑色却发现违反规则4。这时候我们将其父节点颜色改为黑色,父节点的兄弟节点颜色也改为黑色。通常其祖父节点50颜色会由黑色变为红色,但是由于50是根节点,所以我们这里不能改变根节点颜色。

1.2.2 右旋

  首先要说明的是节点本身是不会旋转的,旋转改变的是节点之间的关系,选择一个节点作为旋转的顶端,如果做一次右旋,这个顶端节点会向下和向右移动到它右子节点的位置,它的左子节点会上移到它原来的位置。右旋的顶端节点必须要有左子节点。

1.2.3 左旋

左旋代码实现(右旋正好相反,原理一样):

/**
     * 以C为轴做左旋
     * 左旋操作做了三件事
     * 1 将的B的左子节点赋给C的右子节点,并将C赋予B的左子节点
     * 2 将C的父节点A赋给B的父节点,同时更新A的子节点为B(区分左右)
     * 3 将B的左子节点设置为C,将C父节点设置为B
     * @param C
     */
    private void leftRotate(RBNode<T> C){
        RBNode<T> root;
        RBNode<T> B = C.right;
        /*  1 将的B的左子节点赋给C的右子节点,
            并将B的左子节点父节点指向C(B的左子节点不为空)
         */
        C.right = B.left;
        if(B.left != null){
            B.left.parent=C;
        }
        /*
            2 将C的父节点A赋给B的父节点,同时更新A的子节点为B(区分左右)
         */
        B.parent = C.parent;
        if(C.parent == null){
            root=B;//如果C为根节点则将B设置为根
        }else{
            if(C == C.parent.left){
                C.parent.left = B;
            }else{
                C.parent.right = B;
            }
        }
        //3 将B的左子节点设置为C,将C父节点设置为B
        B.left = C;
        C.parent = B;
    }

 2 插入操作

和搜索二叉树一样,都是都是得先找到插入的位置,然后再将节点插入,最后利用旋转操作修正红黑树;修正的过程要分情况讨论。

1:如果是第一次插入,由于原树为空,所以只会违反红-黑树的规则2,所以只要把根节点涂黑即可;

2:如果插入节点的父节点是黑色的,那不会违背红-黑树的规则,什么也不需要做;

但是遇到如下三种情况,我们就要开始变色和旋转了:

  ①、插入节点的父节点和其叔叔节点(祖父节点的另一个子节点)均为红色。

  ②、插入节点的父节点是红色的,叔叔节点是黑色的,且插入节点是其父节点的右子节点。

  ③、插入节点的父节点是红色的,叔叔节点是黑色的,且插入节点是其父节点的左子节点。

  下面我们挨个分析这三种情况都需要如何操作,然后给出实现代码。

  在下面的讨论中,使用N,P,G,U表示关联的节点。N(now)表示当前节点,P(parent)表示N的父节点,  

        U(uncle)表示N的叔叔节点,G(grandfather)表示N的祖父节点,也就是P和U的父节点。

        ①、插入节点的父节点和其叔叔节点均为红色。此时,肯定存在祖父节点,但是不知道父节点是其左子节点还是右子节点,但是由于对称性,我们只要讨论出一边的情况,另一种情况自然也与之对应。这里考虑父节点是其祖父节点的左子节点的情况,如下左图所示:

                情况1

  对于这种情况,我们要做的操作有:将当前节点(4) 的父节点(5) 和叔叔节点(8) 涂黑,将祖父节点(7)涂红,变成了上有图所示的情况。再将当前节点指向其祖父节点,再次从新的当前节点开始算法(具体看下面的步骤)。这样上右图就变成情况2了。

                情况2

对于情况2:插入节点的父节点是红色的,叔叔节点是黑色的,且插入节点是其父节点的右子节点。我们要做的操作有:将当前节点(7)的父节点(2)作为新的节点,以新的当前节点为支点做左旋操作。完成后如左下图所示,这样左下图就变成情况3了。

                 情况3

 

  对于情况3:插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的左子节点。我们要做的操作有:将当前节点的父节点(7)涂黑,将祖父节点(11)涂红,在祖父节点为支点做右旋操作。最后把根节点涂黑,整个红-黑树重新恢复了平衡,如右上图所示。至此,插入操作完成!

                修复完的红黑树

 

红黑树修复代码:

private void insertFixUp(RBNode<T> node){
    RBNode<T> parent,gparent;//定义父节点和祖父节点
     
    //需要修正的条件:父节点存在,且父节点的颜色是红色
    while(((parent = parentOf(node)) != null) && isRed(parent)){
        gparent = parentOf(parent);//获得祖父节点
         
        //若父节点是祖父节点的左子节点,下面的else相反
        if(parent == gparent.left){
            RBNode<T> uncle = gparent.right;//获得叔叔节点
             
            //case1:叔叔节点也是红色
            if(uncle != null && isRed(uncle)){
                setBlack(parent);//把父节点和叔叔节点涂黑
                setBlack(uncle);
                setRed(gparent);//把祖父节点涂红
                node = gparent;//把位置放到祖父节点处
                continue;//继续while循环,重新判断
            }
             
            //case2:叔叔节点是黑色,且当前节点是右子节点
            if(node == parent.right){
                leftRotate(parent);//从父节点出左旋
                RBNode<T> tmp = parent;//然后将父节点和自己调换一下,为下面右旋做准备
                parent = node;
                node = tmp;
            }
             
            //case3:叔叔节点是黑色,且当前节点是左子节点
            setBlack(parent);
            setRed(gparent);
            rightRotate(gparent);
        }else{//若父节点是祖父节点的右子节点,与上面的情况完全相反,本质是一样的
            RBNode<T> uncle = gparent.left;
             
            //case1:叔叔节点也是红色的
            if(uncle != null && isRed(uncle)){
                setBlack(parent);
                setBlack(uncle);
                setRed(gparent);
                node = gparent;
                continue;
            }
             
            //case2:叔叔节点是黑色的,且当前节点是左子节点
            if(node == parent.left){
                rightRotate(parent);
                RBNode<T> tmp = parent;
                parent = node;
                node = tmp;
            }
             
            //case3:叔叔节点是黑色的,且当前节点是右子节点
            setBlack(parent);
            setRed(gparent);
            leftRotate(gparent);
        }
    }
    setBlack(root);//将根节点设置为黑色
}

  

  

原文地址:https://www.cnblogs.com/sharing-java/p/10808206.html