数据结构算法之红-黑树

描述

    红-黑树是特殊的二叉查找树,又名R-B树(RED-BLACK-TREE),由于红黑树是特殊的二叉查找树,即红黑树具有了二叉查找树的特性。
    二叉查找树(BST)是一颗二叉树,其中每个节点都含有一个可比较的键(以及相关联的值)且每个结点的键都大于其左子树中的任意结点的键而小于右子树的任意结点的键。
  红-黑树也是一颗平衡树(但又区别于完全的平衡二叉树–AVL),对一个要插入的数据项(节点),插入过程会检查会不会破坏树的特征,如果有破坏,会进行修正,根据需要改变数的结构,从而保持树的平衡。红黑树的平均查找效率为O(logN)。

红-黑树特征及规则

特征

  1. 节点都有颜色(两种颜色)。
  2. 在插入和删除的过程中,会遵循保持这些颜色的不同排列规则。

规则(基本性质)

  1. 每个节点的颜色为红色或黑色中的一种。
  2. 根节点总是黑色。
  3. 如果当前节点为红色,则它的子节点必须为黑色,但反之则不一定。(即不能出现两个连续的红色节点)。
  4. 从根节点到叶子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。
  5. 每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]

注意:新插入的节点颜色默认均为红色。
 因为依据红黑树规则,树中的黑色节点数量是红色节点数量的2倍多,插入一个红色节点比插入一个黑色节点违背红-黑树规则的可能性(概率)更小。插入黑色节点总会改变黑色高度,但是插入红色节点只有50%的概率会违背规则3,另外违背规则3要比违背规则4更容易修正。

红黑树修正处理方式

实现逻辑

 首先要说明的是节点本身是不会旋转的,旋转改变的是节点之间的关系。

  • 调整节点颜色
     一般新插入颜色都为红色,那么我们发现直接插入如果违反规则3,改为黑色可能违反规则4。这时候我们将其父节点颜色改为黑色,父节点的兄弟节点颜色也改为黑色。

  • 左旋

    • 条件:插入节点的父节点为红色,叔叔节点为黑色,且插入节点是父节点的右子节点。
    • 步骤:
      1. 将y的左节点ly赋给x的右节点,并将x作为y的左子节点的父节点,即将y的左子节点转移到x的右子节点;
      2. 将y转移到原x的位置,将x作为y的左子节点,y作为x的父节点。
  • 右旋

    • 条件:插入节点的父节点为红色,叔叔节点为黑色,且插入节点是父节点的右子节点。
    • 步骤:
      1. 将x的右节点rx赋给y的左子节点,将y作为x的左子节点的父节点,即将x的右子节点转移到y的左子节点;
      2. 将x转移到原y的位置,将y作为x的右子节点,x作为y的父节点。

代码实现

  • 左旋
private void leftRotate(RBNode<T> x){
    //1. 将y的左子节点赋给x的右子节点,并将x赋给y左子节点的父节点(y左子节点非空时)
    RBNode<T> y = x.right;
    x.right = y.left;
    if(y.left != null){
        y.left.parent = x;
    }
     
    //2. 将x的父节点p(非空时)赋给y的父节点,同时更新p的子节点为y(左或右)
    y.parent = x.parent;
    //如果x的父节点为空(即x为根节点),则将y设为根节点
    if(x.parent == null){
        this.root = y;
    }else{
    	//如果x是左子节点,则也将y设为左子节点,否则将y设为右子节点 
        if(x == x.parent.left){
            x.parent.left = y;
        }else{
            x.parent.right = y;
        }
    }
     
    //3. 将y的左子节点设为x,将x的父节点设为y
    y.left = x;
    x.parent = y;
}
  • 右旋
private void rightRotate(RBNode<T> y){
    //1. 将y的左子节点赋给x的右子节点,并将x赋给y左子节点的父节点(y左子节点非空时)
    RBNode<T> x = y.left;
    y.left = x.right;
    if(x.right != null){
        x.right.parent = y;
    }
     
    //2. 将x的父节点p(非空时)赋给y的父节点,同时更新p的子节点为y(左或右)
    x.parent = y.parent;
    if(y.parent == null){
    	//如果y的父节点为空(即y为根节点),则旋转后将x设为根节点
        this.root = x;
    }else{
    	//如果y是左子节点,则将x也设置为左子节点,否则将x设置为右子节点
        if(y == y.parent.left){
            y.parent.left = x;
        }else{
            y.parent.right = x;
        }
    }
     
    //3. 将x的左子节点设为y,将y的父节点设为y
    x.right = y;
    y.parent = x;
}

插入操作

