RBT(红黑树)插入算法

2-3查找树

为了保持平衡性,同时也为了避免AVL那样多次的判断和旋转,需要一些灵活性,故允许一个节点可以保存两个键,它有三条链接,称3节点,BST中的节点称2节点。

2-3查找树的定义:它是一棵查找树或空树,由2节点和3节点组成,2节点含有一个键和两条链接,2节点的左子树中的键均小于该节点的键,右子树中的键均大于该节点的键;3节点含有两个键和三条链接,左子树中的键小于该节点的键,中子树中的键在该节点的两个键之间,右子树中的键均大于该节点的键。

完美平衡的2-3查找树中,所有空链接到树头节点的距离都是相等的。为方便描述,后面将完美平衡的2-3查找树称2-3树。

2-3树的插入操作

向2节点中插入

由于每个节点最多能含有两个链接,这种情况很简单,递归查找要插入的位置,插入即可
《RBT(红黑树)插入算法》

向3节点插入

  • 该树只有一个3节点
    临时创建一个4节点,然后将其分解为一个2-3树,分解后树的高度增加1
    《RBT(红黑树)插入算法》
  • 向一个父节点为2节点的3节点插入
    临时创建一个4节点,然后将中键移动到原节点的父节点中,指向原3节点的一条链接替换为新父节点中的两条链接,这两条链接分别指向原3节点中的较小者和较大者
    《RBT(红黑树)插入算法》

  • 向一个父节点为3节点的3节点插入
    还是临时创建一个4节点,然后将中间移动到原节点的父节点中,然后父节点进行同样的变换,一直不断的向上分解临时4节点,直到遇到一个2节点并将它替换为一个3节点,或者是到达3节点的根。
    《RBT(红黑树)插入算法》

分解根节点

如果插入节点到根节点的路径上全是3节点,最终根节点将变成一个临时的4节点,处理方法类似向只有一个3节点的树插入,将一个4节点分解为3个2节点,并将树的高度增加1。
《RBT(红黑树)插入算法》

说了这么多,到底2-3树的插入操作有哪些特别的地方呢?对于向2-3树中插入节点,不论是向2节点还是3节点中插入亦或是分解根节点,都只需要修改当前的节点和父节点及链接,并不会涉及树的其他部分(可对比AVL),即是局部的,且保持了树的有序性和平衡性,每次只有在根节点分解时,树的高度才增加1,对比AVL,每次插入都可能会使树高度增加1,同样数量的节点,2-3树的高度会低很多。

红黑树的插入操作

红黑树的基本思想就是用标准的二叉查找树(全部是2节点)和一些额外的信息(替代3节点)来表示2-3树。红链接将两个2节点连接起来构成一个3节点,黑链接即普通的链接。
《RBT(红黑树)插入算法》
可能这样不是很好理解,红黑树的等价定义为:

  • 红链接均为左链接
  • 没有节点同时和两条红链接相连
  • 红黑树是黑色完美平衡的,即任意空链接到根节点的路径上的黑链接数量相同

将红黑树的红链接画平,就是2-3树
《RBT(红黑树)插入算法》

颜色表示

为方便实现,每个节点都只会有一条指向自己的链接,故将颜色保存在节点的boolean变量中,若指向它的节点为红链接,则为true,黑色则为false。约定空链接为黑色
《RBT(红黑树)插入算法》

旋转操作

与AVL的不同,红黑树的旋转条件是:某一节点出现红色右链接或者两条红链接。通过旋转操作修复,使得节点满足红黑树的定义,同时在旋转时要改变颜色,而AVL中是树的高度。

由于先入为主,我自己实现的代码命名左旋转和右旋转与《数据结构与算法分析:C语言描述》上一致,与《算法第四版》的命名相反
1. 左旋转
《RBT(红黑树)插入算法》
注意这是一个中间操作,后面会通过右旋转和颜色转换修复。
2. 右旋转
《RBT(红黑树)插入算法》

旋转的情况

什么情况下要旋转,左还是右?

向2节点插入

向2节点插入又分两种情况,对应2-3树中向2节点插入,单个2节点以及树底部的2节点

单个2节点

向左插入,则结果将是一个3节点,不需要修复;向右插入,出现右红链接,通过一次右旋转修复,修复结果为一个3节点。
《RBT(红黑树)插入算法》《RBT(红黑树)插入算法》

树底的2节点

插入会新增节点,由于每次都是红链接将新节点与父节点相连,故如果是在左边插入,是不需要修复,结果将是新建一个3节点;在右边插入,出现红色右链接,通过一次右旋转修复,修复结果仍然是新建一个3节点。
《RBT(红黑树)插入算法》
图中没有画出不需要修复的情况

向一个3节点中插入

对应2-3树中的分解4节点。

向3节点中插入分3种情况,新键最大,新键最小,新键介于两者之间

新键最大

由于每次都是红链接将新节点与父节点相连,此时父节点将出现左右链接均为红色,将两条红色链接变为黑,就能得到由三个2节点构成的平衡树,同时也是满二叉树,对应2-3树中将一个4节点分解为三个2节点

新键最小

由于每次都是红链接将新节点与父节点相连,此时出现了包含最大键的节点的左孩子和其左孙子都为红节点,不满足红黑树的定义,通过一次左旋转,左旋转后出现了和新建最大一样的情况——处于中间的节点的左右链接均为红,进行颜色转换即可。

新键介于两者之间

