浅析红黑树!【建议收藏】

前些天,我们讲解了搜索二叉树和AVL树,也知道了AVL树的自平衡机制,是如何进行旋转的,也知道了对于AVL树来说,查找数值的时间复杂度在O(logN)内,也就是说整棵树的深度,就是最大的查找次数。但是在AVL树在进行自平衡旋转时,还是耗费了大量的时间,所以就有了后来的红黑树。具体红黑树是什么?我们往下看。

前期文章:二叉树的概念以及搜索二叉树

平衡二叉树(AVL树),原来如此!!!

本期文章源码:GitHub

一、2-3查找树

首先要深刻理解红黑树的性质,我们还得来看一下2-3树是个什么情况。红黑树的来源,可以说就是来自于2-3树。

定义:一颗2-3查找树或者一颗空树,由以下两种节点组合而成:

2-节点: 节点内有一个数值域,还有两个存放左右孩子节点的内存地址的区域。左孩子节点的数值比当前节点小,右孩子节点的数值比当前节点大。

3-节点: 节点内有两个数值域,还有三个存放孩子节点的内存地址的区域。左孩子节点的数值都是小于该节点的,右孩子的数值都是大于该节点的,中间孩子的数值在该节点两个数值的中间。

image-20210826095821526

查找:对于2-3树的查找操作,就类似于BST树是一样的。小于的往左子树,大于的就往右子树,在中间的,往中间的子树就行,比较简单。

插入:对于2-3树来说,插入节点就很奇怪。假设是空树,直接新建2-节点,返回去作为根结点即可。那如果本身不是空树的情况下,我们不能像BST树那样,插入到null节点处,所以我们分为两种情况来讨论:

  • 待插入节点当前是2-节点,这种情况,新插入的数值比本身节点的数值小,就插到左边,反之插到右边。

  • 待插入节点当前是3-节点,因为本身这种树,只有2-节点和3-节点,如果强行插入到3-节点内,就会变为4-节点**(临时)**,所以此时就需要将这个临时的4-节点进行拆分,如下图:

    image-20210826101817087

可能有同学会疑惑,直接插入到左孩子或者右孩子不就行了吗?答案肯定是当然不行!

因为2-3树是一个绝对平衡的树,也就是说,无论什么时候,无论哪一个节点,它的左右子树的高度必须是一样的。而AVL树是左右子树的高度差不超过1,二者有一定的区别。所以在插入新的节点的时候,除了root为null时,其他的情况,都是与另一个节点进行组合,产生3-节点或者临时的4-节点

就是因为2-3树这样的绝对平衡的性质,从而在此基础之上,就衍生了左倾红黑树(红黑树中的一种)。

二、从2-3树到红黑树

上文中,我们介绍了2-3树的情况,又也讲了插入操作。现在我们就在2-3树的插入操作的基础之上,来说一说红黑树。

红黑树的节点:

//红黑树节点
private static final boolean RED = true;
private static final boolean BLACK = false; //为了好区分颜色,我们定义两个静态常量值

private static class TreeNode {
    
    
    public int val;
    public TreeNode left, right;
    public boolean color; //true表示红色,false表示黑色
    
    public TreeNode(int val) {
    
    
        this.val = val;
        color = RED; //默认新的节点是红色
    }
}

红黑树的节点,跟BST的节点差不多,只是在此基础上新添了一个成员变量:color,来表示当前节点的颜色。

我们先来从红黑树的性质讲起:

  • 每个节点不是红色就是黑色

  • 每个叶节点(null节点)是黑色

    此处的叶节点不是左右子树为空的节点;而是null节点。

  • 如果一个节点是红色,那么它的两个孩子节点是黑色

    也就是说,没有连续的红色节点。对比红黑树插入的节点形状就可以知道,总共也就两种插入节点的类型,并且根节点都是黑色的。

  • 从某一个节点,到每个叶节点所途径的节点中,黑色节点的数量一样多

    仔细回想,红色节点的意义就是:说明该节点的和它的父节点组合在一起,形成3-节点。这也就回到了2-3树上,绝对平衡树,肯定是某一个节点,到每个节点途径的节点数是一样的。

    image-20210826104752827

