高阶数据结构之红黑树

news2024/11/24 20:52:40

文章目录

  • 红黑树
    • 红黑树的性质
    • 红黑树的定义
    • 红黑树的插入
      • 情况一:插入节点的父节点为红,祖父节点为黑,叔叔节点存在且为红
      • 情况二:当前节点的父节点为红,祖父节点为黑,叔叔节点不存在或者为黑
    • 红黑树的验证
      • 验证是否是二叉搜索树
      • 验证是否遵循红黑树的性质
  • 总结红黑树
    • AVL树和红黑树的比较
    • 红黑树的应用

红黑树

        在上一章中我们介绍了AVL树,实际上,前面总结的AVL树是为本章的红黑树做铺垫(因为红黑树中也是涉及到旋转的),除此之外,红黑树上的每个节点也都是带上颜色的(顾名思义:红色&黑色)。此处先简单说说红黑树,在最后总结部分再对AVL树与红黑树两者做一个比较。

        红黑树也是一种二叉搜索树,但特殊的是在每一个节点上都增加了一个存储位来表示节点的颜色,这个颜色可以是红色也可以是黑色。红黑树通过对任何一条从根到叶子的路径上各个节点的颜色进行一个限制,限制后的红黑树会确保没有一条路径会比其他路径长出两倍,因此也是接近平衡的。

红黑树的性质

红黑树的五点性质:

  1. 每个节点的颜色不是红色就是黑色
  2. 根节点是黑色的
  3. 不存在2个连续的红色节点。如果一个节点是红色的,那么它的两个孩子节点一定都是黑色的
  4. 对于每一个节点,从该节点到其所有后代节点的路径上,都是包含相同数目的黑色节点
  5. 每个叶子节点都是黑色的(这里的叶子节点指的是最后的空节点)

例如下面的这张图就是一颗红黑树:
在这里插入图片描述
        通过上面的这5点限制,我们就可以保证没有一条路径会比其他路径长出两倍,原因是当一条路径上的节点全是黑色的时候就是最短路径,而当一条路径要达到黑色节点个数相同并且路径最长的时候,那么这条路径就必然是红黑交替的,既然是红黑交替,那么这条最长的路径就干好事最短路径的两倍。

红黑树的定义

class RBTreeNode{
    public RBTreeNode left;
    public RBTreeNode right;
    public RBTreeNode parent;
    public int val;
    public COLOR color;

    public RBTreeNode(int val){
        this.val = val;
        //新建的节点默认是红色的
        this.color = COLOR.RED;
    }
}

        注意:在定义新节点的时候需要把节点的color值默认定义成RED的,原因是如果直接定义成黑色节点的话,新的红黑树是不满足红黑树的性质4的(对于每一个节点,从该节点到其所有后代节点的路径上,都是包含相同数目的黑色节点)。

红黑树的插入

        红黑树也是在二叉搜索树的基础上一些平衡的限制条件。具体可以分为两步,第一步是按照二叉搜索树的规则将新节点插入到树中;第二步是检查新插入的节点是否破坏了红黑树的性质(一定是要遵循上面红黑树的五点性质的),其中在这一步会出现很多种不同的情况需要分别来进行解决。

情况一:插入节点的父节点为红,祖父节点为黑,叔叔节点存在且为红

在这里插入图片描述
此时又可以分成三种情况:
第一种:此图的g节点就是根节点。 那么只需要将p节点和u节点颜色修改成黑色即可。
在这里插入图片描述
第二种:此图的g节点存在一个父节点且为黑色。 那么此时就不能单单将p和u节点修改成黑色,因为这样会导致每条路径上的黑色节点个数不同,此时我们可以将g节点修改成红色就可以很好的解决这个问题。(拓展:由上面的两种情况我们也可以归纳出我们每次将p和u节点置为黑,将g节点置为红,最后再将树的根节点置为黑,就可以将上面这两种情况合并起来)。
在这里插入图片描述
第三种:此图的g节点存在一个父节点且为红色。 如果出现这种情况,说明这个父节点就一定不是这颗树的根节点(只是这棵树的一个子树),对此,我们依旧可以按照上面的规则进行修改节点颜色,当g节点被改成红色,并且其父节点也是红色(是不符合红黑树性质的),此时再继续往上遍历对这颗红黑树进行修改……直到符合红黑树的性质。
在这里插入图片描述

