红黑树Java实现

news2024/10/6 1:40:29

文章目录

  • 红黑树
    • 1. 概念性质
    • 2. 红黑树节点定义
    • 3. 红黑树的插入
      • 情况1
      • 情况2
      • 情况3
      • 其它细节问题
      • 插入代码实现
    • 4. 红黑树的验证
    • 5.性能分析


红黑树

1. 概念性质

红黑树也是一种二插搜索树,每一个节点上比普通二插搜索树都增加了一个存储位置表示节点的颜色,可以是Red或者Black.通过对任何一条从根到叶子的路径上各个节点上色的方式限制,红黑树确保没有一条路径会比其他路径长出2倍,从而得出红黑树是接近平衡的

在这里插入图片描述

红黑树的性质

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

  2. 根节点是黑色的

  3. 如果一个节点是红色的,则它的两个孩子节点是黑色的,红黑树不能有2个连续的红色节点

  4. 对于每个节点,从该节点到其所有后代叶子节点的简单路径上,均包含相同数目的黑色节点

  5. 每个叶子节点都是黑色的(此处的叶子节点指的是是空节点,比如上图的NIL)

通过以上几条性质就能保证最长路径中的节点个数不会超过最短路径节点个数的两倍,因为不能出现两个连续的红色节点,假设有一条路径全是黑色节点,由于每条路径的黑色节点个数是相等的,假设有一种情况是红黑交替,那么全黑节点路径就是最短路径的,而红黑交替路径就是最上路径,就可以保证长路径中的节点个数不会超过最短路径节点个数的两倍

最长路径和最短路径图:

如图最短路径

在这里插入图片描述

2. 红黑树节点定义

enum COLOR {
    RED,BLACK;
}
private static class RBTreeNode {
    int val;
    RBTreeNode parent;
    RBTreeNode left;
    RBTreeNode right;
    COLOR color;

    public RBTreeNode(int val) {
        this.val = val;
        this.left = null;
        this.right = null;
        // 插入节点默认为红色
        this.color = COLOR.RED;
    }
}

3. 红黑树的插入

红黑树插入节点默认应该插入红色的,因为如果默认插入的节点是黑色,因为红黑树的性质4每条路径中的黑色节点数目相同,如果默认插入的节点为黑色就需要新增节点,所以默认插入节点应该为红色

红黑树的插入步骤:

  1. 以二插搜索树的插入方式进行插入新节点
  2. 插入节点后,检测红黑树的性质是否被影响

我们约定cur为插入节点、p为插入节点的父亲节点、g为插入节点的爷爷节点、u为插入节点的叔叔节点

情况1

情况1:cur为红,p为红,g为黑,u存且为红

需要注意的是这里需要分两种情况,一种是看到的是一棵完整的树,另一种是一棵树的子树,此处为抽象图:

在这里插入图片描述

当把cur插入到红黑树当中,因为插入的节点是红色的,此时就不满足红黑树的性质3,所以此时就需要把p和u改成黑色的。

在这里插入图片描述

但是这还没完,假设g不是根节点,这棵树是另外一棵树的子树,那么g还有父节点。有可能g的父节点是红色的,也有可能是黑色的。假设g的父亲节点是黑色的,它的兄弟节点也是黑的。那么此时把p和u修改成的黑色节点,那么路径上的黑色节点就增加了,此时就需要把g修改成红色。那如果g本身就是根节点呢?这个可以放在最后再来处理,处理完所有情况后,不管g是红色和是黑色都把g改成黑色。

在这里插入图片描述

还有一种情况就是如果g父亲的节点本身就是一个红色的节点,如果g的父亲节点是红色的说明其上面肯定还有节点,因为根节点是黑色的,也就是g的父亲节点可定不是根节点。此时就以同样的方式继续向上调整。所以解决方式就是:**将p,u改为黑,g改为红,然后把g当成cur,继续向上调整 **

在这里插入图片描述

情况2

情况2:cur为红、p为红、u不存在或者u为黑

情况2抽象图如下:

在这里插入图片描述

需要注意的是情况2这种抽象图是在红黑树调整过程中产生的,因为它并不遵循红黑树的性质:每条路径上的黑色节点个数一致,那么其实p的右边其实是有黑色节点的,同样cur也应该是黑色节点,cur变成红色就是因为在调整过程中改变了颜色。

