高阶数据结构之红黑树

红黑树

        在上一章中我们介绍了AVL树,实际上,前面总结的AVL树是为本章的红黑树做铺垫(因为红黑树中也是涉及到旋转的),除此之外,红黑树上的每个节点也都是带上颜色的(顾名思义:红色&黑色)。此处先简单说说红黑树,在最后总结部分再对AVL树与红黑树两者做一个比较。

        红黑树也是一种二叉搜索树,但特殊的是在每一个节点上都增加了一个存储位来表示节点的颜色,这个颜色可以是红色也可以是黑色。红黑树通过对任何一条从根到叶子的路径上各个节点的颜色进行一个限制,限制后的红黑树会确保没有一条路径会比其他路径长出两倍,因此也是接近平衡的。

红黑树的性质

红黑树的五点性质:

  1. 每个节点的颜色不是红色就是黑色
  2. 根节点是黑色的
  3. 不存在2个连续的红色节点。如果一个节点是红色的,那么它的两个孩子节点一定都是黑色的
  4. 对于每一个节点,从该节点到其所有后代节点的路径上,都是包含相同数目的黑色节点
  5. 每个叶子节点都是黑色的(这里的叶子节点指的是最后的空节点)

例如下面的这张图就是一颗红黑树:
在这里插入图片描述
        通过上面的这5点限制,我们就可以保证没有一条路径会比其他路径长出两倍,原因是当一条路径上的节点全是黑色的时候就是最短路径,而当一条路径要达到黑色节点个数相同并且路径最长的时候,那么这条路径就必然是红黑交替的,既然是红黑交替,那么这条最长的路径就干好事最短路径的两倍。

红黑树的定义

class RBTreeNode{
    
    
    public RBTreeNode left;
    public RBTreeNode right;
    public RBTreeNode parent;
    public int val;
    public COLOR color;

    public RBTreeNode(int val){
    
    
        this.val = val;
        //新建的节点默认是红色的
        this.color = COLOR.RED;
    }
}

        注意:在定义新节点的时候需要把节点的color值默认定义成RED的,原因是如果直接定义成黑色节点的话,新的红黑树是不满足红黑树的性质4的(对于每一个节点,从该节点到其所有后代节点的路径上,都是包含相同数目的黑色节点)。

红黑树的插入

        红黑树也是在二叉搜索树的基础上一些平衡的限制条件。具体可以分为两步,第一步是按照二叉搜索树的规则将新节点插入到树中;第二步是检查新插入的节点是否破坏了红黑树的性质(一定是要遵循上面红黑树的五点性质的),其中在这一步会出现很多种不同的情况需要分别来进行解决。

情况一:插入节点的父节点为红,祖父节点为黑,叔叔节点存在且为红

在这里插入图片描述
此时又可以分成三种情况:
第一种:此图的g节点就是根节点。 那么只需要将p节点和u节点颜色修改成黑色即可。
在这里插入图片描述
第二种:此图的g节点存在一个父节点且为黑色。 那么此时就不能单单将p和u节点修改成黑色,因为这样会导致每条路径上的黑色节点个数不同,此时我们可以将g节点修改成红色就可以很好的解决这个问题。(拓展:由上面的两种情况我们也可以归纳出我们每次将p和u节点置为黑,将g节点置为红,最后再将树的根节点置为黑,就可以将上面这两种情况合并起来)。
在这里插入图片描述
第三种:此图的g节点存在一个父节点且为红色。 如果出现这种情况,说明这个父节点就一定不是这颗树的根节点(只是这棵树的一个子树),对此,我们依旧可以按照上面的规则进行修改节点颜色,当g节点被改成红色,并且其父节点也是红色(是不符合红黑树性质的),此时再继续往上遍历对这颗红黑树进行修改……直到符合红黑树的性质。
在这里插入图片描述

(注意:以下代码均包含镜像情况)