(注意:以下代码均包含镜像情况)

while(parent != null && parent.color == COLOR.RED){
    RBTreeNode grandFather = parent.parent;  //grandFather不可能是空,因为红色节点必然有父节点
    if(parent == grandFather.left){
        RBTreeNode uncle = grandFather.right;
        if(uncle != null && uncle.color == COLOR.RED){
            parent.color = COLOR.BLACK;
            uncle.color = COLOR.BLACK;
            grandFather.color = COLOR.RED;
            //继续向上修改
            cur = grandFather;
            parent = cur.parent;
        }else{
            //uncle不存在或者uncle的颜色为黑

        }
    }else{
        //parent == grandFather.right
        RBTreeNode uncle = grandFather.left;
        if(uncle != null && uncle.color == COLOR.RED) {
            parent.color = COLOR.BLACK;
            uncle.color = COLOR.BLACK;
            grandFather.color = COLOR.RED;
            //继续向上修改
            cur = grandFather;
            parent = cur.parent;
        }else{
            //uncle不存在或者uncle的颜色为黑
            
        }
    }
}

情况二:当前节点的父节点为红,祖父节点为黑,叔叔节点不存在或者为黑

        在情况二中又会分出来两种情况:一种是当前节点在父节点的左边,一种是当前节点在父节点的右边。如果出现情况二的话,那么说明这一步操作一定不是在插入新的节点时候的,而是在向上调整的过程中出现的,原因是这种情况在还没插入节点之前本身就是不符合红黑树性质的。

当前节点在父节点的左边:
在这里插入图片描述
(注意:cur是在其子树中向上调整后变成红色的,并非插入的新节点。)
        这时候如果只是进行简单的修改颜色,是无法保证cur子树路径黑色节点个数与其他路径上的黑色节点个数一样的,这时候就需要对这棵树进行右单旋+修改颜色(p置为黑、g置为红)。
在这里插入图片描述

当前节点在父节点的右边:
在这里插入图片描述
(注意:cur是在其子树中向上调整后变成红色的,并非插入的新节点。)
        这种情况就变得更加特殊了,既不能只是进行简单的修改颜色,也不能像上面一样右旋解决,这时候如果对这棵树的p节点进行一下左单旋。
在这里插入图片描述
        在进行左单旋之后,可以惊奇地发现,这不就是“当前节点在父节点的左边”的情况吗?唯一的区别就是cur节点与p节点的顺序互换了,当然cur子树部分(未画出来)位置也是更换了的,但是这并不会产生影响(因为也都会是满足红黑树的性质的)。那么在下一步操作的时候,我们就可以交换一个这两个节点的指向并直接使用上一情况的代码即可。

插入操作完整代码:

while(parent != null && parent.color == COLOR.RED){
    RBTreeNode grandFather = parent.parent;  //grandFather不可能是空,因为红色节点必然有父节点
    if(parent == grandFather.left){
        RBTreeNode uncle = grandFather.right;
        if(uncle != null && uncle.color == COLOR.RED){
            parent.color = COLOR.BLACK;
            uncle.color = COLOR.BLACK;
            grandFather.color = COLOR.RED;
            //继续向上修改
            cur = grandFather;
            parent = cur.parent;
        }else{
            //uncle不存在或者uncle的颜色为黑
            //如果当前节点在父节点的右边,则先进行左单旋
            if(cur == parent.right){
                rotateLeft(parent);
                //交换cur和parent
                RBTreeNode tmp = parent;
                parent = cur;
                cur = tmp;
            }
            //不管当前节点在父节点的左边还是右边,都是需要进行右单旋并修改颜色
            rotateRight(grandFather);
            parent.color = COLOR.BLACK;
            grandFather.color = COLOR.RED;
        }
    }else{
        //parent == grandFather.right
        RBTreeNode uncle = grandFather.left;
        if(uncle != null && uncle.color == COLOR.RED) {
            parent.color = COLOR.BLACK;
            uncle.color = COLOR.BLACK;
            grandFather.color = COLOR.RED;
            //继续向上修改
            cur = grandFather;
            parent = cur.parent;
        }else{
            //uncle不存在或者uncle的颜色为黑
            //如果当前节点在父节点的右边,则先进行左单旋
            if(cur == parent.left){
                rotateRight(parent);
                //交换cur和parent
                RBTreeNode tmp = parent;
                parent = cur;
                cur = tmp;
            }
            //不管当前节点在父节点的左边还是右边,都是需要进行右单旋并修改颜色
            rotateLeft(grandFather);
            parent.color = COLOR.BLACK;
            grandFather.color = COLOR.RED;
        }
    }
}

红黑树的验证

        我们前面说过,红黑树其实就是一颗二叉搜索树,只是通过一些规则来限制树的高度尽量平衡,所以我们在对红黑树进行验证的时候,可以分为两步来进行验证:验证这棵树是否是二叉搜索树 & 验证这棵树是否遵循红黑树的性质。

验证是否是二叉搜索树

        我们在验证一棵树是否是二叉搜索树的时候,一般都是使用中序遍历来查看遍历的结果是否是有序的。

public void inorder(RBTreeNode root){
    if(root == null) return;
    inorder(root.left);
    System.out.println(root.val + " ");
    inorder(root.right);
}

验证是否遵循红黑树的性质

        在这一步验证中,我们需要判断的内容是:1.根节点是否是黑色;2.是否存在两个连续的红色节点(判断每个红色节点的父节点颜色即可);3.每条路径上的黑色节点数量是否相同(使用DFS搜索每条路径上的黑色节点是否符合预期即可)。

public boolean isValidRBTree(){
    if(root == null){
        return true;  //空树也是红黑树
    }
    if(root.color != COLOR.BLACK){
        System.out.println("根节点必须是黑色的!");
        return false;
    }
    int blackNum = getBlackNum(root);
    return checkRedColor(root) && checkBlackNum(root, 0, blackNum);
}

//获取其中一条路径上的黑色节点个数
private int getBlackNum(RBTreeNode root) {
    int num = 0;
    while(root != null){
        if(root.color == COLOR.BLACK){
            num++;
        }
        root = root.left;
    }
    return num;
}

//判断每条路径上的黑色节点数量是否相同
private boolean checkBlackNum(RBTreeNode root, int pathBlackNum, int blackNum) {
    if(root == null){
        return true;
    }
    if(root.color == COLOR.BLACK){
        pathBlackNum++;
    }
    if(root.left == null && root.right == null){
        if(pathBlackNum != blackNum){
            System.out.println("每条路径上的黑色节点数量不相同");
            return false;
        }
    }
    return checkBlackNum(root.left, pathBlackNum, blackNum) && checkBlackNum(root.right, pathBlackNum, blackNum);
}

//判断是否出现两个连续的红色节点
private boolean checkRedColor(RBTreeNode root) {
    if(root == null){
        return true;
    }
    if(root.color == COLOR.RED){
        RBTreeNode parent = root.parent;
        if(parent.color == COLOR.RED){
            System.out.println("连续出现两个红色节点!");
            return false;
        }
    }
    return checkRedColor(root.left) && checkRedColor(root.right);
}

总结红黑树