具象图如下:

我们在情况1的条件下修改完对应节点颜色后进行向上调整,发现就得到了情况2,所以说情况2是在调整过程中的到的。

在这里插入图片描述

调整情况2第一步就是进行右旋,再修改颜色。

  • 对g节点进行右单旋
  • 修改p的颜色为黑色、修改g的颜色为红色

在这里插入图片描述

此处讨论的是grandFather.left == parent,如果是grandFather.right == parent那么就要是左单旋g节点

情况3

情况3:cur为红,p为红,g为黑,u不存在或者u为黑

情况3也是和情况2类似也是在红黑树的调整过程中产生的,在调整过程中cur变成了红色。抽象图如下:

在这里插入图片描述

再来看一个对应的具象图:

在这里插入图片描述

对应情况3我们先要将p节点进行左单旋。

在这里插入图片描述

对p节点进行左单旋后,发现此时的树节点的颜色情况和情况2非常相似,只是引用不一致。我们回想一下情况2的条件:

  • 情况2:cur为红、p为红、u不存在或者u为黑
  • 对比情况2只是p和cur的指向不一样入下图所示

在这里插入图片描述

所以对于情况3我们只需要左旋后将p和cur的引用进行交换,再以情况2的方式进行处理即可。

还有一种情况grandFather.right == parent,此时的判断又不一样了,此时的条件是cur == parent.left,且是对parent进行右单旋,再修改指向。

其它细节问题

  • 注意每次插入后都要将根节点root的颜色修改为黑色,避免调整时root被修改成红色,从而导致问题
  • 情况2和情况3,要分两种情况讨论
    • grandFather.right == parentgrandFather.left == parent
    • grandFather.left == parent的时候情况2是对grandFather进行右单旋,当grandFather.right == parent的时候情况2是对grandFather进行左单旋
    • grandFather.left == parent情况3判断的是cur == parent.right并且对parent进行左单旋,如果是grandFather.right == parent情况3判断的是cur == parent.left并且对parent进行右单旋
    • 但情况1是不需要区分的

插入代码实现


/**
     * 插入节点
     * @param val
     */
public void insert(int val) {
    RBTreeNode node = new RBTreeNode(val);
    if (root == null) {
        root = node;
        // 根节点是黑色的
        root.color = COLOR.BLACK;
        return;
    }
    // 以搜索树树的方式进行插入
    RBTreeNode cur = root;
    RBTreeNode parent = cur;
    while (cur != null) {
        parent = cur;
        if (cur.val > val) {
            cur = parent.left;
        } else if (cur.val < val) {
            cur = parent.right;
        } else {
            System.out.println("元素: "+val+"+已经存在,插入失败!");
            return;
        }
    }
    if (parent.val > val) {
        parent.left = node;
    } else {
        parent.right = node;
    }
    // 修改指向
    node.parent = parent;
    cur = node;
    //调整红黑树
    // 如果parent为红色说明,parent一定不是根节点
    while (parent != null && parent.color == COLOR.RED) {
        RBTreeNode grandFather = parent.parent;
        if (grandFather.left == parent) {
            RBTreeNode uncle = grandFather.right;
            // 情况1
            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是黑色的
                // 情况3:把情况3修改成情况2
                if (cur == parent.right) {
                    rotateLeft(parent);
                    // 修改引用指向
                    RBTreeNode tmp = cur;
                    cur = parent;
                    parent = tmp;
                }
                // 情况2
                rotateRight(grandFather);
                parent.color = COLOR.BLACK;
                grandFather.color = COLOR.RED;
            }
        } else {
            // grandFather.right == parent
            RBTreeNode uncle = grandFather.left;
            // 情况1
            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是黑色的
                // 情况3:把情况3修改成情况2
                if (cur == parent.left) {
                    rotateRight(parent);
                    // 修改引用指向
                    RBTreeNode tmp = cur;
                    cur = parent;
                    parent = tmp;
                }
                // 情况2:
                rotateLeft(grandFather);
                parent.color = COLOR.BLACK;
                grandFather.color = COLOR.RED;
            }
        }
    }
    // 关键一步,防止向上调整的时候把根节点的颜色给修改了
    root.color = COLOR.BLACK;
}

