红黑树的定义和实现原理浅析以及手写一棵Java版红黑树

在学习红黑树的实现原理之前,首先要明白为什么会出现红黑树?它是怎么来的?它存在的意义是什么以及解决了什么问题?要搞懂这些问题,就不得不先谈谈二叉搜索树和AVL树了。

一、二叉搜索树

1、 二叉搜索树的概念

二叉搜索树又称二叉排序树,它要么是一棵空树,要么是具有以下性质的二叉树:

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值。
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值。
  • 它的左右子树也分别为二叉搜索树。

在这里插入图片描述

上图就是一棵标准的二叉搜索树,从上述概念以及图中可以看出,二叉搜索树具有以下特性:

  • 二叉搜索树中最左侧的节点是树中最小的节点,最右侧节点一定是树中最大的节点;
  • 采用中序遍历遍历二叉搜索树,可以得到一个有序的序列,比如上图中序遍历的结果就是:0123456789。

2、二叉搜索树查找节点的过程

在二叉搜索树查找某个节点时,我们必须从根节点开始查找。

  • ①、 如果查找值小于当前节点值,则搜索左子树。
  • ②、 如果查找值大于当前节点值,则搜索右子树;
  • ③、 如果查找值等于当前节点值,则停止搜索(终止条件)。

3、查找性能分析

进行插入和删除操作时,都必须先查找,所以查找效率在很大程度上决定了二叉搜索树中各个操作的性能。

对于有n个结点的二叉搜索树,查询一个节点的效率跟要查询的节点的深度呈正相关。即结点越深,则查询时要比较次数越多。
但是对于同一个关键码集合,如果各关键码插入的次序不同,就可能得到不同结构的二叉搜索树。此时查询效率也天差地别,如下图:
在这里插入图片描述
在上面两个二叉搜索树中,同样是查找9这个节点,第一个图只需要比较三次,而第二个图则需要比较7次。所以二叉搜索树的查询效率很依赖插入的次序:

  • 最优情况下,二叉搜索树为完全二叉树,其平均比较次数为: l o g 2 N log_2 N log2N
  • 最差情况下,二叉搜索树退化为单支树,其平均比较次数为: N 2 \frac{N}{2} 2N

4、如何避免二叉搜索树出现查询效率过低的问题?

