《算法4》红黑树原理实现---笔记

前面讲了AVL平衡树的实现https://blog.csdn.net/weixin_43696529/article/details/104701374,但由于AVL是高度平衡的树(高度差小于等于1),而红黑树是根据颜色来不严格的实现平衡,因此在插入和删除节点时,红黑树的调整次数较少,尤其是在大量数据面前时,红黑树的效率会更高。

一、定义介绍

首先我们需要知道什么是2-节点,什么是3-节点。
2-节点就是我们常说的二叉节点,一个节点两个链接
3-节点则是两个节点三个链接

红黑树中,树的链接有两种类型:红链接和黑链接
一个红链接将两个2-节点连接起来,形成一个3-节点
黑链接就是一个普通连接,如下:
在这里插入图片描述
上面的2-3树对应下面的红黑树

定义如下:
1.红链接都是左链接,不允许为右链接(仅因减小代码量)
2.没有任何一个节点同时和两条红链接相连
3.该树是完美黑色平衡的,即任意空连接到根节点的路径上的黑链接数量相同。

二、数据结构

如下:
每个节点增加了一个布尔类型color变量,表示指向该节点的链接的颜色,color的取值为RED(true)BLACK(false)

public class RedBlackTree<Key extends Comparable<Key>,Value> {
 
    private static final boolean RED=true;
    private static final boolean BLACK= false;

    private Node root;

    private class Node{
    //	左右孩子
        private Node left,right;
        /**
         * 其父节点指向该节点的链接颜色
         */
        private boolean color;
        //键
        private Key key;
        //键关联的值
        private Value value;
        /**
         * 子树中的节点总数
         */
        private int size;

        public Node( boolean color,Key key,Value value, int size) {
            this.key=key;
            this.color = color;
            this.value = value;
            this.size = size;
        }

    }
}    

三、旋转操作

在对红黑树进行操作时,难免会出现连续的两个红链接相连,红链接可能是连续两个左链接,也可能是一个右链接,因此我们需要旋转链接进行修复。
红黑树的旋转一共有两个情况:
1.对一个右红链接左旋转
如下图,对右链接为红色的节点f进行左旋转,变为下面一个图:
代码如下:

在这里插入图片描述
代码如下:

 /**
     * 左旋转(右链接为红色)
     * @param node
     * @return
     */
    private Node rotateLeft(Node node){
		//这里旋转逻辑同AVL
        Node right=node.right;		
        node.right=right.left;
        right.left=node;
        //但是需要修改两个节点的颜色
        //将右节点的颜色改为node的颜色
        right.color=node.color;
        //将node 的颜色改为红色,因为是把红链接旋转过来,这里必然是红色
        node.color=RED;

        right.size=node.size;
        node.size=1+size(node.left)+size(node.right);
        return right;
    }

2.对一个左红链接右旋转
如下图,对左链接为红色的节点b进行右旋转,变为下面一个图:
在这里插入图片描述
代码如下:
逻辑和上面一样,相反而已

   /**
     * 右旋转(左链接为红色)
     * @param node
     * @return
     */
    private Node rotateRight(Node node){

        Node left=node.left;
        node.left=left.right;
        left.right=node;

        left.color=node.color;
        node.color=RED;

        left.size=node.size;
        node.size=1+size(node.left)+size(node.right);
        return left;
    }

四、插入操作

我们在插入一个节点的时候,默认让其颜色为红色,这样就会出现以下几种情况:

当插入到一个2-节点时:
此时有两种情况:

  1. 指向新节点的链接是父节点的左链接:
    此时父链接直接成了一个3-节点,如下图,插入节点a,此时a与父节点b成为一个3-节点
    在这里插入图片描述
  2. 指向新节点的链接是父节点的右链接:
    此时是一个错误的3-节点,因为我们规定红链接必须的左链接,因此我们需要对其进行左旋转:
    如下图新插入了一个节点c,指向它的链接为父链接节点b的右链接在这里插入图片描述
    对齐进行左旋转修正,如下图:
    在这里插入图片描述
    当插入到一个3-节点时:
    1.插入的是3-节点的右链接
    在这里插入图片描述
    此时需要将两个链接都变为黑色:
    这个过程叫做颜色转换:
  private void flipColors(Node node){
        node.color = !node.color;
        node.left.color = !node.left.color;
        node.right.color = !node.right.color;
    }

书上给的是让node变为红色,左右孩子变为黑色,但因为后面还要用到这个方法,并且是转换为与原来颜色对应的颜色,所以这里直接写成这样,同样适用。
在我们每次插入后都会让根节点变为黑色,而如果根节点从红变为黑是,就意味着其高度加1

在这里插入图片描述
2.插入的是3-节点的中链接