/**
     * 对grandFather节点进行右单旋
     * @param grandFather
     */
private void rotateRight(RBTreeNode grandFather) {
    // 定义相关节点
    RBTreeNode gLeft = grandFather.left;
    RBTreeNode gLR = gLeft.right;
    RBTreeNode gParent = grandFather.parent;

    // 修改引用
    grandFather.left = gLR;
    grandFather.parent = gLeft;
    gLeft.right = grandFather;
    if (gLR != null) {
        gLR.parent = gParent;
    }

    // 判断g是否是根节点
    if (grandFather == root) {
        gLeft.parent = null;
        root = gLeft;
    } else {
        // 如果不是根节点那么就分时gParent的左节点和右节点
        if (gParent.left == grandFather) {
            gParent.left = gLeft;
        } else {
            gParent.right = gLeft;
        }
        gLeft.parent = gParent;
    }

}

/**
     * 对parent节点进行左旋
     * @param parent
     */
private void rotateLeft(RBTreeNode parent) {
    // 记录对应节点
    RBTreeNode parentR = parent.right;
    RBTreeNode parentRL = parentR.left;
    RBTreeNode pParent = parent.parent;

    // 修改节点
    parent.right = parentRL;
    // 如果parentRL存在
    if (parentRL != null) {
        parentRL.parent = parent;
    }
    parent.parent = parentR;
    parentR.left = parent;
    // 如果旋转的是根节点
    if (parent == root) {
        root = parentR;
        parentR.parent = null;
    } else {
        // 如果旋转的不是根节点就判断旋转的是pParent的左子树还是右子树
        if (pParent.left == parent) {
            pParent.left = parentR;
        } else {
            pParent.right = parentR;
        }
        parentR.parent = pParent;
    }
}

4. 红黑树的验证

根据红黑树的性质编写代码验证:

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

  2. 根节点是黑色的

  3. 如果一个节点是红色的,则它的两个孩子节点是黑色的,红黑树不能有2个连续的红色节点

  4. 对于每个节点,从该节点到其所有后代叶子节点的简单路径上,均包含相同数目的黑色节点

  5. 再对红黑树进行中序遍历验证

/**
     * 判断当前是否红黑树
     * @return
     */
public boolean isRBTree() {
    if (root == null) {
        return true;
    }
    // 1.根节点为黑色
    if (root.color != COLOR.BLACK) {
        System.out.println("根节点不为黑色!");
        return false;
    }
    // 计算某一条黑色节点个数
    int checkNum = 0;
    RBTreeNode cur = root;
    while (cur != null) {
        if (cur.color == COLOR.BLACK) {
            checkNum++;
        }
        cur = cur.left;
    }
    // 中序遍历
    inorderTraversal(root);
    // 红黑树不能有两个连续的红色节点&& 每条路径的黑色节点个数一致

    return checkRedColor(root) && checkBlackNum(root,0,checkNum);
}

/**
     * 判断是否有两个连续红色节点
     * @param node
     * @return
     */
private boolean checkRedColor(RBTreeNode node) {
    if (node == null) {
        return true;
    }
    if (node.color == COLOR.RED) {
        if (node.left != null && node.left.color == COLOR.RED) {
            return false;
        }
        if (node.right !=  null && node.right.color == COLOR.RED) {
            return false;
        }
    }
    return checkRedColor(node.left) && checkRedColor(node.right);
}

/**
     * 判断每条路径上的黑色节点个数
     * @param root
     * @param count
     * @param checkNum
     * @return
     */
private boolean checkBlackNum(RBTreeNode root,int count,int checkNum) {
    if (root == null) {
        return true;
    }
    if (root.color == COLOR.BLACK) {
        count++;
    }
    if (root.left == null && root.right == null && count != checkNum) {
        System.out.println("每条路径黑色节点个数不一致");
        return false;
    }
    return checkBlackNum(root.left,count,checkNum) && checkBlackNum(root.right,count,checkNum);
}
private void inorderTraversal(RBTreeNode root) {
    if (root == null) return;
    inorderTraversal(root.left);
    System.out.print(root.val+" ");
    inorderTraversal(root.right);
}

5.性能分析

