红黑树(R-B Tree)

Mr.LR2022年7月29日
大约 9 分钟

红黑树(R-B Tree)

R-B Tree,全称是Red-Black Tree,又称为“红黑树”,它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。

R-B Tree简介

红黑树的特性

  • 每个节点是黑色或者红色。
  • 根节点是黑色。
  • 每个叶子节点都是黑色(指向空的叶子节点)。
  • 如果一个叶子节点是红色,那么其子节点必须都是黑色的。
  • 从一个节点到该节点的子孙节点所有路径上包含相同数目的黑节点。

2-3树

红黑树与2-3树具有等价性,我们在了解红黑树前先了解2-3树对我们理解红黑树是有帮助的,同时,对于理解B树也是有帮助的(用于磁盘存储,文件系统或数据库存储)

2-3树特性

满足二分搜索树的基本性质

是一颗绝对平衡的树

节点可以存放一个或者两个元素

image-20220729104629477

上图是一个2-3树,满足二分搜索树的基本性质,比如根节点42的左子树全部比根节点小;42的左孩子是一个3节点,具有两个元素 [17 33] ,并且左孩子都小于17,右孩子大于33,中孩子大于17且小于33;根节点的右孩子是一个二节点,只有一个元素 50,并且有两个孩子。

2-3数新增元素

2-3树是一个绝对平衡的树,那么它在新增元素时是如何维护平衡性的?

下面以各种添加案例来说明

image-20220729105617484

2-3树添加节点,永远不会添加到一个空的位置 因此37添加到42左边 融合为三节点

image-20220729110134219

先融合为四节点,但是2-3树不能存在4节点,所以此时的根节点可以分裂成一棵子树,有三个2-节点组成的绝对平衡树

image-20220729110601174

同理18添加到12的右边即可

image-20220729111242788

先添加到12的左侧,然后对四节点拆解,但是此时2-3树就不是绝对平衡的树,因此让12和父亲节点融合

image-20220729111605594

同理放到6的右侧

image-20220729112216376

添加的5节点为3节点,并且父亲节点也是三节点,会进行两次拆解

红黑树与2-3树的等价性

如何等价的?

对于红黑树,它只能存在一个元素,所以2-3树中2节点和3节点在红黑树中对应关系

image-20220729113909029

也就是11节点和12节点在原2-3树中,是在同一层

image-20220729113755206

初始化node节点

private static final boolean Red = true;
private static final boolean Black = false;

class Node{
    private Node left; //左孩子
    private Node right; //右孩子
    private E e; //值
    public boolean color;

    public Node(E e){
        left = null;
        right = null;
        this.e = e;
        color = Red;//新增节点默认是红色
    }
}

判断一个节点是否是红色

如果一个节点是空,定义为黑节点,因为红色节点的含义,就是表示和父亲节点组合成 2-3节点的3节点,因此空节点,谈不上融合3节点,因此这里为黑节点

private boolean isRed(Node node){
    if (node==null){
        return Black;//
    }
    return node.color;
}

再看红黑树5个性质

  1. 每个节点是黑色或者红色。
  2. 根节点是黑色。
  3. 每个叶子节点都是黑色(指向空的叶子节点)。
  4. 如果一个叶子节点是红色,那么其子节点必须都是黑色的。
  5. 从一个节点到该节点的子孙节点所有路径上包含相同数目的黑节点。

1、第一点很好理解

2、对比2-3树,红色节点本身的意义就是和父亲节点在同一层,因此根节点一点不可能是红色,因为其没有父亲节点,只能是黑色。

3、第三条我们要看清楚是指的最后的空节点,这条与其说是性质,不如说是定义,我们再红黑树定义空为黑色。

4、红色节点下面有两个节点,如果子节点是2-节点的话,那肯定就是黑色,如果是3-节点,那首先连接的也是黑色,然后黑色的左节点才是红色(而对于黑色节点,它的右孩子肯定是黑色,左孩子就不一定了)