在这里插入图片描述
此时需要先对节点a进行左旋转变成右边的图,然后再对节点c进行右旋转,如下:
在这里插入图片描述
此时变成了第一种情况,按第一种情况继续处理即可

3.插入的是3-节点的左链接
在这里插入图片描述
如上图,插入节点a,此时变成了第二种情况左旋转后的状态,按上面的步骤处理即可。

综合可以得出插入算法的步骤:
1.如果插入的是2-节点,只需按2-节点的两种情况操作即可
2.如果插入的是3-节点,则临时创建了一个4-节点,我们需要将其分解并将红链接向上传递,直到遇到一个2-节点或根节点。观察上面的几种方法,都是为了完成这个目标。

而在代码中的体现就是:
1.如果右孩子是红色,左孩子为黑色,则左旋转
2.如果左孩子和左孩子的左孩子都是红色, 则进行右旋转
3.如果左孩子和右孩子都是红色,则进行颜色转换

观察上述3点,其可以涵盖我们上面的任意情况。

插入算法如下:

 public void put(Key key,Value value){
        if (key == null) throw new IllegalArgumentException("key为空");
        if(value==null){
            delete(key);
            return;
        }

        root=put(root,key,value);
        //插入后将根节点颜色变为黑色
        root.color=BLACK;
    }
    private Node put(Node node,Key key,Value value) {
        if (node==null){
            //插入的节点,总是让它的颜色初始化为红
            return new Node(RED,key,value,1);
        }
        int result=key.compareTo(node.key);
        if (result<0){
            node.left=put(node.left,key,value);
        }else if (result>0){
            node.right=put(node.right,key,value);
        }else {
            node.value=value;
        }

        
        /*
            右链接为红色 
            1.即向2-节点右边插入
            2 或是 向3-节点中间插入
         */
        if (isRed(node.right)&&!isRed(node.left)){
            //左旋转
            node=rotateLeft(node);
        }
        //3.指向新节点的链接为3-节点的左链接
        //4.或是情况2左旋转后的状态
        if (isRed(node.left)&&isRed(node.left.left)){
            //右旋转
            node=rotateRight(node);
        }
        //5.左右链接均为红,即成为一个4-节点(3、4 右旋转后的状态)
        if (isRed(node.left)&&isRed(node.right)){
            //转换颜色
            flipColors(node);
        }
        node.size=size(node.left)+size(node.right)+1;
        return node;
    }
     private int size(Node node) {
        return node==null?0:node.size;
    }

判断当前节点是否被红链接指向:

五、删除最小值

删除操作比较麻烦,也是看的很久才看懂(hhhh)
如果待删除的节点是一个3-节点,那么直接删除就好了,但是如果是一个2-节点,删除后便后印象树的结构,因此删除最小值的思路如下:
从根节点开始向下寻找最小值,路径上的每一个节点都需要满足以下条件之一:
1.当前节点的左孩子是3-节点,过
2.当前节点的左孩子是2-节点,但是兄弟节点是3-节点,此时可以向兄弟节点借一个过来,保证自己不是2-节点;
3.当前节点左孩子右孩子都是2-节点,则向父节点借一个,并将借的节点和左孩子右孩子合并。

按如上操作遍历到最小值处,此时最小值就在一个3-节点或者4-节点中,删除接即可。然后就可以自底向上修复临时的4-节点(同前面的步骤)。

public void  delMin(){
        if (isEmpty()){
            throw new NoSuchElementException("树为空");
        }
        if(!isRed(root.left) && !isRed(root.right)){
            root.color = RED;   // 如果根节点的左右子节点是2-节点,我们要先根设为红的,这样才能进行后面的moveRedLeft操作,因为左孩子要从根节点借一个
        }
        root = delMin(root);
        root.color = BLACK;  // 借完以后,我们将根节点的颜色复原
    }
     private Node delMin(Node node) {
        if (node.left==null){
            return null;
        }
        if (!isRed(node.left) && !isRed(node.left.left)){
        // node的左节点如果是2-节点,则按上面的方法编程3-节点或是临时4-节点
            node=moveRedLeft(node);
        }
        node.left=delMin(node.left);
        return balance(node); //   平衡临时组成的4-节点
    }

以下是2-节点变3-节点或4-节点的方法:

 private Node moveRedLeft(Node node) {
        /**
         * 因为我们规定红链接只能在左,
         * 因此当前节点的左右子节点都是2-节点,这时候我们就需要通过颜色转换,将这三个节点合并在一起
         */
        flipColors(node);
        //如果兄弟节点为2-节点的话,那么到上一步就结束了

        if(isRed(node.right.left)){     // 而如果兄弟节点不是2-节点的话,我们就需要通过旋转从兄弟节点借一个过来
            node.right = rotateRight(node.right);
            node = rotateLeft(node);
            //  因为条件2要求我们只向兄弟节点借一个,
            //  而一开始从父节点那里借了一个,因此需要还一个给父节点
            flipColors(node);
        }
        return node;

    }