while(parent != null && parent.color == COLOR.RED){
    
    
    RBTreeNode grandFather = parent.parent;  //grandFather不可能是空,因为红色节点必然有父节点
    if(parent == grandFather.left){
    
    
        RBTreeNode uncle = grandFather.right;
        if(uncle != null && uncle.color == COLOR.RED){
    
    
            parent.color = COLOR.BLACK;
            uncle.color = COLOR.BLACK;
            grandFather.color = COLOR.RED;
            //继续向上修改
            cur = grandFather;
            parent = cur.parent;
        }else{
    
    
            //uncle不存在或者uncle的颜色为黑

        }
    }else{
    
    
        //parent == grandFather.right
        RBTreeNode uncle = grandFather.left;
        if(uncle != null && uncle.color == COLOR.RED) {
    
    
            parent.color = COLOR.BLACK;
            uncle.color = COLOR.BLACK;
            grandFather.color = COLOR.RED;
            //继续向上修改
            cur = grandFather;
            parent = cur.parent;
        }else{
    
    
            //uncle不存在或者uncle的颜色为黑
            
        }
    }
}

情况二:当前节点的父节点为红,祖父节点为黑,叔叔节点不存在或者为黑

        在情况二中又会分出来两种情况:一种是当前节点在父节点的左边,一种是当前节点在父节点的右边。如果出现情况二的话,那么说明这一步操作一定不是在插入新的节点时候的,而是在向上调整的过程中出现的,原因是这种情况在还没插入节点之前本身就是不符合红黑树性质的。

当前节点在父节点的左边:
在这里插入图片描述
(注意:cur是在其子树中向上调整后变成红色的,并非插入的新节点。)
        这时候如果只是进行简单的修改颜色,是无法保证cur子树路径黑色节点个数与其他路径上的黑色节点个数一样的,这时候就需要对这棵树进行右单旋+修改颜色(p置为黑、g置为红)。
在这里插入图片描述

当前节点在父节点的右边:
在这里插入图片描述
(注意:cur是在其子树中向上调整后变成红色的,并非插入的新节点。)
        这种情况就变得更加特殊了,既不能只是进行简单的修改颜色,也不能像上面一样右旋解决,这时候如果对这棵树的p节点进行一下左单旋。
在这里插入图片描述
        在进行左单旋之后,可以惊奇地发现,这不就是“当前节点在父节点的左边”的情况吗?唯一的区别就是cur节点与p节点的顺序互换了,当然cur子树部分(未画出来)位置也是更换了的,但是这并不会产生影响(因为也都会是满足红黑树的性质的)。那么在下一步操作的时候,我们就可以交换一个这两个节点的指向并直接使用上一情况的代码即可。

插入操作完整代码:

while(parent != null && parent.color == COLOR.RED){
    
    
    RBTreeNode grandFather = parent.parent;  //grandFather不可能是空,因为红色节点必然有父节点
    if(parent == grandFather.left){
    
    
        RBTreeNode uncle = grandFather.right;
        if(uncle != null && uncle.color == COLOR.RED){
    
    
            parent.color = COLOR.BLACK;
            uncle.color = COLOR.BLACK;
            grandFather.color = COLOR.RED;
            //继续向上修改
            cur = grandFather;
            parent = cur.parent;
        }else{
    
    
            //uncle不存在或者uncle的颜色为黑
            //如果当前节点在父节点的右边,则先进行左单旋
            if(cur == parent.right){
    
    
                rotateLeft(parent);
                //交换cur和parent
                RBTreeNode tmp = parent;
                parent = cur;
                cur = tmp;
            }
            //不管当前节点在父节点的左边还是右边,都是需要进行右单旋并修改颜色
            rotateRight(grandFather);
            parent.color = COLOR.BLACK;
            grandFather.color = COLOR.RED;
        }
    }else{
    
    
        //parent == grandFather.right
        RBTreeNode uncle = grandFather.left;
        if(uncle != null && uncle.color == COLOR.RED) {
    
    
            parent.color = COLOR.BLACK;
            uncle.color = COLOR.BLACK;
            grandFather.color = COLOR.RED;
            //继续向上修改
            cur = grandFather;
            parent = cur.parent;
        }else{
    
    
            //uncle不存在或者uncle的颜色为黑
            //如果当前节点在父节点的右边,则先进行左单旋
            if(cur == parent.left){
    
    
                rotateRight(parent);
                //交换cur和parent
                RBTreeNode tmp = parent;
                parent = cur;
                cur = tmp;
            }
            //不管当前节点在父节点的左边还是右边,都是需要进行右单旋并修改颜色
            rotateLeft(grandFather);
            parent.color = COLOR.BLACK;
            grandFather.color = COLOR.RED;
        }
    }
}