实现逻辑

  1. 先找到节点插入的位置
  2. 进行插入节点操作
  3. 平衡树,进行修正处理(insertFixUp(node))

修正处理的情形与处理:

  • 如果是第一次插入,由于原树为空,则只需将根节点颜色改为黑色。–改变颜色
  • 如果插入节点的父节点是黑色的,那不会违背红-黑树的规则,什么也不需要做。–不需要处理
  • 其他情形
    • 插入节点的父节点和其叔叔节点(祖父节点的另一个子节点)均为红色。–改变颜色
    • 插入节点的父节点是红色的,叔叔节点是黑色的,且插入节点是其父节点的右子节点。–左旋
    • 插入节点的父节点是红色的,叔叔节点是黑色的,且插入节点是其父节点的左子节点。–右旋

代码实现

public void insert(T key){
    RBNode<T> node = new RBNode<T>(RED, key, null, null, null);
    if(node != null){
        insert(node);
    }
}

public void insert(RBNode<T> node){
	//表示最后node的父节点
    RBNode<T> current = null;
    //用来向下搜索
    RBNode<T> x = this.root;
     
    //1.找到插入位置
    while(x != null){
        current = x;
        int cmp = node.key.compareTo(x.key);
        if(cmp < 0){
            x = x.left;
        }else{
            x = x.right;
        }
    }
    //找到了插入的位置,将当前current作为node的父节点
    node.parent = current;
     
    //2.接下来判断node是左子节点还是右子节点
    if(current != null){
        int cmp = node.key.compareTo(current.key);
        if(cmp < 0){
            current.left = node;
        }else{
            current.right = node;
        }
    }else{
        this.root = node;
    }
     
    //3.利用旋转操作将其修正为一颗红黑树
    insertFixUp(node);
}

private void insertFixUp(RBNode<T> node){
	//定义父节点和祖父节点
    RBNode<T> parent,gparent;
     
    //需要修正的条件:父节点存在,且父节点的颜色是红色
    while(((parent = parentOf(node)) != null) && isRed(parent)){
        gparent = parentOf(parent);
         
        //若父节点是祖父节点的左子节点,下面的else相反
        if(parent == gparent.left){
        	//获得叔叔节点
            RBNode<T> uncle = gparent.right;
             
            //case1:叔叔节点也是红色
            if(uncle != null && isRed(uncle)){
                setBlack(parent);
                setBlack(gparent);
                setRed(gparent);
                //把位置放到祖父节点处
                node = gparent;
                //继续while循环,重新判断
                continue;
            }
             
            //case2:叔叔节点是黑色,且当前节点是右子节点
            if(node == parent.right){
            	//从父节点出左旋
                leftRotate(parent);
                //然后将父节点和自己调换一下,为下面右旋做准备
                RBNode<T> tmp = parent;
                parent = node;
                node = tmp;
            }
             
            //case3:叔叔节点是黑色,且当前节点是左子节点
            setBlack(parent);
            setRed(gparent);
            rightRotate(gparent);
        }else{
        	//若父节点是祖父节点的右子节点,与上面的情况完全相反,本质是一样的
            RBNode<T> uncle = gparent.left;
             
            //case1:叔叔节点也是红色的
            if(uncle != null && isRed(uncle)){
                setBlack(parent);
                setBlack(uncle);
                setRed(gparent);
                node = gparent;
                continue;
            }
             
            //case2:叔叔节点是黑色的,且当前节点是左子节点
            if(node == parent.left){
                rightRotate(parent);
                RBNode<T> tmp = parent;
                parent = node;
                node = tmp;
            }
             
            //case3:叔叔节点是黑色的,且当前节点是右子节点
            setBlack(parent);
            setRed(gparent);
            leftRotate(gparent);
        }
    }
    //将根节点设置为黑色
    setBlack(root);
}

总结

  红黑树的查找、插入和删除时间复杂度都为O(log2Nlog2^N),额外的开销是每个节点的存储空间都稍微增加了一点,因为一个存储红黑树节点的颜色变量。插入和删除的时间要增加一个常数因子,因为要进行旋转,平均一次插入大约需要一次旋转,因此插入的时间复杂度还是O(log2Nlog2^N)(时间复杂度的计算要省略常数),但实际上比普通的二叉树是要慢的。
  大多数应用中,查找的次数比插入和删除的次数多,所以应用红黑树取代普通的二叉搜索树总体上不会有太多的时间开销。而且红黑树的优点是对于有序数据的操作不会慢到O(N)的时间复杂度。

参考文章

Java数据结构和算法(十一)——红黑树
JAVA实践红黑树-小试牛刀
java数据结构与算法之平衡二叉树(AVL树)的设计与实现

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章