红黑树

上一篇博客我们介绍了二叉搜索树,二叉搜索树对于某个节点而言,其左子树的节点关键值都小于该节点关键值,右子树的所有节点关键值都大于该节点关键值。二叉搜索树作为一种数据结构,其查找、插入和删除操作的时间复杂度都为O(logn),底数为2。但是我们说这个时间复杂度是在平衡的二叉搜索树上体现的,也就是如果插入的数据是随机的,则效率很高,但是如果插入的数据是有序的,比如从小到大的顺序【10,20,30,40,50】插入到二叉搜索树中:

从大到小就是全部在左边,这和链表没有任何区别了,这种情况下查找的时间复杂度为O(N),而不是O(logN)。当然这是在最不平衡的条件下,实际情况下,二叉搜索树的效率应该在O(N)和O(logN)之间,这取决于树的不平衡程度。

那么为了能够以较快的时间O(logN)来搜索一棵树,我们需要保证树总是平衡的(或者大部分是平衡的),也就是说每个节点的左子树节点个数和右子树节点个数尽量相等。红-黑树的就是这样的一棵平衡树,对一个要插入的数据项(删除也是),插入例程要检查会不会破坏树的特征,如果破坏了,程序就会进行纠正,根据需要改变树的结构,从而保持树的平衡。

红-黑树的特征

有如下两个特征:

①、节点都有颜色;

②、在插入和删除的过程中,要遵循保持这些颜色的不同排列规则。

第一个很好理解,在红-黑树中,每个节点的颜色或者是黑色或者是红色的。当然也可以是任意别的两种颜色,这里的颜色用于标记,我们可以在节点类Node中增加一个boolean型变量isRed,以此来表示颜色的信息。

第二点,在插入或者删除一个节点时,必须要遵守的规则称为红-黑规则:

  1. 每个节点不是红色就是黑色的;
  2. 根节点总是黑色的;
  3. 如果节点是红色的,则它的子节点必须是黑色的(反之不一定),(也就是从每个叶子到根的所有路径上不能有两个连续的红色节点);
  4. 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。

从根节点到叶节点的路径上的黑色节点的数目称为黑色高度,规则 4 另一种表示就是从根到叶节点路径上的黑色高度必须相同。

注意:新插入的节点颜色总是红色的,这是因为插入一个红色节点比插入一个黑色节点违背红-黑规则的可能性更小,原因是插入黑色节点总会改变黑色高度(违背规则4),但是插入红色节点只有一半的机会会违背规则3(因为父节点是黑色的没事,父节点是红色的就违背规则3)。另外违背规则3比违背规则4要更容易修正。当插入一个新的节点时,可能会破坏这种平衡性,那么红-黑树是如何修正的呢?

红-黑树的自我修正

红-黑树主要通过三种方式对平衡进行修正,改变节点颜色、左旋和右旋。

①、改变节点颜色

新插入的节点为15,一般新插入颜色都为红色,那么我们发现直接插入会违反规则3,改为黑色却发现违反规则4。这时候我们将其父节点颜色改为黑色,父节点的兄弟节点颜色也改为黑色。通常其祖父节点50颜色会由黑色变为红色,但是由于50是根节点,所以我们这里不能改变根节点颜色。

②、右旋

首先要说明的是节点本身是不会旋转的,旋转改变的是节点之间的关系,选择一个节点作为旋转的顶端,如果做一次右旋,这个顶端节点会向下和向右移动到它右子节点的位置,它的左子节点会上移到它原来的位置。右旋的顶端节点必须要有左子节点。

③、左旋

左旋的顶端节点必须要有右子节点。

注意:我们改变颜色也是为了帮助我们判断何时执行什么旋转,而旋转是为了保证树的平衡。光改变节点颜色是不能起到任何作用的旋转才是关键的操作,在新增节点或者删除节点之后,可能会破坏二叉树的平衡,那么何时执行旋转以及执行什么旋转,这是我们需要重点关注的。

左旋和右旋代码

①、节点类

节点类和二叉树的节点类差不多,只不过在其基础上增加了一个 boolean 类型的变量来表示节点的颜色。

public class RBNode<T extends Comparable<T>> {
    boolean color;//颜色
    T key;//关键值
    RBNode<T> left;//左子节点
    RBNode<T> right;//右子节点
    RBNode<T> parent;//父节点
     
    public RBNode(boolean color,T key,RBNode<T> parent,RBNode<T> left,RBNode<T> right){
        this.color = color;
        this.key = key;
        this.parent = parent;
        this.left = left;
        this.right = right;
    }
     
    //获得节点的关键值
    public T getKey(){
        return key;
    }
    //打印节点的关键值和颜色信息
    public String toString(){
        return ""+key+(this.color == RED ? "R":"B");
    }
}

②、左旋的具体实现

/*************对红黑树节点x进行左旋操作 ******************/
/* 
 * 左旋示意图:对节点x进行左旋 
 *     p                       p 
 *    /                       / 
 *   x                       y 
 *  / \                     / \ 
 * lx  y      ----->       x  ry 
 *    / \                 / \ 
 *   ly ry               lx ly 
 * 左旋做了三件事: 
 * 1. 将y的左子节点赋给x的右子节点,并将x赋给y左子节点的父节点(y左子节点非空时) 
 * 2. 将x的父节点p(非空时)赋给y的父节点,同时更新p的子节点为y(左或右) 
 * 3. 将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;
    if(x.parent == null){
        this.root = y;//如果x的父节点为空(即x为根节点),则将y设为根节点
    }else{
        if(x == x.parent.left){//如果x是左子节点
            x.parent.left = y;//则也将y设为左子节点 
        }else{
            x.parent.right = y;//否则将y设为右子节点 
        }
    }
     
    //3. 将y的左子节点设为x,将x的父节点设为y
    y.left = x;
    x.parent = y;
}

③、右旋的具体实现  

/*************对红黑树节点y进行右旋操作 ******************/ 
/*
 * 左旋示意图:对节点y进行右旋
 *        p                   p
 *       /                   /
 *      y                   x
 *     / \                 / \
 *    x  ry   ----->      lx  y
 *   / \                     / \
 * lx  rx                   rx ry
 * 右旋做了三件事:
 * 1. 将x的右子节点赋给y的左子节点,并将y赋给x右子节点的父节点(x右子节点非空时)
 * 2. 将y的父节点p(非空时)赋给x的父节点,同时更新p的子节点为x(左或右)
 * 3. 将x的右子节点设为y,将y的父节点设为x
 */
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){
        this.root = x;//如果y的父节点为空(即y为根节点),则旋转后将x设为根节点
    }else{
        if(y == y.parent.left){//如果y是左子节点
            y.parent.left = x;//则将x也设置为左子节点
        }else{
            y.parent.right = x;//否则将x设置为右子节点
        }
    }
     
    //3. 将x的左子节点设为y,将y的父节点设为y
    x.right = y;
    y.parent = x;
}

插入操作

和二叉树的插入操作一样,都是得先找到插入的位置,然后再将节点插入。先看看插入的前段代码:

 

posted @   残城碎梦  阅读(62)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示