红黑树的验证

        我们前面说过,红黑树其实就是一颗二叉搜索树,只是通过一些规则来限制树的高度尽量平衡,所以我们在对红黑树进行验证的时候,可以分为两步来进行验证:验证这棵树是否是二叉搜索树 & 验证这棵树是否遵循红黑树的性质。

验证是否是二叉搜索树

        我们在验证一棵树是否是二叉搜索树的时候,一般都是使用中序遍历来查看遍历的结果是否是有序的。

public void inorder(RBTreeNode root){
    
    
    if(root == null) return;
    inorder(root.left);
    System.out.println(root.val + " ");
    inorder(root.right);
}

验证是否遵循红黑树的性质

        在这一步验证中,我们需要判断的内容是:1.根节点是否是黑色;2.是否存在两个连续的红色节点(判断每个红色节点的父节点颜色即可);3.每条路径上的黑色节点数量是否相同(使用DFS搜索每条路径上的黑色节点是否符合预期即可)。

public boolean isValidRBTree(){
    
    
    if(root == null){
    
    
        return true;  //空树也是红黑树
    }
    if(root.color != COLOR.BLACK){
    
    
        System.out.println("根节点必须是黑色的!");
        return false;
    }
    int blackNum = getBlackNum(root);
    return checkRedColor(root) && checkBlackNum(root, 0, blackNum);
}

//获取其中一条路径上的黑色节点个数
private int getBlackNum(RBTreeNode root) {
    
    
    int num = 0;
    while(root != null){
    
    
        if(root.color == COLOR.BLACK){
    
    
            num++;
        }
        root = root.left;
    }
    return num;
}

//判断每条路径上的黑色节点数量是否相同
private boolean checkBlackNum(RBTreeNode root, int pathBlackNum, int blackNum) {
    
    
    if(root == null){
    
    
        return true;
    }
    if(root.color == COLOR.BLACK){
    
    
        pathBlackNum++;
    }
    if(root.left == null && root.right == null){
    
    
        if(pathBlackNum != blackNum){
    
    
            System.out.println("每条路径上的黑色节点数量不相同");
            return false;
        }
    }
    return checkBlackNum(root.left, pathBlackNum, blackNum) && checkBlackNum(root.right, pathBlackNum, blackNum);
}

//判断是否出现两个连续的红色节点
private boolean checkRedColor(RBTreeNode root) {
    
    
    if(root == null){
    
    
        return true;
    }
    if(root.color == COLOR.RED){
    
    
        RBTreeNode parent = root.parent;
        if(parent.color == COLOR.RED){
    
    
            System.out.println("连续出现两个红色节点!");
            return false;
        }
    }
    return checkRedColor(root.left) && checkRedColor(root.right);
}

总结红黑树

AVL树和红黑树的比较

        AVL树和红黑树都是高效的平衡二叉树,增删改查的时间复杂度都是O(logn)。
        除此之外,红黑树是不追求绝对平衡的,它只需要保证最长路径不超过最短路径的两倍即可(查找的效率虽没有AVL树那么高,但也只是系数的区别,时间复杂度也还是一样的),相对于AVL树而言,降低了插入和旋转的次数,因此在进行插入节点和删除节点的效率是优于AVL树的,而且最重要的一点是,红黑树不会像AVL树那样涉及到大量的旋转代码(左右双旋、右左双旋),实现起来是会比较简单一点的,以及在实际使用的过程中对树插入节点的操作大多数情况下也还是比较多的,所以实际运用红黑树会更多。

红黑树的应用

  • 在Java中TreeMap、TreeSet底层使用的就是红黑树
  • 在C++中map、set、mutil_map、mutil_set底层使用的也是红黑树
  • 在Linux内核中进程调度中使用红黑树管理进程控制块、epoll在内核中实现时使用红黑树管理事件块
  • ……

        最后附上红黑树的完整代码,有需要可以访问此gitee账户(链接)进行参考:红黑树代码

猜你喜欢

转载自blog.csdn.net/Faith_cxz/article/details/128804202