相对来说,这种情况最复杂,但搞懂了AVL,其实也不难理解,和AVL中的右-左双旋转很相似,先通过右-左双旋转,使树平衡,然后就出现了和新键最大一样的情况——处于中间的节点的左右链接均为红,进行颜色转换即可。
《RBT(红黑树)插入算法》

通过上面几种情况的分析,综合起来,旋转和颜色修复的顺序是:
1. 如果出现红色右链接且左链接为黑,进行右旋转
2. 如果当前节点的左孩子和左孙子均为红节点,进行左旋转
3. 如果当前节点的左右链接均为红色,进行颜色转换
步骤一对应向2节点右边插入和向3节点插入时新键介于两者之间的右旋转,步骤二对应向3节点插入时新键最小和新键介于两者直接的左旋转,步骤三对应向3节点插入时新键最大和新键介于两者之间的颜色转换。注意左旋转不仅要满足出现红色左链接,而且要右链接为黑,不然颜色转换可能会失败。

最后上一张图来反映上述文字表述,更直观,注意我对旋转的描述与图相反(懒得去自己画图,): )
《RBT(红黑树)插入算法》

完整代码实现:

package BinarySearchTree;  

import java.util.*;  

/** * 红黑树 * @author 小锅巴 */  
public class RBT {  
    private static final boolean RED = true;  
    private static final boolean BLACK = false;  

    private TreeNode root;

    private class TreeNode{
        String key;
        int value;
        TreeNode left, right;//左右链接
        boolean color;//节点的颜色

        TreeNode(String key, int value, boolean color){
            this.key = key;
            this.value = value;
            this.color = color;
        }
    }

    private boolean isRed(TreeNode node){
        if(node == null)
            return false;
        return node.color == RED;
    }

    //左旋转,由于我先学得AVL,先入为主,我以维斯版的来命名,与algs4的相反
    private TreeNode rotateWithLeft(TreeNode node){
        TreeNode temp = node.left;
        //完成旋转
        node.left = temp.right;
        temp.right = node;
        //和AVL相比,旋转后要变换颜色,
        temp.color = node.color;
        node.color = RED;//被旋转的节点到了子树,所以肯定是设置被旋转的节点为红色节点
        return temp;
    }

    //右旋转
    private TreeNode rotateWithRight(TreeNode node){
        TreeNode temp = node.right;

        node.right = temp.left;
        temp.left = node;

        temp.color = node.color;
        node.color = RED;

        return temp;
    }

    //颜色转换的条件是左右子节点均为红节点,由红节点的定义,那么当前节点的的孩子均不会为null,不会出现空指针异常
    private void flipColors(TreeNode node){
        node.color = RED;
        node.left.color = BLACK;
        node.right.color = BLACK;
    }

    public void insert(String key, int value){
        root = insert(root, key, value);
        root.color = BLACK;//根节点都是黑色
    }
    private TreeNode insert(TreeNode node, String key, int value){
        if( node == null)
            return new TreeNode(key, value, RED);

        int cmp = key.compareTo(node.key);
        if(cmp < 0)
            node.left = insert(node.left, key, value);
        else if(cmp > 0)
            node.right = insert(node.right, key, value);
        else
            node.value = value;

        //旋转
        if(!isRed(node.left) && isRed(node.right))//左链接为黑,右链接为红,则右旋转
            node = rotateWithRight(node);
        if(isRed(node.left.left) && isRed(node.left))//左链接为红,左孩子的左链接也为红,则左旋转
            node = rotateWithLeft(node);
        if(isRed(node.left) && isRed(node.right))//左右链接均为红,调整颜色
            flipColors(node);
        return node;
    }

    private void layerTraversal (TreeNode node){
        Queue<TreeNode> s = new LinkedList<>();
        s.add(node);
        TreeNode curNode;
        TreeNode nlast = null;//局部变量,记得赋值
        TreeNode last = node;
        while(!s.isEmpty()){
            curNode = s.poll();
            System.out.print(curNode.key+" ");
            if(curNode.left != null){
                nlast = curNode.left;
                s.add(curNode.left);
            }
            if(curNode.right != null){
                nlast = curNode.right;
                s.add(curNode.right);
            }
            if(curNode == last){
                System.out.println();
                last = nlast;
            }
        }
    }

    private void preOrderTraversal(TreeNode node){
        Stack<TreeNode> s = new Stack<>();
        TreeNode curNode = null;
        s.push(node);
        while(!s.isEmpty()){
            curNode = s.pop();
            System.out.print(curNode.key+" ");
            if(curNode.right != null)
                s.push(curNode.right);
            if(curNode.left != null)
                s.push(curNode.left);
        }
    }

    public static void main(String[] args) {
        RBT rbt = new RBT();
        System.out.print("请输入节点个数:");
        Scanner s = new Scanner(System.in);
        int num = s.nextInt();
        System.out.println("请依次输入"+num+"个字母");
        for (int i = 1; i <= num; i++){
            String value = s.next();
            rbt.insert(value, i);
        }

        System.out.println("层序遍历");
        rbt.layerTraversal(rbt.root);
        System.out.println();

        System.out.println("先序遍历");
        rbt.preOrderTraversal(rbt.root);
    }
}
/** 测试用例 请输入节点个数:10 请依次输入10个字母 S E A R C H X M P L 层序遍历 M E R C L P X A H S 先序遍历 M E C A L H R P X S */

参考资料:
1. 算法第四版
2. 数据结构与算法分析:c语言描述

    原文作者:算法小白
    原文地址: https://blog.csdn.net/xiaoguobaf/article/details/52104852
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