平衡树-红黑树(史上最通俗易懂的红黑树介绍及实现)

在了解红黑树之前,我们首先来了解一下2-3树,红黑树可以说是2-3树的一种具体实现。

2-3查找树

1. 2-3查找树的定义

一棵2-3查找树要么为空,要么满足满足下面两个要求:

  • 2- 结点:                                                                                                                                            含有一个键(及其对应的值)和两条链,左链接指向2-3树中的键都小于该结点,右链接指向的2-3树中的键都大于该结点。
  • 3- 结点:                                                                                                                                            含有两个键(及其对应的值)和三条链,左链接指向的2-3树中的键都小于该结点,中链接指向的2-3树中的键都位于该结点的两个键之间,右链接指向的2-3树中的键都大于该结点。
  • 简单来说,2-3树有两种类型的节点:第一种是只含有1个数据的节点,该节点因为有左右2个连接指针,所以称为2-节点第二种是含有2个数据的节点,该节点因为有3个连接指针,所以称为3-节点。
  • 其他的性质跟二叉查找树是一致的

            

2. 查找

        将二叉查找树的查找算法一般化我们就能够直接得到2-3树的查找算法。要判断一个键是否在树中,我们先将它和根结点中的键比较。如果它和其中任意一个相等,查找命中;否则我们就根据比较的结果找到指向相应区间的连接,并在其指向的子树中递归地继续查找。如果这个是空链接,查找未命中。

与二叉搜索树查找一致

                        

3. 插入

3.1 向2-结点中插入数据

        往 2-3树中插入元素和往二叉查找树中插入元素一样,首先要进行查找,然后将节点挂到未找到的节点上。2-3树之所以能够保证在最差的情况下的效率的原因在于其插入之后仍然能够保持平衡状态。如果查找后未找到的节点是一个2-结点,那么很容易,我们只需要将新的元素放到这个2-结点里面使其变成一个3-结点即可。但是如果查找的节点结束于一个3-结点,那么可能有点麻烦。

        对于2-3树来说,找到要插入的2-节点并不能直接插入到2-节点的左指针或者右指针,而是要将此2-节点变成3-节点。

                   

3.2 向一棵只含有一个3-结点的树中插入数据

        假设2-3树只包含一个3-结点,这个结点有两个元素,没有空间来插入第三个键了,最自然的方式是我们假设这个结点能存放三个元素,暂时使其变成一个4-结点,同时他包含四条链接。然后,我们将这个4-结点的中间元素提升,左边的键作为其左子结点,右边的键作为其右子结点。插入完成,变为平衡2-3查找树,树的高度从0变为1。

        插入到3-结点,会临时将该3-结点变为4-结点,然后将中间键提升,两个相邻的结点拆分成2个2-结点并且高度+1

3.3 向一个父结点为2-结点的3-结点中插入新数据

        和上面的情况一样一样,我们也可以将新的元素插入到3-结点中,使其成为一个临时的4-结点,然后,将该结点中的中间元素提升到父结点即2-结点中,使其父结点成为一个3-结点,然后将左右结点分别挂在这个3-结点的恰当位置。

            

3.4  向一个父结点为3-结点的3-结点中插入新数据

        当我们插入的结点是 3-结点的时候,我们将该结点拆分,中间元素提升至父结点,但是此时父结点是一个3-结点,插入之后,父结点变成了4-结点,然后继续将中间元素提升至其父结点,直至遇到一个父结点是2-结点,然后将其变为3-结点,不需要继续进行拆分。

              

                         

3.5 分解根结点

        当插入结点到根结点的路径上全部是3-结点的时候,最终我们的根结点会编程一个临时的4-结点,此时,就需要将根结点拆分为两个2-结点,树的高度加1。

                                

4. 2-3查找树的性质

通过对2-3树插入操作的分析,我们发现在插入的时候,2-3树需要做一些局部的变换来保持2-3树的平衡。
一棵完全平衡的2-3树具有以下性质:

  1. 任意空链接到根结点的路径长度都是相等的。
  2. 4-结点变换为3-结点时,树的高度不会发生变化,只有当根结点是临时的4-结点,分解根结点时,树高+1。
  3. 2-3树与普通二叉查找树最大的区别在于,普通的二叉查找树是自顶向下生长,而2-3树是自底向上生长。

2-3树理解起来非常简单,几张图就能够理解。但是2-3树实现是比较复杂的,尤其是插入,下面介绍的红黑树算是2-3树的一种实现。