简单的说了一下红黑树的性质,我们来具体的看一下红黑树该怎么进行插入,与2-3树的插入又有什么区别?

插入

image-20210826112425336

上图就是,罗列出了所有的插入节点的情况,因为我们所写的是左倾红黑树,所以红色节点应该是在它父节点的左边。也就是上图用浅绿色框起来的几种情况,是需要进行调整的。

情况一:左倾红黑树,红色节点在父节点的左边,所以情况一是需要进行左旋转的。如图:

image-20210826153535381

//左旋转代码
public TreeNode leftRotate(TreeNode node) {
    
    
    TreeNode x = node.right;
    //左旋转过程
    node.right = x.left;
    x.left = node;
    //重新改颜色
    x.color = node.color; //新根节点继承原节点的颜色
    node.color = RED; //原根节点改为红色
    return x; //返回新的根结点
}

情况二:类似于AVL树中的LL型,相信大家很自然的就想到是右旋转,如图:

image-20210826155309569

//右旋转代码
public TreeNode rightRoate(TreeNode node) {
    
    
    TreeNode x = node.left;
    //右旋转过程
    node.left = x.right;
    x.right = node;
    //重新改颜色
    x.color = node.color; //新根结点 继承 原根结点的颜色
    node.color = RED; //原根结点改为红色
    return x; //返回新的根结点
}

情况三: 这种情况,相信大家并不陌生,类似于AVL树的LR型,先左旋转,再右旋转,如图:

image-20210826160903299

情况四: 终于来到了这最后一种情况,那就是颜色的反转。当一个节点的左右两个孩子节点都是红色的时候,我们专门有一个方法,用于反转这种情况,需要将父节点改为红色,将两个子节点改为黑色。

image-20210826162044160

//颜色反转
public void flipColor(TreeNode node) {
    
    
    node.color = RED;
    node.left.color = BLACK;
    node.right.color = BLACK;
}

上面就是红黑树插入节点的所有情况分析,总结起来,就那么一点点,跟AVL树的旋转操作差不多,我们只需要修改颜色,以及颜色反转的方法。现在我们就将上面的几种情况,进行综合:

//红黑树插入节点
public void add(int val) {
    
    
    root = add(root, val);
    root.color = BLACK; //红黑树性质,根结点始终保持黑色
}

private TreeNode add(TreeNode node, int val) {
    
    
    if (node == null) {
    
    
        return new TreeNode(val);
    }
    
    if(val < node.val) {
    
    
        node.left = add(node.left, val);
    } else {
    
    
        node.right = add(node.right, val);
    }
    
    //旋转操作以及颜色调整
    //左孩子是黑色,右孩子是红色---情况一
    if (!isRed(node.left) && isRed(node.right)) {
    
     
        node = leftRotate(node); //左旋转
    }
    
    //左孩子是红色,左孩子的左孩子也是红色---情况二
    if (isRed(node.left) && isRed(node.left.left)) {
    
    
        node = rightRotate(node); //右旋转
    }
    
    //左右两个孩子都是红色---情况四
    if (isRed(node.left) && isRed(node.right)) {
    
    
        node = flipColor(node); //颜色反转
    }
    
    return node; //返回当前节点
}

private boolean isRed(TreeNode node) {
    
    
    if (node == null) {
    
    
        return BLACK; //null节点,是黑色
    }
    return node.color == RED; //判断是不是红色
}

可能有同学会疑惑,为什么没有将情况三的代码写进去??

试想一下,当我出现情况三的时候,是在递归函数里面实现的左旋转,然后当前函数结束后,返回到上一个递归函数,实现右旋转。所以这里我们就并不需要进行额外的情况三的代码。

各种查询时间复杂度分析:

好啦,红黑树的插入操作,我们就讲到这里,删除操作,这里我们就不细讲了。删除操作更麻烦一点,有兴趣的同学可以看看《算法4》或者《算法导论》等等书籍。

好啦,各位同学,下期见!!!

Guess you like

Origin blog.csdn.net/x0919/article/details/119935072