5、这条是核心,我们看下下面的图就一清二楚了

image-20220729141549539

由于红黑树的最大高度为2logn,它会比AVL树的高度高,所以我们在红黑树上对元素的寻找会比AVL慢一点,虽然都是O(logn)级别,但是为什么红黑树这么重要,因为对于红黑树来说,添加与删除元素这两个操作相比于AVL来说更快一些,如果我们存储的数据结构要经常发生添加和删除的变动的话,相应的使用红黑树会更好些。而数据不怎么变动,查询更多的话,AVL还是快一点点。

红黑树添加元素

最终的根节点是黑色

public void add(E e) {
    root = add(root, e);
    root.color = Black;//最终的根节点是黑色
}

左旋转

image-20220729142044064

插入红色节点 37,即如果往一个黑色元素左侧添加时,那么直接添加一个红色的就好了

image-20220729142504961

但是如果加入的元素在黑色元素的右侧,则需要进行左旋转

image-20220729143329963

左旋转和AVL树的规则相同

image-20220729143704250

在左旋转的基础上,还需要做维护颜色的操作

注:x.color node.color 可能导致x的节点也是红色,37节点也是红色,但是这里本身是一个递归子过程,x还会传回去,还会有后续处理 即旋转并不维持红黑树的性质,仅维护37和42是一个正确的三节点,即37 需要是47的左子树

代码

//    node                     x
//   /   \                   /  \
// T1     x   --------->   node  T3
//       / \               /  \
//      T2  T3            T1  T2
private Node LeftRotate(Node node){//以node为根的左旋转,返回旋转后新的根节点
    Node x = node.right;
    //左旋转
    node.right=x.left;
    x.left  = node;
    //维护颜色
    x.color = node.color;
    node.color = Red;
    return x;
}

颜色反转和右旋转

颜色反转

image-20220729144608144

对照2-3树的添加操作,这一步就很好理解了,即先添加为4节点,然后再拆解,而这个拆解过程,对应红黑树中为颜色反转

//颜色反转
private void flipColors(Node node){
    node.color = Red;
    node.left.color = Black;
    node.right.color = Black;
}

右旋转

image-20220729145205450

这种情况,需要先对42进行右旋转,再进行颜色反转,步骤请看下图

image-20220729145834102

完成右旋转和颜色维护

image-20220729150123340

完成颜色反转

private Node rightRotate(Node node){//右旋转
    Node x = node.left;
    //右旋转
    node.left = x.right;
    x.right = node;
    //维护颜色
    x.color = node.color;
    node.color = Red;
    return x;
}

第三种情况

image-20220729150930381

即添加的节点在37的右侧,则先对37进行左旋转,之后就又和之前的情况一样了。

添加代码

然后我们以这个最复杂的案例演示

image-20220729151426775

image-20220729151452926

发现在解决最复杂的情况时,都能包含之前解决的情况,因此我们只需要通过if判断,从最复杂逻辑开始处理即可。左旋转->右旋转->颜色翻转

public void add(E e) {
    root = add(root, e);
    root.color = Black;//最终的根节点是黑色
}

//以node为根的红黑树中插入元素e,递归算法
//返回插入新节点后红黑树的根
public Node add(Node node, E e) {
    if (node == null) {
        size++;
        return new Node(e);
    }
    if (e.compareTo(node.e) < 0) {
        node.left = add(node.left, e);
    }
    if (e.compareTo(node.e) > 0) {
        node.right = add(node.right, e);
    }else {//修改当前的值
        node.e = e;
    }
    //维护红黑树的性质
    if(isRed(node.right)&&!isRed(node.left)){//右孩子红色,左孩子不是红色
         node = LeftRotate(node);
    }
    if(isRed(node.left)&&isRed(node.left.left)){//左孩子红色,左孩子的左孩子也是红色
        node = rightRotate(node);
    }
    if(isRed(node.left)&&isRed(node.right)){//左右都是红色
        flipColors(node);//颜色翻转
    }
    return node;
}