红黑树

        我们前面介绍了2-3树,可以看到2-3树能保证在插入元素之后,树依然保持平衡状态,它的最坏情况下所有子结点都是2-结点,树的高度为lgN,相比于我们普通的二叉查找树,最坏情况下树的高度为N,确实保证了最坏情况下的时间复杂度,但是2-3树实现起来过于复杂,所以我们介绍一种2-3树思想的简单实现:红黑树。
        红黑树主要是对2-3树进行编码,红黑树背后的基本思想是用标准的二叉查找树(完全由2-结点构成)和一些额外的信息(替换3-结点)来表示2-3树。我们将树中的链接分为两种类型:
        红链接:将两个2-结点连接起来构成一个3-结点; 黑链接:则是2-3树中的普通链接。
        确切的说,我们将3-结点表示为由由一条左斜的红色链接(两个2-结点其中之一是另一个的左子结点)相连的两个2-结点。这种表示法的一个优点是,我们无需修改就可以直接使用标准的二叉查找树的get方法。

1. 红黑树定义

红黑树是含有红黑链接并满足下列条件的二叉查找树:

  1. 红链接均为左链接;
  2. 没有任何一个结点同时和两条红链接相连;
  3. 该树是完美黑色平衡的,即任意空链接到根结点的路径上的黑链接数量相同;

下面是红黑树与2-3树的对应关系:

        通过这个图可以很明显的看出来红黑树的2-3树的一种二叉树表现方式。因为二叉树每个结点有且只有一个父结点,所以图中红链接可以在结点中用一个color属性表示,被父结点用红链接连接的结点又称为红结点,被父结点用黑链接连接的结点又称为黑结点

           

2. 平衡保持

        在对红黑树进行一些增删改查的操作后,很有可能会出现红色的右链接或者两条连续红色的链接,而这些都不满足红黑树的定义,所以我们需要对这些情况通过旋转进行修复,让红黑树保持平衡。

2.1 左旋

        当某个结点的左子结点为黑色,右子结点为红色,此时需要左旋。
        前提:当前结点为h,它的右子结点为x;
        左旋过程:
        1.让x的左子结点变为h的右子结点:h.right=x.left;
        2.让h成为x的左子结点:x.left=h;
        3.让x的color属性变为h的color属性值:x.color=h.color;
        4.让h的color属性变为RED:h.color=RED;

           

2.2 右旋

        当某个结点的左子结点是红色,且左子结点的左子结点也是红色,需要右旋。
        前提:当前结点为h,它的左子结点为x;
        右旋过程:
        1. 让x的右子结点成为h的左子结点:h.left = x.right;
        2. 让h成为x的右子结点:x.right=h;
        3. 让x的color变为h的color属性值:x.color = h.color;
        4. 让h的color为RED:h.color=RED;

        可以看到下面旋转完后又不满足规则了,右链接出现红链接,别急,继续往下看

             

2.3 颜色反转

        当一个结点的左子结点和右子结点的color都为RED时,也就是出现了临时的4-结点,此时只需要把左子结点和右子结点的颜色变为BLACK,同时让当前结点的颜色变为RED即可。

2.4 根结点的颜色必须总是黑色

        由于根结点不存在父结点,所以每次插入操作后,我们都需要把根结点的颜色设置为黑色。

2.5 向单个2-结点中插入新键

        红黑树新增的结点必然是红色标记的

  • 如果新键小于当前结点的键,我们只需要新增一个红色结点即可,新的红黑树和单个 3-结点完全等价。                                                                                                                                       
  • 如果新键大于当前结点的键,那么新增的红色结点将会产生一条红色的右链接,此时我们需要通过左旋,把红色右链接变成左链接,插入操作才算完成。形成的新的红黑树依然和3-结点等价,其中含有两个键,一条红色链接。                                                                                               

2.6 向底部的2-结点插入新键

        用和二叉查找树相同的方式向一棵红黑树中插入一个新键,会在树的底部新增一个结点(可以保证有序性),唯一区别的地方是我们会用红链接将新结点和它的父结点相连。如果它的父结点是一个2-结点,那么刚才讨论的两种方式仍然适用。总的来说:红黑树新增的结点必然是红色标记的。

              

2.7 向树底部的3-结点插入新键

        假设在树的底部的一个3-结点下加入一个新的结点。前面我们所讲的3种情况都会出现。指向新结点的链接可能是3-结点的右链接(此时我们只需要转换颜色即可),或是左链接(此时我们需要进行右旋转然后再转换),或是中链接(此时需要先左旋转然后再右旋转,最后转换颜色)。颜色转换会使中间结点的颜色变红,相当于将它送入了父结点。这意味着父结点中继续插入一个新键,我们只需要使用相同的方法解决即可,直到遇到一个2-结点或者根结点为止。

                       

                          

