数据结构之红黑树

news2024/11/25 2:49:21

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

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

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

1、红-黑树的特征
  有如下两个特征:

①、节点都有颜色;

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

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

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

1.每个节点不是红色就是黑色的;

2.根节点总是黑色的;

3.如果节点是红色的,则它的子节点必须是黑色的(反之不一定),(也就是从每个叶子到根的所有路径上不能有两个连续的红色节点);

4.从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。

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

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

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

①、改变节点颜色
  在这里插入图片描述

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

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

左旋的顶端节点必须要有右子节点。
  在这里插入图片描述

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

3、左旋和右旋代码
  ①、节点类

节点类和二叉树的节点类差不多,只不过在其基础上增加了一个 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;
}

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

/*********************** 向红黑树中插入节点 **********************/
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){
    RBNode<T> current = null;//表示最后node的父节点
    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;
        }
    }
    node.parent = current;//找到了插入的位置,将当前current作为node的父节点
     
    //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);
}

这与二叉搜索树中实现的思路一样,这里不再赘述,主要看看方法里面最后一步insertFixUp(node)操作。因为插入后可能会导致树的不平衡,insertFixUp(node) 方法里主要是分情况讨论,分析何时变色,何时左旋,何时右旋。我们先从理论上分析具体的情况,然后再看insertFixUp(node) 的具体实现。

如果是第一次插入,由于原树为空,所以只会违反红-黑树的规则2,所以只要把根节点涂黑即可;如果插入节点的父节点是黑色的,那不会违背红-黑树的规则,什么也不需要做;但是遇到如下三种情况,我们就要开始变色和旋转了:

①、插入节点的父节点和其叔叔节点(祖父节点的另一个子节点)均为红色。

②、插入节点的父节点是红色的,叔叔节点是黑色的,且插入节点是其父节点的右子节点。

③、插入节点的父节点是红色的,叔叔节点是黑色的,且插入节点是其父节点的左子节点。

下面我们挨个分析这三种情况都需要如何操作,然后给出实现代码。

在下面的讨论中,使用N,P,G,U表示关联的节点。N(now)表示当前节点,P(parent)表示N的父节点,U(uncle)表示N的叔叔节点,G(grandfather)表示N的祖父节点,也就是P和U的父节点。

对于情况1:插入节点的父节点和其叔叔节点(祖父节点的另一个子节点)均为红色。此时,肯定存在祖父节点,但是不知道父节点是其左子节点还是右子节点,但是由于对称性,我们只要讨论出一边的情况,另一种情况自然也与之对应。这里考虑父节点是其祖父节点的左子节点的情况,如下左图所示:

在这里插入图片描述
 对于这种情况,我们要做的操作有:将当前节点(4) 的父节点(5) 和叔叔节点(8) 涂黑,将祖父节点(7)涂红,变成了上有图所示的情况。再将当前节点指向其祖父节点,再次从新的当前节点开始算法(具体看下面的步骤)。这样上右图就变成情况2了。

对于情况2:插入节点的父节点是红色的,叔叔节点是黑色的,且插入节点是其父节点的右子节点。我们要做的操作有:将当前节点(7)的父节点(2)作为新的节点,以新的当前节点为支点做左旋操作。完成后如左下图所示,这样左下图就变成情况3了。
在这里插入图片描述
  对于情况3:插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的左子节点。我们要做的操作有:将当前节点的父节点(7)涂黑,将祖父节点(11)涂红,在祖父节点为支点做右旋操作。最后把根节点涂黑,整个红-黑树重新恢复了平衡,如右上图所示。至此,插入操作完成!

我们可以看出,如果是从情况1开始发生的,必然会走完情况2和3,也就是说这是一整个流程,当然咯,实际中可能不一定会从情况1发生,如果从情况2开始发生,那再走个情况3即可完成调整,如果直接只要调整情况3,那么前两种情况均不需要调整了。故变色和旋转之间的先后关系可以表示为:变色->左旋->右旋。

至此,我们完成了全部的插入操作。下面我们看看insertFixUp方法中的具体实现(可以结合上面的分析图,更加利与理解):

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;//把位置放到祖父节点处
                continue;//继续while循环,重新判断
            }
             
            //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);//将根节点设置为黑色
}

5、删除操作
  上面探讨完了红-黑树的插入操作,接下来讨论删除,红-黑树的删除和二叉查找树的删除是一样的,只不过删除后多了个平衡的修复而已。我们先来回忆一下二叉搜索树的删除:

①、如果待删除的节点没有子节点,那么直接删除即可。

②、如果待删除的节点只有一个子节点,那么直接删掉,并用其子节点去顶替它。