总结

  • 对于完全随机的数据,普通的二分搜索树很好用!
    • 缺点:极端情况退化成链表(或者高度不平衡)
  • 对于查询较多的使用情况,AVL树很好用!
    • 红黑树牺牲了平衡性(21ogn的高度)
    • 统计性能更优(综合增删改查所有的操作)

代码

public class RedBlackTree<E extends Comparable<E>>{

    private static final boolean Red = true;
    private static final boolean Black = false;

    class Node{
        private Node left; //左孩子
        private Node right; //右孩子
        private E e; //值
        public boolean color;

        public Node(E e){
            left = null;
            right = null;
            this.e = e;
            color = Red;//新增节点默认是红色
        }
    }

    private Node root;
    private int size;

    public int size(){
        return size;
    }

    //判断一个节点是否是红色
    private boolean isRed(Node node){
        if (node==null){
            //如果一个节点是空,定义为黑节点,
            //因为红色节点的含义,就是表示和父亲节点组合成 2-3节点的3节点
            //因此空节点,谈不上融合3节点,因此这里为黑节点
            return Black;
        }
        return node.color;
    }

    public void add(E e) {
        root = add(root, e);
        root.color = Black;//最终的根节点是黑色
    }

    //以node为根的红黑树中插入元素e,递归算法
    //返回插入新节点后红黑树的根
    public Node add(Node node, E e) {
        if (node == null) {
            size++;
            return new Node(e);
        }
        if (e.compareTo(node.e) < 0) {
            node.left = add(node.left, e);
        }
        if (e.compareTo(node.e) > 0) {
            node.right = add(node.right, e);
        }else {//修改当前的值
            node.e = e;
        }
        //维护红黑树的性质
        if(isRed(node.right)&&!isRed(node.left)){//右孩子红色,左孩子不是红色
             node = LeftRotate(node);
        }
        if(isRed(node.left)&&isRed(node.left.left)){//左孩子红色,左孩子的左孩子也是红色
            node = rightRotate(node);
        }
        if(isRed(node.left)&&isRed(node.right)){//左右都是红色
            flipColors(node);//颜色翻转
        }
        return node;
    }

    //    node                     x
    //   /   \                   /  \
    // T1     x   --------->   node  T3
    //       / \               /  \
    //      T2  T3            T1  T2
    private Node LeftRotate(Node node){//以node为根的左旋转,返回旋转后新的根节点
        Node x = node.right;
        //左旋转
        node.right=x.left;
        x.left  = node;
        //维护颜色
        x.color = node.color;
        node.color = Red;
        return x;
    }

    private Node rightRotate(Node node){//右旋转
        Node x = node.left;
        //右旋转
        node.left = x.right;
        x.right = node;
        //维护颜色
        x.color = node.color;
        node.color = Red;
        return x;
    }

    //颜色反转
    private void flipColors(Node node){
        node.color = Red;
        node.left.color = Black;
        node.right.color = Black;
    }
    @Override
    public String toString(){
        StringBuilder res = new StringBuilder();
        generateBSTString(root, 0, res);
        return res.toString();
    }

    
    private void generateBSTString(Node node, int depth, StringBuilder res){

        if(node == null){
            res.append(generateDepthString(depth) + "null\n");
            return;
        }

        res.append(generateDepthString(depth) + node.e +"\n");
        generateBSTString(node.left, depth + 1, res);
        generateBSTString(node.right, depth + 1, res);
    }

    private String generateDepthString(int depth){
        StringBuilder res = new StringBuilder();
        for(int i = 0 ; i < depth ; i ++)
            res.append("--");
        return res.toString();
    }
}

参考

上次编辑于: 2022/8/3 17:58:36
贡献者: liurui-60837