3. 代码实现

        笔者这边使用的是java代码实现。 

  1. 新建RedBlackTree类和建立Node结点类;RED和BALCK是两个常量,颜色设置而已;Key继承Comparable类是为了后面做大小比较用。
    public class RedBlackTree<Key extends Comparable<Key>, Value> {
        private Node root;//根节点
        private final boolean RED = true;//红色
        private final boolean BLACK = false;//黑色
        class Node{
            Key key;//键
            Value value;//值
            Node left;//左子树
            Node right;//右子树
            boolean color;//颜色
    
            public Node(Key key, Value value, Node left, Node right, boolean color) {
                this.key = key;
                this.value = value;
                this.left = left;
                this.right = right;
                this.color = color;
            }
        }
  2. 编写一个判断当前结点是否为红色的方法
    //判断当前节点是否为红色
        public boolean isRed(Node node){
            if (node == null) return BLACK;
            return node.color == RED;
        }
  3. 编写左旋转方法
    //左旋 h:要旋转的节点
        public Node rotateLeft(Node h){
            Node x = h.right;//h的右节点,破坏红黑树特性之一:右连接不能是红色
            //1.让x的左子结点变为h的右子结点:
            h.right = x.left;
            //2.让h成为x的左子结点
            x.left = h;
            //3.让x的color属性变为h的color属性值
            x.color = h.color;
            //4.让h的color属性变为RED
            h.color = RED;
            return x;//返回旋转后的node节点
        }
  4. 编写右旋转方法
    //右旋 h:要旋转的节点
        public Node rotateRight(Node h){
            Node x = h.left;//h的左节点,特性之一:不能有4-节点,所以不能出现连续的左红连接,连续的左红连接需要右旋
            //1. 让x的右子结点成为h的左子结点
            h.left = x.right;
            //2. 让h成为x的右子结点
            x.right = h;
            //3. 让x的color变为h的color属性值
            x.color = h.color;
            //4. 让h的color为RED
            h.color = RED;
            return x;//返回旋转后的node节点
        }
  5. 编写颜色翻转方法
    //颜色翻转
        public void flipColor(Node h){
            //红黑树特性之一:不能有4-节点,所以不能有左右都是红的节点
            //1. 让h的左右节点变为黑色
            h.left.color = BLACK;
            h.right.color = BLACK;
            //2. 让h变为红色
            h.color = RED;
        }
  6. 编写插入方法
    /**
         *
         * @param node 当前递归到的节点 初始传入root节点开始递归
         * @param key 新增节点的键
         * @param value 新增节点的值
         * @return 新节点的父节点 最终递归完返回的为root节点
         */
        public Node put(Node node, Key key, Value value){
            //递归到底,新建节点返回,该节点就是增加的节点
            if (node == null) return new Node(key,value,null,null,RED);
            int cp = key.compareTo(node.key);//要添加的节点key和当前节点key的比较值
            //key大于当前节点key,右递归
            if (cp > 0){
                node.right = put(node.right,key,value);
            }else if (cp < 0){//key小于当前节点key,左递归
                node.left = put(node.left,key,value);
            }else{//key等于当前节点key,修改value值
                node.value = value;
            }
            //破坏红黑树特性之一:右节点不能为红色,左旋
            if (isRed(node.right) && !isRed(node.left)) node = rotateLeft(node);
            //破坏红黑树特性之一:左节点不能连续红色,右旋
            if (node.left != null && isRed(node.left) && isRed(node.left.left)) node = rotateRight(node);
            //破坏红黑树特性之一:左右节点不能同时为红色,颜色翻转
            if (isRed(node.left) && isRed(node.right)) flipColor(node);
            return node;
        }

    插入方法重载,作为api使用,并且每次将root结点置为黑色

    public void put(Key key, Value value){
            root = put(root,key,value);
            root.color = BLACK;//红黑树特性之一:根节点必定为黑色
        }
  7. 编写获取方法 与二叉搜索树是一样的,代码就没多解释了,比较简单
    //根据key获取节点,比较简单,就不做多解释~
        public Node get(Node node, Key key){
            if (node == null) return null;
            int cp = key.compareTo(node.key);
            return cp > 0 ? get(node.right,key) : cp < 0 ? get(node.left,key) : node;
        }

    get方法重载,作为提供api使用,并且返回value值

    //根据下面get方法得到节点取出value值
        public Value get(Key key){
            Node val = get(root, key);
            return val == null ? null : val.value;
        }
  8. 测试代码
    public static void main(String[] args) {
            RedBlackTree<String,Integer> rBTree = new RedBlackTree<>();
            rBTree.put("vuhen",10);
            rBTree.put("无痕",100);
            System.out.println(rBTree.get("vuhen"));
            System.out.println(rBTree.get("无痕"));
        }
  9. 执行结果                                                                                                                                    

以上就是红黑树的实现了~是不是也没有想象中那么难,如果有bug请在评论区中指出~

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值