③、如果待删除的节点有两个子节点,这种情况比较复杂:首先找出它的后继节点,然后处理“后继节点”和“被删除节点的父节点”之间的关系,最后处理“后继节点的子节点”和“被删除节点的子节点”之间的关系。每一步中也会有不同的情况。

实际上,删除过程太复杂了,很多情况下会采用在节点类中添加一个删除标记,并不是真正的删除节点。详细的删除我们这里不做讨论。

6、红黑树的效率
  红黑树的查找、插入和删除时间复杂度都为O(log2N),额外的开销是每个节点的存储空间都稍微增加了一点,因为一个存储红黑树节点的颜色变量。插入和删除的时间要增加一个常数因子,因为要进行旋转,平均一次插入大约需要一次旋转,因此插入的时间复杂度还是O(log2N),(时间复杂度的计算要省略常数),但实际上比普通的二叉树是要慢的。

大多数应用中,查找的次数比插入和删除的次数多,所以应用红黑树取代普通的二叉搜索树总体上不会有太多的时间开销。而且红黑树的优点是对于有序数据的操作不会慢到O(N)的时间复杂度。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/859570.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C#小轮子 Debug,Release,发布模式如何运行不同的代码

文章目录 前言C#运行模式运行模式介绍三种模式区分代码 前言 编译模式和发布模式的代码不一样是非常正常的。比较常见的是数据库不一样。编译测试数据库和发布真实的数据库地址不一样。 C#运行模式 运行模式介绍 运行模式有三种&#xff1a; Debug 不进行优化&#xff0c;…

逆向破解学习-经典贪吃蛇大作战_1.00