从上面分析我们可以知道,二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年 发明了一种解决上述问题的方法:
当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(插入时需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。也就是AVL树。

二、AVL树

1、AVL树的特点

由于AVL树是从二叉搜索树发展而来的,所以:

  • ①、 具有二叉搜索树的全部特性;
  • ②、 每个节点的左子树和右子树的高度差最多等于1;
  • ③、 它的左右子树都是AVL树。

在这里插入图片描述

从上图可以看到AVL树的每个节点,它的左右子树高度差都不会超过1。

在插入或者删除时,会发生左旋、右旋操作,使这棵树再次左右保持一定的平衡。AVL树基于这些特点,就可以保证不会出现大量节点偏向于一边的情况了。

2、为什么有了AVL平衡树还需要红黑树呢?

虽然平衡树解决了二叉查找树退化为近似链表的缺点,能够把查找时间控制在 l o g 2 N log_2 N log2N。但是因为平衡树要求每个节点的左子树和右子树的高度差最多等于1,这个要求实在是太严了,导致每次进行插入/删除节点的时候,几乎都会破坏这个规则。

而一旦破坏了这个规则,我们就需要不断地通过左旋和右旋来进行调整,使之再次成为一颗符合要求的平衡树。情况最差的时候,有可能一直要让旋转持续到根的位置才可以。

显然,如果在插入、删除很频繁的场景中,AVL平衡树就需要频繁地进行调整,这就会使的AVL平衡树的性能大打折扣,为了解决这个问题,于是就有了红黑树!!!

三、红黑树

1、红黑树概念

红黑树,是一种特殊的二叉搜索树,它给每个结点上都增加一个存储位表示结点的颜色,可以是红色或黑色。并且任何一条路径都不会比其他路径长出两倍,因而可以看作是接近平衡的。

在这里插入图片描述

红黑树的性质如下:

  • ①、 每个节点要么是黑色,要么是红色;
  • ②、 红黑树的根节点一定是黑色的;
  • ③、 每个叶子结点都是黑色的(此处的叶子结点指的是最后的空结点NIL);
  • ④、 如果一个节点是红色的,则它的两个孩子结点都是黑色的。不能有两个红色节点相连;
  • ⑤、 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点。

由性质5其实还可以推出一条性质,就是:
如果某一个节点存在一个黑色子节点,那么该结点肯定有两个子节点。否则黑色节点就不平衡了。

2、红黑树的特点

从上面的性质可以得知,红黑树并不是一个完美平衡二叉查找树,因为有可能根结点的左子树比右子树高,但左子树和右子树中的黑结点的层数是相等的,即任意一个结点到到每个叶子结点的路径都包含数量相同的黑结点(性质5)。

所以我们称红黑树的这种平衡为黑色完美平衡。只要一棵树满足上面五个性质,那么这棵树就是趋近与平衡状态的。

3、红黑树的定义

红黑树节点的定义如下:

class RBTreeNode{
	RBTreeNode left = null;  //左子节点
	RBTreeNode right = null; //右子节点
	RBTreeNode parent = null; //父节点
	COLOR color = RED;  //节点的颜色
	int val;  //节点值
	
	public RBTreeNode(int val){
		this.val = val;
	}
}

思考一个问题:在节点的定义中,为什么要将节点的默认颜色给成红色的?
答:理由很简单,就是如果插入节点默认为红色,那么在父节点存在且为黑色节点时,红黑树的黑色平衡不会被破坏,也就不需要做自平衡操作。但是如果插入的结点默认是黑色,那么不管父节点是什么颜色,插入位置所在的子树的黑色结点总是多1,必须做自平衡。

4、红黑树中的操作

前面讲到红黑树能自平衡,它靠的是什么?靠的就是三种操作,分别为:左旋、右旋和变色。

  • ①、变色: 这个很好理解,就是将一个结点的颜色由红变黑或者由黑变红;

  • ②、左旋: 以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变,如下图:
    在这里插入图片描述
    看上图就很好理解左旋操作了,上图中以E为支点进行左旋操作。具体就是,让它的右子节点S成为它的父节点,并且让S的左子节点,成为E的右子节点。

  • ③、右旋: 以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变,如下图:
    在这里插入图片描述
    上图中以S为支点进行右旋操作。具体就是,让它的左子节点E成为它的父节点,并且让E的右子节点,成为S的右子节点。

5、红黑树插入情景分析

插入操作包含两部分内容:

  • ①、 查找插入的位置;
  • ②、 插入后的自平衡操作。

情景1:红黑树为空树
这是最简单的一种情景,直接把插入结点作为根结点就行。但是根据红黑树性质2:根节点是黑色。所以还需要把插入结点变为黑色。

情景2:要插入的结点的Key已存在
这种情况下只需要更新节点的值就可以了。如下图:
在这里插入图片描述

情景3:插入结点的父结点为黑结点
由于插入的结点默认是红色的,所以当插入结点的父节点为黑色时,并不会影响红黑树的平衡。此时直接插入即可,无需做自平衡操作。如下图:
在这里插入图片描述

情景4:插入节点的父节点为红色
由于红黑树的性质2:根结点必须是黑色的,所以如果插入节点的父结点为红色时,那么该父结点必不可能是根结点,所以这时插入的结点总是存在祖父结点。这一点很关键,因为后续的旋转操作肯定需要祖父结点的参与。

而叔叔节点此时就有两种情况了,叔叔节点要么为红,要么为黑或者空。
在这里插入图片描述

由于叔叔节点存在不确定性,所以在上面的基础上,还需要进行分开讨论:

插入情景4–1:叔叔结点存在并且为红结点
父节点为红色,根据红黑树的性质4可知,两个红色节点不能相连,所以祖父结点肯定为黑结点。
那么插入这个红色节点后的红黑层数的情况就是:黑红红。因为不可以同时存在两个相连的红结点,所以最简单的处理方式就是把其改为:红黑红。具体的操作步骤就是:

  • ①、 将父节点和叔叔节点改为黑色;
  • ②、 将祖父节点改为红色;
  • ③、 将祖父设置为当前节点,进行后续处理。

在这里插入图片描述

如果祖父节点的父结点是黑色,那么就无需再做任何处理了。
但如果祖父节点的父结点是红色,则违反红黑树性质了。需要将祖父节点设置为当前节点,继续做自平衡处理,直到平衡为止(这里可以使用递归操作来完成)。

插入情景4–2:叔叔结点不存在或为黑结点,并且插入结点的父亲结点是其祖父结点的左子结点
单纯从插入前情况来看,叔叔节点非红即空(NIL节点)。但是如果为黑的话,就会比当前插入节点的这条路径多一个黑色节点。这样就破坏了红黑树的性质5,造成此路径会比其它路径多一个黑色节点,所以叔叔节点肯定为NIL。

在这里插入图片描述

在4-2的基础上,插入节点的位置,是其父节点的左子节点还是右子节点,也会影响自旋操作,所以这时候又要分开讨论了。

插入情景4–2--1:新插入节点,为其父节点的左子节点
这种情况下分两步来处理:

  • ①、 变颜色,将父节点设置为黑色,将祖父节点设置为红色;
  • ②、 对祖父节点进行右旋操作。

在这里插入图片描述

插入情景4–2--2:新插入节点,为其父节点的右子节点
这依旧是在4-2的基础上来说的,即叔叔结点不存在或为黑结点,并且插入结点的父亲结点是祖父结点的左子结点。

这种情况下处理操作分为三步:

  • ①、 对父节点进行左旋;
  • ②、 将父节点设置为当前节点,得到4-2-1的情况,也就是LL红色情况;
  • ③、 按照LL红色情况处理(1.变颜色 2.右旋祖父节点)

在这里插入图片描述

插入情景4–3:叔叔结点不存在或为黑结点,并且插入结点的父亲结点是祖父结点的右子结点

该情景对应情景4.2,只是方向反转。
在这里插入图片描述

这时候依旧要分开讨论插入节点的位置,是其父节点的左子节点还是右子节点。
插入情景4–3--1:新插入节点,为其父节点的右子节点
这种情况下的处理方法为:

  • ①、 变颜色,将父节点设置为黑色,将祖父节点设置为红色;
  • ②、 对祖父节点进行左旋。
    在这里插入图片描述

插入情景4–3--2:新插入节点,为其父节点的左子节点
这依旧是在4–3的基础上来说的,即叔叔结点不存在或为黑结点,并且插入结点的父亲结点是祖父结点的右子结点。

这种情况下的处理操作分为三步:

  • ①、 对父节点进行右旋;
  • ②、 将父节点设置为当前节点,得到4–3--1的情况,也就是RR红色情况;
  • ③、 按照RR红色情况处理(1.变颜色 2.左旋祖父节点)。

在这里插入图片描述

四、手写一棵红黑树

1、代码

package dataStructure;

public class RBTree<K extends Comparable<K>,V> {
    //定义颜色常量
    private static final boolean RED = true;
    private static final boolean BLACK = false;
    //树根节点的引用
    private RBNode root;

    public RBNode getRoot() {
        return root;
    }

    //获取当前节点的父节点
    private RBNode parentOf(RBNode node) {
        if(node != null) {
            return node.parent;
        }
        return null;
    }

    //判断节点是否为红色
    private boolean isRed(RBNode node) {
        if(node != null) {
            return node.isColor() == RED;
        }
        return false;
    }

    //判断节点是否为黑色
    private boolean isBlack(RBNode node) {
        if(node != null) {
            return node.isColor() == BLACK;
        }
        return false;
    }

    //设置节点为红色
    private void setRed(RBNode node) {
        if(node != null) {
            node.setColor(RED);
        }
    }

    //设置节点为黑色
    private void setBlack(RBNode node) {
        if(node != null) {
            node.setColor(BLACK);
        }
    }

    //中序打印二叉树方法
    public void inOrderPrint() {
        if(this.root != null) {
            inOrderPrint(this.root);
        }
    }
    //封装重载,方便调用
    private void inOrderPrint(RBNode node) {
        if(node != null) {
            inOrderPrint(node.left);
            System.out.println("key -> " + node.key + ", value -> " + node.value);
            inOrderPrint(node.right);
        }
    }

    /**
     * 左旋方法
     * 左旋示意图:左旋x节点
     *    p                   p
     *    |                   |
     *    x                   y
     *   / \         ---->   / \
     *  lx  y               x   ry
     *     / \             / \
     *    ly  ry          lx  ly
     *
     * 左旋做了几件事?
     * 1.将x的右子节点设置为y的左子节点,并将y的左子节点的父节点设置为x;
     * 2.当x的父节点不为空时,将y的父节点设置为x的父节点,并将x的父节点指定为y
     * 3.//将x的父节点更新为y,将y的左子节点更新为x
     */
    private void leftRotate(RBNode x) {
        RBNode y = x.right;

        //将x的右子节点设置为y的左子节点
        x.right = y.left;

        //将y的左子节点的父节点设置为x
        if(y.left != null) {
            y.left.parent = x;
        }

        //当x的父节点不为空时,将y的父节点设置为x的父节点,并将x的父节点指定为y
        if(x.parent != null) {
            y.parent = x.parent;
            //x是其父节点的左子树时,将y设置为x父节点的左子树
            if(x == x.parent.left) {
                x.parent.left = y;
            }else {
                x.parent.right = y;
            }
        }else {
            //x没有父节点,说明x为根节点
            this.root = y;
            this.root.parent = null;
        }
        //将x的父节点更新为y,将y的左子节点更新为x
        y.left = x;
        x.parent = y;
    }

    /**
     * 右旋方法
     * 右旋示意图:右旋y节点
     *
     *    p                       p
     *    |                       |
     *    y                       x
     *   / \          ---->      / \
     *  x   ry                  lx  y
     * / \                         / \
     *lx  ly                      ly  ry
     *
     * 右旋做了几件事?
     * 1.将y的左子节点设置为x的右子节点,并将x的右子节点的父节点设置为y;
     * 2.当y的父节点不为空时,将x的父节点设置为y的父节点,并将y的父节点指定为x
     * 3.将y的父节点更新为x,将x的右子节点更新为y
     */
    private void rightRotate(RBNode y) {
        RBNode x = y.left;

        //1.将y的左子节点设置为x的右子节点,并将x的右子节点的父节点设置为y;
        y.left = x.right;
        if(x.right != null) {
            x.right.parent = y;
        }

        //2.当y的父节点不为空时,将x的父节点设置为y的父节点,并将y的父节点指定为x
        if (y.parent != null) {
            x.parent = y.parent;
            if (y == y.parent.left) {
                y.parent.left = x;
            }else {
                y.parent.right = x;
            }
        }else {
            this.root = x;
            this.root.parent = null;
        }

        //3.将y的父节点更新为x,将x的右子节点更新为y
        x.right = y;
        y.parent = x;
    }


    //对外公开的插入方法
    public void insert(K key, V value) {
        RBNode node = new RBNode();
        node.setKey(key);
        node.setValue(value);
        node.setColor(RED);
        insert(node);
    }
    //内部封装的插入方法
    private void insert(RBNode node) {
        //找到node的父节点
        RBNode parent = null;
        RBNode x = this.root;

        while (x != null) {
            parent = x;
            //cmp > 0说明node.key大于x.key需要到x的右子树查找,否则cmp小于0,相等cmp=0
            int cmp = node.key.compareTo(parent.key);
            if(cmp > 0) {
                x = x.right;
            }else if(cmp < 0) {
                x = x.left;
            }else {
                parent.setValue(node.getValue());
                return;
            }
        }
        node.parent = parent;

        //判断node与parent的key谁大,然后决定node是它的左节点还是右节点
        if(parent != null) {
            if(node.key.compareTo(parent.key) < 0) {
                parent.left = node;
            } else {
                parent.right = node;
            }
        }else {
            this.root = node;
        }

        //插入之后需要进行修复红黑树,让红黑树再次平衡。
        insertFixUp(node);
    }

    /**
     * 插入后修复红黑树平衡的方法
     *     |---情景1:红黑树为空树。
     *     处理方法:将根节点染为黑色
     *     |---情景2:插入节点的key已经存在
     *     处理方法:不需要处理,这种情况不会调用到这个方法
     *     |---情景3:插入节点的父节点为黑色
     *     处理方法:由于插入的结点是红色的,当插入结点的父节点为黑色时,并不会影响红黑树的平衡,
     *     直接插入即可,无需做自平衡。
     *
     *     情景4 需要咱们去处理
     *     |---情景4:插入节点的父节点为红色
     *          |---情景4.1:叔叔节点存在,并且为红色(父-叔 双红)
     *          处理方法:将父节点和叔叔节点改为黑色,将祖父节点改为红色,将祖父设置为当前节点,进行下一轮处理
     *          |---情景4.2:叔叔节点不存在,或者为黑色,父节点为爷爷节点的左子树
     *               |---情景4.2.1:插入节点为其父节点的左子节点(LL情况)
     *               处理方法:将父节点设置为黑色,将祖父节点设置为红色,然后对祖父节点进行右旋
     *               |---情景4.2.2:插入节点为其父节点的右子节点(LR情况)
     *               处理方法:对父节点进行左旋,得到LL双红情况(也就是4.2.1),然后父节点成为当前节点,再按照4.2.1的方法处理
     *          |---情景4.3:叔叔节点不存在,或者为黑色,父节点为爷爷节点的右子树
     *               |---情景4.3.1:插入节点为其父节点的右子节点(RR情况)
     *               处理方法:将父节点设置为黑色,将祖父节点设置为红色,对祖父节点进行左旋处理
     *               |---情景4.3.2:插入节点为其父节点的左子节点(RL情况)
     *               处理方法:对父节点进行右旋,得到RR双红色情况(也就是4.3.1),然后将父节点设置为当前节点,再按照4.3.1的方法处理
     */
    private void insertFixUp(RBNode node) {
        RBNode parent = parentOf(node);
        RBNode gparent = parentOf(parent);

        //情景4:插入节点的父节点为红色
        //如果父节点是红色,因为根节点不可能是红色,所以一定存在爷爷节点
        if(parent != null && isRed(parent)) {
            RBNode uncle = null;
            //如果parent是gparent的左节点,那么uncle一定是gparent的右节点
            if(parent == gparent.left) {
                uncle = gparent.right;

                //情景4.1:叔叔节点存在,并且为红色(父-叔 双红)
                if(uncle != null && isRed(uncle)) {
                    setBlack(parent);
                    setBlack(uncle);
                    setRed(gparent);
                    insertFixUp(gparent);
                    return;
                }
                //情景4.2:叔叔节点不存在,或者为黑色,父节点为爷爷节点的左子树
                if(uncle == null || isBlack(uncle)) {
                    //情景4.2.1:插入节点为其父节点的左子节点(LL双红情况)
                    if (node == parent.left) {
                        setBlack(parent);
                        setRed(gparent);
                        rightRotate(gparent);
                        return;
                    }
                    //情景4.2.2:插入节点为其父节点的右子节点(LR情况)
                    if(node == parent.right) {
                        leftRotate(parent);
                        insertFixUp(parent);
                        return;
                    }
                }
            }else {  //父节点为爷爷节点的右子树
                uncle = gparent.left;
                //情景4.1:叔叔节点存在,并且为红色(父-叔 双红)
                if(uncle != null && isRed(uncle)) {
                    setBlack(parent);
                    setBlack(uncle);
                    setRed(gparent);
                    insertFixUp(gparent);
                    return;
                }

                //情景4.3:叔叔节点不存在,或者为黑色,父节点为爷爷节点的右子树
                if(uncle == null || isBlack(uncle)) {
                    //情景4.3.1:插入节点为其父节点的右子节点(RR情况)
                    if(node == parent.right) {
                        setBlack(parent);
                        setRed(gparent);
                        leftRotate(gparent);
                        return;
                    }

                    //情景4.3.2:插入节点为其父节点的左子节点(RL情况)
                    if(node == parent.left) {
                        rightRotate(parent);
                        insertFixUp(parent);
                        return;
                    }
                }
            }
        }
        setBlack(this.root);
    }


    static class RBNode<K extends Comparable<K>,V> {
        //父节点
        private RBNode parent;
        //左子节点
        private RBNode left;
        //右子节点
        private RBNode right;
        //颜色
        private boolean color;
        //key
        private K key;
        //value
        private V value;

        public RBNode() {

        }

        public RBNode(RBNode parent, RBNode left, RBNode right, boolean color, K key, V value) {
            this.parent = parent;
            this.left = left;
            this.right = right;
            this.color = color;
            this.key = key;
            this.value = value;
        }

        public RBNode getParent() {
            return parent;
        }

        public void setParent(RBNode parent) {
            this.parent = parent;
        }

        public RBNode getLeft() {
            return left;
        }

        public void setLeft(RBNode left) {
            this.left = left;
        }

        public RBNode getRight() {
            return right;
        }

        public void setRight(RBNode right) {
            this.right = right;
        }

        public boolean isColor() {
            return color;
        }

        public void setColor(boolean color) {
            this.color = color;
        }

        public K getKey() {
            return key;
        }

        public void setKey(K key) {
            this.key = key;
        }

        public V getValue() {
            return value;
        }

        public void setValue(V value) {
            this.value = value;
        }
    }
}

2、插入测试

就简单拿key测试一下,不输入value了

import java.util.Scanner;


public class RBTreeTest {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);

        RBTree<String,Object> rbt = new RBTree<>();
        while (true) {
            System.out.println("请输入key:");
            String key = scan.next();
            System.out.println();
            rbt.insert(key,null);  //value置为null就可以了
            TreeOperation.show(rbt.getRoot());   //这个是封装好的打印红黑树的代码
        }
    }
}

运行结果:

请输入key:
a

a-B
请输入key:
b

   a-B 
    \  
     b-R
请输入key:
c

   b-B 
  / \  
 a-R c-R
请输入key:
d

      b-B    
    /   \    
  a-B     c-B
           \ 
            d-R
请输入key:
d

      b-B    
    /   \    
  a-B     c-B
           \ 
            d-R
请输入key:
e

      b-B    
    /   \    
  a-B     d-B
         / \ 
        c-R e-R
请输入key:
f

            b-B          
         /     \         
      a-B         d-R    
                /   \    
              c-B     e-B
                       \ 
                        f-R
请输入key:
g

            b-B          
         /     \         
      a-B         d-R    
                /   \    
              c-B     f-B
                     / \ 
                    e-R g-R
请输入key:
h

            d-B          
         /     \         
      b-R         f-R    
    /   \       /   \    
  a-B     c-B e-B     g-B
                       \ 
                        h-R
请输入key:
i

            d-B          
         /     \         
      b-R         f-R    
    /   \       /   \    
  a-B     c-B e-B     h-B
                     / \ 
                    g-R i-R

最后可以看到每条路径中的黑色节点数目都是2,且满足红黑树的其他性质,所以插入测试成功。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值