红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是 O ( l o g n ) O(logn) O(logn)

  • AVL树通过左旋右旋来保证树的绝对平衡(左右子树的高度差不超过1),所以旋转的次数比较多

  • 红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,通过修改颜色来降低旋转的次数。

  • 红黑树对比AVL树,其通过修改颜色大大减低了旋转的次数,在增删的场景中使用红黑树更优,而AVL树只是适合查找,所以红黑树在实际运用更多的是红黑树,比如说TreeMap和TreeSet。


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

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

相关文章

【lesson10】进程状态

文章目录 认识进程状态新建运行阻塞挂起 Linux具体的进程状态RSDtTXZ是什么为什么 认识进程状态 上面就是各种进程状态&#xff0c;上面都是理论进程状态理论进程状态放在哪个操作系统中都是正确的&#xff0c;但是具体的操作系统实现可能又会有所不同。 下面我们来理解进程状态…

SurroundOcc:用于自动驾驶的多摄像头3D占用网格预测

文章&#xff1a;SurroundOcc: Multi-Camera 3D Occupancy Prediction for Autonomous Driving 作者&#xff1a;Yi Wei, Linqing Zhao, Wenzhao Zheng, Zheng Zhu , Jie Zhou, Jiwen Lu 编辑&#xff1a;点云PCL 代码&#xff1a;https://github.com/weiyithu/SurroundOcc.git…

【AI视野·今日NLP 自然语言处理论文速览 第三十七期】Wed, 20 Sep 2023

AI视野今日CS.NLP 自然语言处理论文速览 Wed, 20 Sep 2023 Totally 64 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Computation and Language Papers SlimPajama-DC: Understanding Data Combinations for LLM Training Authors Zhiqiang Shen, Tianhua Tao, Li…

js惰性函数

看下面这份ts代码 实现的效果也很简单,就是将一份文本,复制到剪切板上,未了兼容更多的浏览器(没错说的就是你>ie !),做了一个兼容性判断, 当浏览器支持navigator.clipboard这个api时,就直接调用这个api将文本复制到剪切板中, 如果不支持这个api的话,就执行else里面的代码,这…

【Kafaka实现高吞吐量、低延迟的底层原理】

文章目录 Kafaka实现高吞吐量、低延迟的底层原理顺序写入Page Cache零拷贝分区分段索引批量读写批量压缩 Kafaka实现高吞吐量、低延迟的底层原理 Kafka虽然是基于磁盘做的数据存储&#xff0c;但却具有高并发、高吞吐量、低延时的特点&#xff0c;其吞吐量动辄几万、几十上百万…

springBoot对接多个mq并且实现延迟队列---未完待续

mq调用流程 创建消息转换器 package com.wd.config;import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.context.annotation.Bean; import o…

深度解析React 18应用性能提升

众所周知,React 18 引入的一个重要特性就是并发功能,从根本上改变了 React 应用程序的渲染方式。本文将带大家一同探讨这些最新功能的具体作用,特别是如何提高应用程序性能。 一、主线程与长任务 当我们在浏览器中运行 JavaScript 时,JS 引擎会在单线程环境下执行代码内容…

Kubernetes基础(二)-Headless Service

1 简介 Headless Service是一种特殊的服务类型&#xff0c;它不会分配虚拟 IP&#xff0c;而是直接暴露所有 Pod 的 IP 和 DNS 记录。这客户端可以直接访问 Pod IP 地址&#xff0c;并使用这些 IP 地址进行负载均衡。 Headless Services是一种特殊的service&#xff0c;其spec…

封装了一个中间放大效果的iOS轮播视图

效果图 计算逻辑 设定在中间展示的size&#xff0c;即正常size&#xff0c;然后设置水平和竖直方向上的margin, 在view的origin和scrollView的contentoffset相等的时候&#xff0c;即 视图处在正中间的时候&#xff0c;最大&#xff0c;然后通过计算其他视图的origin和scrollV…

计算机基础 堆和栈

首先我们需要知道的是栈和堆是两种数据结构 1.栈和堆的定义 栈&#xff1a;是一种先进后出的数据结构&#xff0c;是一种线性结构 堆是一种树形结构&#xff0c;是一颗完全二叉树&#xff0c; 其存储的元素可以通过指针或引用访问 最大堆【大根堆】 &#xff1a;堆中的每一个…

js-cookie使用 js深度克隆(判断引用类型是数组还是对象的方法)