试玩游戏 有支付取消关键字 找静态代码 找到关键字 ![在这里插入图片描述](https://img-blog.csdnimg.cn/5d9f3cc57d18477794197ea5fae8c5ba.png Hook代码 import de.robv.android.xposed.XC_MethodHook; import de.robv.android.xposed.XC_MethodReplacement; import de.r…

MyBatis操作数据库常用用法总结1

文章目录 1.单表查询1.1返回所有的表记录1.2根据id查找结果1.3根据名字查找结果 2.单表修改2.1修改密码 3.单表删除3.1根据id删除信息 4.单表增加&#xff08;根据业务情况返回&#xff09;4.1添加返回影响的行数4.2添加返回影响行数和id 5.多表查询&#xff08;多&#xff09;…

网易云信回声消除量化评估方法

背景 随着现代通信技术的发展&#xff0c;语音通信成为了人们生活中不可或缺的一部分。然而&#xff0c;环境噪声和回声等因素对语音通信质量产生了不良影响&#xff0c;降低了通信效率和用户体验&#xff0c;声学回声消除技术的应用变得越来越重要。如何评估回声消除的效果也变…

详解如何计算字符中的字节数

文章目录 字符概念转义 进制的表示前缀区分后缀区分 什么是ASCII产生表述局限性字节计算 什么是Unicode编码方式UCS-2UCS-4 实现方式UTF的字节序和BOM字节计算 JavaScript中使用字符字符使用缺陷规避字符使用缺陷 MIME 编码Base64编码字节计算QP(Quote-Printable) 总结 字符概念…

进阶-LayUI的基本必知入门教程使用3-layui-laydate选择的值怎么获取?苟日新一刻钟总结反观:

1 效果镇楼&#xff1a; 对于layui框架的刚接触没几天&#xff0c;但是您的年轻同事为啥能搞出来功能啊&#xff01;&#xff1f; 到您就不行了呢&#xff1f; 这就是您对您饭碗的态度了吗&#xff01; 但是&#xff0c;是您尽力了&#xff0c;还能说明啥子问题&#xff1f; 就…

SOLIDWORKS自动标注插件

在设计工作中&#xff0c;三维模型设计好之后&#xff0c;还要创建二维工程图&#xff0c;用来指导加工生产。二维图出图的过程是一个非常枯燥的过程&#xff0c;而且还需要根据公司规范来进行尺寸标注、公差标注等&#xff0c;所以有时候&#xff0c;可能建模只用了几分钟&…

在UOS系统中管理ORACLE数据库

在明确了“数字中国”建设战略后。自主创新与国产化已成为我国实现科技强国、经济强国的发展趋势与行业共识。 即信息技术应用创新产业&#xff0c;简称“信创”。 而现有的国产操作系统&#xff0c;虽然已日趋成熟&#xff0c;但因为很多应用软件由国外垄断&#xff0c;因此…

一键获取数百张免费商用人脸!AI人脸生成器来袭

随着科技的发展&#xff0c;人工智能正在渗透到生活的各个角落&#xff0c;设计行业也不例外。在网页、APP、PPT 等界面设计中&#xff0c;设计师经常需要插入真实的人脸素材&#xff0c;以增强作品的真实感和场景化。但是获取素材既不容易&#xff0c;质量和价格也难免成为设计…

PK Nounique CASCADE DROP INDEX keep index

Explicit Control Over Indexes when Creating, Disabling, or Dropping PK/Unique Constraints (Doc ID 139666.1)​编辑To Bottom PURPOSEIn Oracle 9i, the DBA has an explicit control over how indexes are affectedwhile creating, disabling, or dropping Primary Ke…

【网络基础实战之路】基于BGP协议连接三个AS区域的实战详解

系列文章传送门&#xff1a; 【网络基础实战之路】设计网络划分的实战详解 【网络基础实战之路】一文弄懂TCP的三次握手与四次断开 【网络基础实战之路】基于MGRE多点协议的实战详解 【网络基础实战之路】基于OSPF协议建立两个MGRE网络的实验详解 【网络基础实战之路】基于…

网页版的微信客户管理系统是怎样的?

登录账号密码&#xff1a; 微信扫码登录到系统上&#xff1a; 支持多个微信号登录到系统&#xff0c;聚合管理聊天&#xff0c;可以快速查看客服号的聊天记录 群发助手&#xff1a;群发可突破200 朋友圈功能&#xff1a;定时发圈&#xff0c;支持多个微信账号同步发圈、跟圈和一…

恒运资本:投资前瞻:储能产业迎政策密集支持 AIGC有望进入加速状态

昨日&#xff0c;沪指、深成指盘中弱势下探&#xff0c;创业板指冲高回落。到收盘&#xff0c;沪指跌0.49%报3244.49点&#xff0c;深成指跌0.53%报11039.45点&#xff0c;创业板指微跌0.01%报2228.73点&#xff0c;科创50指数跌0.85%&#xff1b;两市合计成交7366亿元&#xf…

STM32 F103C8T6学习笔记4:时钟树、滴答计时器、定时器定时中断

今日理解一下STM32F103 C8T6的时钟与时钟系统、滴答计时器、定时器计时中断的配置&#xff0c;文章提供原理&#xff0c;代码&#xff0c;测试工程下载。 目录 时钟树与时钟系统&#xff1a; 滴答计时器&#xff1a; 定时器计时中断&#xff1a; 测试结果&#xff1a; 测…

echarts3d柱状图

//画立方体三个面 const CubeLeft echarts.graphic.extendShape({shape: {x: 0,y: 0,width: 9.5, //柱状图宽zWidth: 4, //阴影折角宽zHeight: 3, //阴影折角高},buildPath: function (ctx, shape) {const api shape.api;const xAxisPoint api.coord([shape.xValue, 0]);con…

OSCS 闭门研讨第一期实录:软件供应链安全建设价值

2023 年 7 月 18 日晚 19:30&#xff0c;软件供应链安全技术交流群&#xff08;OSCS&#xff09;组织了第一次线上的闭门研讨会&#xff0c;本次研讨会我们收到 71 个来自各个企业关注软件供应链安全的技术专家的报名&#xff0c;根据研讨会参与规则要求&#xff0c;我们对报名…

一文告诉你,Intellij IDEA神器隐藏的11种实用小技巧!

el/2022/10/4 6:58:50 点击上方“程序员大咖”&#xff0c;选择“置顶公众号” 关键时刻&#xff0c;第一时间送达&#xff01; Intellij IDEA真是越用越觉得它强大&#xff0c;它总是在我们写代码的时候&#xff0c;不时给我们来个小惊喜。出于对Intellij IDEA的喜爱&#x…

【uniapp】uniapp打包H5(网页端):

文章目录 一、设置appid&#xff1a;二、设置router&#xff1a;三、打包&#xff1a;【1】[CLI 发行uni-app到H5&#xff1a;https://hx.dcloud.net.cn/cli/publish-h5](https://hx.dcloud.net.cn/cli/publish-h5)【2】HBuilderX 四、最终效果&#xff1a; 一、设置appid&…

W5100S-EVB-PICO 做TCP Server进行回环测试(六)

前言 上一章我们用W5100S-EVB-PICO开发板做TCP 客户端连接服务器进行数据回环测试&#xff0c;那么本章将用开发板做TCP服务器来进行数据回环测试。 TCP是什么&#xff1f;什么是TCP Server&#xff1f;能干什么&#xff1f; TCP (Transmission Control Protocol) 是一种面向连…

芯片常见测试手段:CP测试和FT测试

参考链接&#xff1a; 芯片测试的术语解释&#xff08;FT、CP&#xff09;&#xff0c;持续更新...._ft测试_染不尽的流年的博客-CSDN博客 如何区分芯片cp测试和ft测试 (baidu.com) 芯片常见测试手段&#xff1a;CP测试和FT测试 - 测量仪表 - 电子发烧友网 芯片测试是极其重要…