删除完毕后,我们需要自顶向上分解临时的4-节点:
以下代码和上面的put最后几个if相同,只是在开始添加了一个条件,但是这个条件去掉后也不影响,因为
如果第一个if左旋转后,第二个if必然不会再走,因为左孩子必然是红色;
如果去掉第一个if,如果左黑右红,则进行左旋转,如果左红右红,那就属于最后一个情况,转换颜色即可,不影响结果

private Node balance(Node node){
        if (isRed(node.right)) {

            node = rotateLeft(node);
        }
        if (isRed(node.right) && !isRed(node.left)) {
            node=rotateLeft(node);
        }
        if (isRed(node.left) && isRed(node.left.left)) {
            node=rotateRight(node);
        }
        if (isRed(node.left) && isRed(node.right))  {
            flipColors(node);
        }
        node.size = size(node.left)+size(node.right)+1;
        return node;
    }

六、删除最大值

逻辑同删除最小值相同,方向相反。

  public void delMax(){
        if (isEmpty()){
            throw new NoSuchElementException("树为空");
        }
        if (!isRed(root.left) && !isRed(root.right)){
            root.color=RED;
        }
        root=delMax(root);
        root.color=BLACK;
    }

在删除中需要添加一个左孩子是否为红链接的判断,
因为最大值要么不存在子孩子,要么最多存在一个左链接(从以上几个平衡旋转可以发现,最大值节点一定是这两个情况)
如果有左红链接,则应该将此节点右旋转,让最大值没有一个孩子,这样就可以直接删除,否则会破坏树的结构,丢失该左孩子

 private Node delMax(Node node) {
        if(isRed(node.left)){
         
            node = rotateRight(node);
        }
        if (node.right==null){
            return null;
        }
        if (!isRed(node.right) && !isRed(node.right.left)){
            node=moveRedRight(node);
        }
        node.right=delMax(node.right);
        return balance(node);
    }

七、删除任意key

删除操作则就是将删除最小值和删除最大值组合在一起,但需要考虑key不在树底的情况,此时按二叉查找树的删除逻辑删除即可,具体如下:

public void delete(Key key) {
        if (key == null) throw new IllegalArgumentException("argument to delete() is null");
        if (!contains(key)) return;

        if (!isRed(root.left) && !isRed(root.right))
            root.color = RED;

        root = delete(root, key);
        if (!isEmpty()) root.color = BLACK;
    }
    private Node delete(Node node, Key key) {

        if (key.compareTo(node.key) < 0)  {
            //key在左子树,按删除最小值一样删除
            if (!isRed(node.left) && !isRed(node.left.left))
                node = moveRedLeft(node);
            node.left = delete(node.left, key);
        }
        else {//key在右子树,按删除最大值一样删除
            if (isRed(node.left))
                node = rotateRight(node);
            //需首先判断待删除的节点是否在树底,否则下一步的node.right.left会出现空指针
            if (key.compareTo(node.key) == 0 && (node.right == null))
                return null;
            //递归在右子树中删除
            if (!isRed(node.right) && !isRed(node.right.left))
                node = moveRedRight(node);
            //如果待删除的节点不在树底
            if (key.compareTo(node.key) == 0) {
                //像二叉查找树一样删除,从node右子树找到最小的节点放在当前位置,然后再将该最小节点从右子树删除
                Node x = min(node.right);
                node.key = x.key;
                node.value = x.value;
              
                node.right = delMin(node.right);
            }
            else {
                //key仍然大于该node,继续在右子树递归
                node.right = delete(node.right, key);
            }
        }
        return balance(node);
    }

获取给定键的值,同前面二叉查找树相同。

public Value get(Key key){
        if (key==null){
            return null;
        }
        return get(root,key);
    }
     private Value get(Node node, Key key) {
        while (node!=null){
            int result=key.compareTo(node.key);
            if (result<0){
                node=node.left;
            }else if (result>0){
                node=node.right;
            } else {
                return node.value;
            }
        }
        return null;
    }

总结:

1.一颗大小为N的红黑树的高度不会超过2lgN
2.红黑树最坏情况下的运行时间的增长数量级:
查找:2lgN
删除:2lgN
平均情况:
查找:lgN
插入:lgN
3. AVL树追求完美平衡,读取的性能略高;但维护较慢,空间开销较大。
红黑树的读取性能略低(但最多也就多比较一次左右),空间复杂度同AVL差不多,但数据量大时,红黑树的综合性能更高
4.因此在查找大于插入或删除的场景时,使用AVL树,如果次数都差不多,就用红黑树。**

发布了75 篇原创文章 · 获赞 13 · 访问量 8369

猜你喜欢

转载自blog.csdn.net/weixin_43696529/article/details/104707495