AVL树和红黑树的比较

        AVL树和红黑树都是高效的平衡二叉树,增删改查的时间复杂度都是O(logn)。
        除此之外,红黑树是不追求绝对平衡的,它只需要保证最长路径不超过最短路径的两倍即可(查找的效率虽没有AVL树那么高,但也只是系数的区别,时间复杂度也还是一样的),相对于AVL树而言,降低了插入和旋转的次数,因此在进行插入节点和删除节点的效率是优于AVL树的,而且最重要的一点是,红黑树不会像AVL树那样涉及到大量的旋转代码(左右双旋、右左双旋),实现起来是会比较简单一点的,以及在实际使用的过程中对树插入节点的操作大多数情况下也还是比较多的,所以实际运用红黑树会更多。

红黑树的应用

  • 在Java中TreeMap、TreeSet底层使用的就是红黑树
  • 在C++中map、set、mutil_map、mutil_set底层使用的也是红黑树
  • 在Linux内核中进程调度中使用红黑树管理进程控制块、epoll在内核中实现时使用红黑树管理事件块
  • ……

        最后附上红黑树的完整代码,有需要可以访问此gitee账户(链接)进行参考:红黑树代码。

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

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

相关文章

Linux常用命令——read命令

在线Linux命令查询工具(http://www.lzltool.com/LinuxCommand) read 从键盘读取变量值 补充说明 read命令从键盘读取变量的值,通常用在shell脚本中与用户进行交互的场合。该命令可以一次读取多个变量的值,变量和输入的值都需要使用空格隔开。在read命…

这些预测性维护的专业术语你都了解吗?

一、前言 随着时代的发展,越来越多的企业希望能够在对设备和系统无损的前提下,通过一系列的测试和分析来实现维护。这种维护工作是基于设备和系统本身的运行状态来安排实施的,被称为CBM(Condition Based Maintenance)…

Apache自带压力测试工具—ab

ab压力测试工具: ab全称为:apache bench 我们先来了解一下压力测试的概念: 吞吐率(Requests per second) 概念:服务器并发处理能力的量化描述,单位是reqs/s,指的是某个并发用户数…

【c++】模拟实现vector

一.vector的成员变量与迭代器vector里面可以存各种数据类型,所以必定会用到模板,假设vector的模板参数为T,那么T*这个指针封装后就是vector的迭代器和string的迭代器很像,只不过string确定存的是字符,所以迭代器直接是…

ubuntu20.04网络配置

安装net-toolssudo apt-get install net-tools2、ifconfig查看网卡设备其中flags表中:running表示正在使用中。查看设备核心网络路由表:route -nDestination目标网段或者主机Gateway网关地址,”*” 表示目标是本主机所属的网络,不…

【Vue2+Element ui通用后台】用户列表

文章目录新增用户用户列表新增用户 首先增加一个 ‘新增’ 按钮,点击弹出对话框来新增用户。弹出框可以使用 Element UI 的 Dialog对话框,其中 visible 表示是否显示 Dialog,支持 .sync 修饰符。我们点击新增按钮把这个标识置为 true&#x…

金山系不惧微软,前有WPS力扛Office,后有eversheet接力再战

金山软件作为国产互联网元老企业,这里出来的IT大佬不计其数,小米的雷军、逸趣网络的吴裔敏、甜瓜在线的朱勇......金山软件不屈不挠的企业文化,沉淀已久,造就一大批人才,甚至追溯到40年前。 1981年,张旋龙在…

IntelliJ IDEA 闪退的解决办法

场景 最近这idea闪退频率又多了不少 以前 几天一闪退 现在 一天N多次闪退 如下图 看这崩溃日志 这怎么顶 解决办法 查看崩溃日志 日志 1 日志2 日志3 可以看出现在生效的参数 Command Line: -Xms128m -Xmx750m -XX:ReservedCodeCacheSize512m -XX:IgnoreUnrecognized…

SCA技术进阶系列(一):SBOM应用实践初探

现代软件都是组装的而非纯自研。随着开源组件在数字化应用中的使用比例越来越高,混源开发已成为当前业内主流开发方式。开源组件的引入虽然加快了软件开发效率,但同时将开源安全问题引入了整个软件供应链。软件组成成分的透明性成为软件供应链安全保障的…

Flink检查点详解

说白了就是等你要处理的这个或这波数据被所有任务(执行完所有算子)处理完了 再做检查点保存(下图就是三个数据都被map、sum处理完 就做检查点保存 source是读取数据的) 下图只是一个检查点的保存过程(拆解&#xff09…

Express框架中JWT基础 - 对称|非对称加密

在上一篇内容当中已经使用过了JWT(JSONWebToken)做验证登录,采用的是对称加密的方式,那么在本篇当中来进一步的讲解关于JWT的基础使用对称以及非对称加密;先来简单的回顾上一篇内容当中使用到的对称加密: 对称加密 首先是通过expr…

Kafka架构组成及相关内容

0. 主要参考:1. Kafka基础架构组成:2. Kafka的一些操作命令:3. Kafka 生产者消息发送流程:4. Kafka 的ack机制:5. Kafka 生产者消息发送模式(同步/异步):6. Kafka发送消息的分区策略…

元宇宙之声:nspace

nspace 行政总裁为我们介绍他在元宇宙中的最新创作以及对 2023 年的愿景。 本期节目我们邀请了 nspace 行政总裁 Ethan Liu 分享他的 The Sandbox 之旅以及他们的最新创作。 可以告诉我们更多关于 nspace 的信息吗? nspace 是一家专注于开发新的元宇宙商业模式的初创…

html 拖拽事件详解

为了使元素可拖拽,需要在标签上设置draggabletrue属性。 文本、图片和链接是默认可以拖放的,它们的draggable属性自动被设置成了true。 图片和链接按住鼠标左键选中,就可以拖放。 文本只有在被选中的情况下才能拖放。如果显示设置文本的dr…

LinkedIn工具-领英精灵参数怎么设置?

前言: 领英精灵是高端技术人员针对领英平台研发的工具。具有好友分组、备注,一键批量加-好友,批量撤-回邀请,批量群-发消息,批量导出好友资料,批量点-赞、Groups管理七大功能。通过领英精灵可提高领英开发…

Java集合进阶 | Collection接口

本专栏主要是记录学习完本专栏主要是记录学习Java中的知识点,如果刚开始学习Java的小伙伴可以点击下方连接查看专栏 JavaWeb:🔥JavaWeb Java入门篇: 🔥Java基础学习篇 Java进阶学习篇(持续更新中&#xff0…

Three.js入门以及案例(全方位解析)

下载three.js 压缩包 github链接查看所有版本 threejs:https://github.com/mrdoob/three.js/releases 下载即可 常用的文件目录 three.js-文件包 └───build——three.js相关库,可以引入你的.html文件中。│ └───docs——Three.js API文档文件│…

【机器学习算法】模型评估 “神经网络,聚类,向量机,关联规则”算法模型的评估。

模型评估* 数据集的切割 训练-测试数据的方式、交叉验证的方式 我们通常会把数据集切割为训练数据集或者测试数据集,训练数据集用来训练模型用,测试数据集我们一般用来测试模式的实际效能怎么样。 我们在将数据分为训练和测试数据集的时候我们会使用…

go-zero使用consul作为注册中心

目录 在rpc服务中添加配置 导入包: 在rpc服务中添加配置: 引入 Consul config 配置项 user.yml 文件 修改 user.go,将 rpc注册到consul rpc的发现 在api服务中添加配置: 修改api/etc/user.yam 文件 修改 user.yml 修改api/user.go …

@Import的用法

官方定义: https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#spring-core Using the ImportAnnotation Much as the <import/> element is used within Spring XML files to aid in modularizing configurations, the Import annotat…