cookie和深度拷贝的使用 1、js-cookie使用2、js深度克隆 1、js-cookie使用 前端的本地存储分为 localstorage、sesstionstorage、cookie 但是咱们有时候需要做7天免登录的需求时&#xff0c;选择 cookie 作为前端的本地存储是在合适不过的了 直接操作 cookie 可以&#xff0c; …

ModbusTCP 转 Profinet 主站网关在博图配置案例

兴达易控ModbusTCP转Profinet网关&#xff0c;在 Profinet 侧做为 Profinet 主站控制器&#xff0c;接 Profinet 设备&#xff0c;如伺服驱动器&#xff1b;兴达易控ModbusTCP 和 Profinet网关在 ModbusTCP 侧做为 ModbusTCP 从站&#xff0c;接 PLC、上位机、wincc 屏等。 拓扑…

Spring事务1+入门案例(简约银行转账)

0、事务基础概念 1.事务角色&#xff1a; 2.事务相关配置 一、配置文件的书写 1.JDBC配置文件 public class JdbcConfig {Value("${jdbc.driver}")private String driver;Value("${jdbc.url}")private String url;Value("${jdbc.username}")p…

win11安装h3c lab无法启动putty终端的解决方法

文章目录 问题记录作者自己的解决方法第一步&#xff1a;安装MobaXterm第二步&#xff1a;修改h3c lab的终端工具 问题记录 win11可以同时安装vm、virtualBox、typer-v安装&#xff0c;因为在最新的系统中已经兼容但是可能出现win11安装h3c lab无法启动putty终端的问题&#x…

数据结构_顺序表_尾插、尾删、头插、头删(附带详解)

文章目录 前言一. 线性表二. 顺序表 - - - 数组2.1 什么是顺序表2.2 顺序表一般可以分为2.2.1 静态顺序表&#xff08;使用定长数组存储元素&#xff09;2.2.2 动态顺序表&#xff1a;使用动态开辟的数组存储2.2.3 顺序表的接口实现 三. SeqList.c 中各个接口的实现。3.1 初始化…

Java实现Modbus Tcp协议读写模拟工具数据

标题 前言一、读写模拟工具中数据(1) 定义Controller层(2) 定义Service层实现 二、调试(1) 读数据(2) 向寄存器写单个数据(3) 向寄存器写多个数据 前言 参考文章&#xff1a;https://www.cnblogs.com/ioufev/p/10831289.html 该文中谈及常见的几种读取设备数据实现&#xff0…

无硬盘的版本 1099,14寸笔记本,而且无硬盘的,特别有有意思,可以自己购买个硬盘,安装linux系统或者windows。

1&#xff0c;千元笔记本&#xff0c;金属外壳 有人进行评测了&#xff1a; https://www.bilibili.com/video/BV1Td4y1K7Cp 1499元的全新笔记本&#xff0c;有什么猫腻&#xff1f; 看了下价格&#xff0c;现在还优惠400&#xff0c;变成了1099。 https://item.jd.com/100851…

Django — 请求和响应

目录 一、请求1、概念2、请求参数方式分类3、案例3.1、URL 路径参数3.2、查询字符串参数3.3、form 表单传参3.4、Json 格式参数3.5、上传文件 二、响应1、HttpResponse2、JsonResponse 三、GET 和 POST 区别1、相同点2、不同点 一、请求 1、概念 请求&#xff08;Request&…

DDR4 眼图测试方法

DDR的全拼是Double Data Rate SDRAM双倍数据速率同步动态随机存取内存。主要就是用在电脑的内存。他的特点就是走线数量多&#xff0c;速度快&#xff0c;操作复杂&#xff0c;给测试和分析带来了很大的挑战。目前DDR技术已经发展到了DDR5&#xff0c;性能更高&#xff0c;功耗…

【算法练习Day4】 两两交换链表节点删除链表倒数第 N 个结点环形链表 II

​ ​&#x1f4dd;个人主页&#xff1a;Sherry的成长之路 &#x1f3e0;学习社区&#xff1a;Sherry的成长之路&#xff08;个人社区&#xff09; &#x1f4d6;专栏链接&#xff1a;练题 &#x1f3af;长路漫漫浩浩&#xff0c;万事皆有期待 文章目录 两两交换链表中的节点一…