《算法4》深入理解红黑树

article/2025/8/6 10:48:18

红黑树是一种性能非常优秀的数据结构,关键在于它能保证最坏的性能也是对数的,主要是因为它是一种平衡的树,所以也叫平衡查找树。要理解红黑树,最好先看看我的上一篇博客《算法4》符号表以及二叉查找树,了解二叉查找树以及我们为什么需要平衡查找树。

2-3查找树

二叉查找树中树高会受到输入数据的影响,极端情况下一棵树和一个链表没什么区别,所以我们需要一种树,它的所有叶节点到根节点的距离都是相等的,这种树为平衡树,并且随着数据的加入,这种平衡性会一直保持。下面介绍一种理论上的平衡树——2-3查找树。

这里写图片描述
2-3树示意图

2-3树的主要特点就是树由普通的2节点和一种三节点组成。2节点和二叉查找树中的特性相同。对于3节点,它的键有两个,并且有三个链接,左链接指向的左子树中的所有元素都小于3节点中的两个键,中间链接指向的子树大小在两个键之间,右链接指向的子树中的元素都大于两个键。

查找

对于查找操作和二叉查找树基本相同,递归比较要查找的键和树的根节点,小于就向左继续查找,大于就向右查找,相等就查找命中。不同的就是对于3节点,还要有中间节点的情况,在三节点两个键的大小之间的情况,要向中间子树递归。最后如果查找到叶节点的空连接就直接返回null。

插入

2-3树的插入相对来说是比较复杂的,因为它是保证树本身平衡性的关键。我们分几种情况来论述。

向2节点插入

插入先是要查找,查找到了就直接更新,如果未命中就插入新元素,插入新元素一定是在叶节点的空连接上,如果叶节点是一个2节点,那么就直接插入,让特们合成一个3节点。显然这时树高没有变化。示意图如下
这里写图片描述
这就是向一个2节点插入的情况。

向3节点插入

这里写图片描述
假如向一个3节点中插入,我们首先可以做的是像2节点一样把他们暂时合在一起形成一个4节点(有三个元素),然后再对这个4节点进行分解,将中间的元素插入他们的父元素剩下两个元素变成两个2节点。注意:只能是中间的元素拿上去,因为这样才能保证树的有序性,即左边和右边的元素相对于根元素的大小关系,然后再考察父节点,如父节点原来是一个2节点,那么此时直接插入将其变成一个3节点,插入操作就完成了。如果原来父节点就是一个3节点,那么依然可以再重复这个过程,不断将中间元素加入父节点,如果这个过程持续到了根节点,那么我们就分裂形成的一个暂时的4节点的根节点,得到三个2节点,同时整个的树高增加1。这里写图片描述
上图为根节点分裂的示意图

这种插入相当于把元素插入这个会引起树的高度变化的不利因素放在3节点中储存起来,随着3节点的插入将这种不利因素不断传导至根节点,然后通过根节点的分裂将整个的树高加1,可以看出,3节点以及相关的插入方法是保证平衡性的关键,也可以看出2-3树的生长是从下往上通过根节点生长的。2-3树就可以实现在最坏条件下也有对数性能。

下面我们就可以看到一种2-3树的具体实现——红黑二叉查找树,以下简称红黑树。

红黑树

前文已经提到3节点是实现平衡性的关键,这里我们用红链接即一条红色的左链接来表示3节点
这里写图片描述
而2节点就用普通的黑色连接来表示。
那么一棵红黑树应该是完美黑色平衡的,即从任意空连接出发到根节点所经历的黑连接数目应该是相同的。再加入一个条件:没有任意一个节点同时和两个红链接相连,那么此时红黑树就可以和2-3树一一对应。

节点代码

private static final boolean RED = true;//定义RED为trueprivate static final boolean BLACK = false;private Node root;private class Node{Key key;Value value;int N;Node left, right;boolean color;//表示颜色 Node(Key key, Value value,int N,boolean color){this.key = key;this.value = value;this.N = N;this.color = color;}}private boolean isRed(Node x){if (x==null) return false;return x.color == RED;}

我们在这里加入了一个表示颜色的布尔变量。这里的一个关键是一个节点的颜色指的是指向这个节点的连接的颜色。

旋转

旋转是一项非常重要的操作。我们在不改变树的有序性的情况下,将某个红链接从左链接变成右链接,或者从右链接变成左链接,这在处理一些情况比如对应于2-3树中向3节点插入元素的时候,更新整个树是很有用的。
这里写图片描述 这里写图片描述

代码在图中显示的有,所以不再重复写一遍。
还有个flipColors()的操作
这里写图片描述
可以看出这个函数就对应于2-3树中将中间元素插入父节点的操作,因为它把原来的两条红链接变成黑链接,相当于分裂成了两个2节点,而中间元素因为颜色是红的,所以就加入了父节点。flipColors()在后面还有其他的作用,所以在这里我给出它的最终形式。

private void flipColors(Node h){h.color = !h.color;h.left.color = !h.left.color;h.right.color = !h.right.color;}

其实就是对颜色求反,这种形式显然是包容上图中的那种形式的。
注意:根节点都是黑色的

查找

红黑树的查找算法和二叉查找树的查找算法是完全一样的,也就是说,对于查找算法来说,红黑树中节点或者说链接的颜色是没有用到的,但是没有关系,虽然红黑树只是黑链接平衡,但是即使不考虑颜色的查找,整个树也不会出现像二叉树里面那种最极端的情况,所以性能依然是有保障的。

插入

红黑树的插入的算法是比较复杂的,对于2-3树来说相对较简单,但是在具体实现的时候,每个3节点中是有着具体结构的,那么我们在插入后就要调节这些具体的结构,才能实现2-3树中的功能。

向2节点插入

在2-3树中向2节点插入非常简单,直接合并成一个3节点就行。但是具体实现时,因为相对于父节点可能有大有小,那么在插入的时候就可能在父节点的左边或者右边,而红链接只能是左链接,那么当在右边插入的时候,就需要进行旋转操作将右链接变成左链接。

向3节点插入

向3节点插入就更加复杂了,因为此时不仅有插入方向的问题,还有父节点也是红色的问题,我们要调整几个节点的结构,实现2-3树中将中间节点插入到父节点的操作。这里主要分三种情况。

1

如果插入后一个节点的两个子节点都是红色的,那么我们通过flipColors()可以很容易的实现2-3树中将中间节点插入父节点,两边节点独立成两个2节点,同时保持有序性(这里默认中间节点是黑色的,因为默认在插入之前整个树是有序的,这个可以通过正确的插入来保证)。

2

第二种情况下,需要先将第一个红链接进行右旋转,这样就变成了第一种情况,可以按照情况1 处理

3

第三种情况下,需要先将下面的红链接进行左旋转就变成了第二种情况,然后就可以按照第二种情况处理
这里写图片描述
上图从左到右分别对应1,2,3三种情况。通过处理以上三种情况,我们就可以将红黑树的插入和2-3树的插入算法一一对应起来。下面是插入的代码。

public void put(Key key,Value value){root = put(root,key, value);
//查找键值,找不到就新建一个root.color = BLACK;}private Node put(Node h,Key key, Value value){if (h==null) return new Node(key, value, 1, RED);int cmp = key.compareTo(h.key);if   (cmp<0) h.left = put(h.left, key, value);
//递归查找else if   (cmp>0) h.right = put(h.right, key, value);else     h.value = value;if (isRed(h.right) && !isRed(h.left)) h=rotateLeft(h);if (isRed(h.left) && isRed(h.left.left)) h=rotateRight(h);if (isRed(h.left) && isRed(h.right))    flipColors(h);h.N = size(h.left)+size(h.right)+1;
//更新走过的节点的N值return h;}

这里面值得注意的一点就是那三行if条件句,因为是放在递归语句之后的,所以是相当于沿着树往下走到底或者找到相等值,处理完再返回的时候运行的,可以看到这三句刚好可以将情况三处理完成,同时也容易检验,这个语句是完全可以兼容前两种情况。所以不断再返回根节点的过程运行这三句,相当于2-3树中把可能的多余节点移到。节点的过程。最后树是平衡的。

删除最小值最大值和删除

删除比较麻烦,我们先考虑删除最小值,当我们删除一个3节点中的元素的时候倒还好,直接删除之后留下了一个2节点,树的平衡性没有发生变化。但是直接删除2节点会造成树的高度的变化。所以,我们还是要处理一下,从上往下进行变换,最终的目标就是保证在删除的时候当前节点不只是一个2节点。

删除最小值

最小值在最左边,我们沿着左边下去的时候需要合并三个2节点形成一个4节点,或者右边是三节点的话从右边节点“借”一个形成一个3节点或者4节点,这样就能保证当前节点大于2节点
这里写图片描述

private Node moveRedLeft(Node h){//这个函数是用来处理2节点的flipColors(h);//把上面的节点"拉下来",形成一个大节点if (isRed(h.right.left)){//h.right = rotateRight(h.right);h = rotateLeft(h);flipColors(h);//注意!!!《算法4》书中这一章的习题中的代码缺少这一行,这一行代表借了一个节点之后,再还一个给父节点。否则我们就连着兄弟节点一起变成一个大节点了。}return h;}public void delMin(){if (!isRed(root.left) && !isRed(root.right))root.color = RED;root = delMin(root);if (!isEmpty()) root.color = BLACK;}private Node delMin(Node h) {if (h.left == null) return null;if (!isRed(h.left) && !isRed(h.left.left))//意味着h的左子节点为一个2节点h= moveRedLeft(h);h.left= delMin(h.left);return balance(h);}

其中balance()函数就是在返回的时候解开临时的4节点,使整个树再次平衡。代码如下,就是在上面的put()函数里面的三行if条件句前面加上一句话,其实对于为什么加这一句我不是很理解,开始以为是因为删除最小值之后h.left为null,但是isRed(null)返回的是false,那么就感觉这一句话和下面几句功能上是重复的,并且在实验的时候,删去这一句话例子也能正确输出。

private Node balance(Node h){if (isRed(h.right)) h = rotateLeft(h);if (isRed(h.right) && !isRed(h.left)) h=rotateLeft(h);if (isRed(h.left) && isRed(h.left.left)) h=rotateRight(h);if (isRed(h.left) && isRed(h.right))    flipColors(h);h.N = size(h.left)+size(h.right)+1;return h;}

删除最大值

删除最大值和删除最小值类似,但是因为红链接都是左链接,所以代码有所不同。

private  Node moveRedRight(Node h){//功能和moveRedRight()类似,但是方向是向右的flipColors(h);if (isRed(h.left.left)){h=rotateRight(h);flipColors(h);}return h;}public  void delMax(){if (!isRed(root.right) && !isRed(root.left))root.color = RED;root = delMax(root);if (!isEmpty()) root.color = BLACK;}private Node delMax(Node h){if (isRed(h.left))h=rotateRight(h);//保证方向的一致性if (h.right == null)return null;//找到最右的节点就删除。if (!isRed(h.right) && !isRed(h.right.left))h= moveRedRight(h);//右子节点为2节点的话就运行这个函数将其变成至少3节点h.right = delMax(h.right);return balance(h);}

真正的删除函数就用到了delMin()函数

public void delete(Key key){if (!isRed(root.right) && !isRed(root.left))root.color = RED;root = delete(root,key);if (!isEmpty()) root.color = BLACK;}private Node delete(Node h, Key key){if (key.compareTo(h.key)<0){if (!isRed(h.left) && !isRed(h.left.left))h= moveRedLeft(h);h.left = delete(h.left, key);}else {if (isRed(h.left))h= rotateRight(h);if (key.compareTo(h.key)==0 && (h.right==null) )return null;if (!isRed(h.right) && !isRed(h.right.left))h= moveRedRight(h);if (key.compareTo(h.key) ==0){h.value = get(h.right, min(h.right).key);h.key = min(h.right).key;h.right = delMin(h.right);}else h.right = delete(h.right, key);}return balance(h);}

我们来考察删除的代码。首先对于public 的 delete()中的变换根节点的颜色的行为,我也不是很了解,似乎没有这个必要,书中也只是简单的在答疑这个部分说了下,但也不甚明了,我试了下不改变颜色例子似乎也能运行。

在下面的私有的delete()函数中,首先也是查找,小于根节点就在左子树中继续查找,这中间,遇到2节点要用moveRedLeft()函数构造出临时的3或者4节点,方便最后删除。大于或等于根节点的情况下,但是可以看出,在这一块代码中,前三句基本就是delMax()中的,这是因为沿着右子树下降的过程中,我们依然要保证当前节点不是2节点,而这也是delMax()所要求的。最后一个条件句很好理解,就是我们找到了目标键,但是这个键不是叶节点,那么我们就把这个键的右子树的最小节点的键和值都赋给它,然后删除右子树的最小值,这样就删除了目标键并且整个树还是有序的,平衡的。

分析

红黑树几乎是完全平衡的,但是我们在查找的时候完全没有用到颜色这个性质,那么所谓的红黑到底有什么作用?我觉得颜色是起到动态调整的作用的,在二叉树中最极端的情况是以有序的方式插入元素,,最后得到的树高就等于插入的元素个数N,但是这在红黑树中不会发生,读者可以试验一下,即使以有序的方式插入,那种最坏的情况也不会发生,一棵大小为N的红黑树高度不会超过 2lgN ,平均高度为  1.00lgN 。这是因为如果某一条路径全部都是3节点的话,那么这条路径最大为 2lgN 。究其原因我觉得是新节点插入的时候都是红色的,以及任意一个节点都不可能和两条红链接相连的原因。


http://chatgpt.dhexx.cn/article/T7dYfnht.shtml

相关文章

【算法4总结】第四章:图

目录备份 第四章&#xff1a;图 概述 图可以根据是否有向和带权分成以下四种&#xff1a; 无向图 &#xff08;无向不带权&#xff09;有向图 &#xff08;有向不带权&#xff09;加权无向图&#xff08;无向带权&#xff09;加权有向图&#xff08;有向带权&#xff09; …

算法4(一、递归学习)

每次用递归都感觉有点难&#xff0c;这个趁着恶补基础知识的时候&#xff0c;专门看了一遍递归&#xff0c;算法4的。 1.1 递归介绍 方法可以调用自己&#xff0c;例如&#xff1a;下面给出了bin_search的二分查找的一种实现。&#xff08;算法4中使用的是Java&#xff0c;但…

【算法4总结】第一章:基础

目录备份 第一章&#xff1a;基础 我认为这一章主要介绍的是如何使用工具。 一共五节&#xff0c;前两节主要是对 Java 语法的回顾&#xff0c;第三节则是三个数据结构&#xff0c;背包&#xff0c;队列和栈的API讲解。 而第四节是讲解的是如何分析算法。第五节则是针对具体…

SQL修改语句

如果我们要修改数据库中表的数据&#xff0c;这个时候我们就要使用到UPDATE语句。 UPDATE语句的基本语法是&#xff1a; UPDATE <表名> SET 字段1值1, 字段2值2, ... WHERE ...; 例如&#xff0c;我们想更新employees表id100的记录的last_name和salary这两个字段&…

【数据库】SQL语句之修改语句(INSERT,UPDATE,DELETE)

1.INSERT INSERT INTO <表名> (字段1, 字段2, ...) VALUES (值1, 值2, ...); 例如&#xff1a; 一次插入一个 INSERT INTO students (class_id, name, gender, score) VALUES (2, 小明, M, 80);一次插入多条 INSERT INTO students (class_id, name, gender, score) VA…

SQL Server修改数据

本篇主要讲解的是SQL Server 中修改数据的几种语句&#xff1a; INSERT语句INSERT INTO SELECT语句UPDATE语句DELETE语句 一&#xff1a;INSERT语句 INSERT语句向表中添加新行&#xff0c;以下是INSERT语句的最基本形式&#xff1a; 首先&#xff1a;table_name指定要插入的…

使用SQL语句修改表数据

使用SQL语句修改表数据 文章目录 使用SQL语句修改表数据利用INSERT语句输入数据利用UPDATE语句更新表数据利用DELETE语句删除表中数据利用Truncate Table语句删除表中数据 利用INSERT语句输入数据 INSERT语句的基本语法格式如下&#xff1a; 上述格式主要参数说明如下&#xf…

初探POC编写

文章目录 前言什么是POC什么是 ExpPOC注意事项尝试编写第一个POCpikachu sql盲注poc参考 前言 想锻炼一下编程能力&#xff0c;师兄说以后很重要的&#xff0c;最好学好一点 但是我又想学习安全相关的&#xff0c;那就来练练poc吧 什么是POC PoC(全称: Proof of Concept), …

POC_MeterSphere-RCE

MeterSphere-RCE 漏洞详情影响范围指纹- fingerPOC-YAML《飞致云MeterSphere开源测试平台远程代码执行漏洞》 漏洞详情 MeterSphere一站式开源持续测试平台存在的远程代码执行漏洞。由于自定义插件功能处存在缺陷,未经身份验证的攻击者可利用该漏洞在目标系统上远程执行任意…

【POC---概念验证】

文章目录 前言一、Proof of Concept是什么&#xff1f;验证内容 PoC测试工作准备前提PoC测试工作参与者PoC测试工作准备文档PoC测试工作第一阶段 工作启动第二阶段 产品宣讲及现场集中测试第三阶段 技术测评第四阶段 间歇性测试工作 第五阶段 商务验证第六阶段 背书归档、分析总…

POC_Jenkins

简介 Jenkins是一个开源软件项目,是基于Java开发的一种持续集成工具,用于监控持续重复的工作,旨在提供一个开放易用的软件平台,使软件项目可以进行持续集成,Jenkins用Java语言编写,可在Tomcat等流行的servlet容器中运行,也可独立运行。通常与版本管理工具(SCM)、构建工…

网络安全中的POC、EXP、Payload、ShellCode_网络安全payload是什么意思

什么是 POC、EXP、Payload&#xff1f; POC&#xff1a;概念证明&#xff0c;即概念验证&#xff08;英语&#xff1a;Proof of concept&#xff0c;简称POC&#xff09;是对某些想法的一个较短而不完整的实现&#xff0c;以证明其可行性&#xff0c;示范其原理&#xff0c;其…

Zendstudio 9.0.2 安装Aptana3 并且配置 jQuery

原文地址:http://my.oschina.net/feek/blog/64517 aptana-javascript-jquery.ruble文件夹下载地址: http://dl.dbank.com/c04bfgbchz 一直在用zenstudio9&#xff0c;有时候又需要用到jquery等插件辅助制作前台效果&#xff0c;想安装个Aptana3插件&#xff0c;但是查了好多网…

elfk

1. 2. ​​​​​​​ 3. 4. 5.

c/c++读写文件(c方法篇)

读写文件操作 C语言方法 参考博客&#xff1a;函数基本介绍&#xff1a; https://www.cnblogs.com/lidabo/p/6813354.html 自己写了个demo测试一下fopen、feek、fwrite、fread函数的使用&#xff0c;代码如下&#xff1a; void OperationFile_C() {FILE* fp NULL;fp fopen(…

EFLFK

目录 zookeeper概述 zookeeper 定义 Zookeeper工作机制 Zookeeper特点 Zookeeper数据结构 Zookeeper 应用场景 Zookeeper选举机制 第一次启动选举机制 非第一次启动选举机制 部署 Zookeeper 集群 准备 3 台服务器做 Zookeeper 集群 安装前准备 关闭防火墙 安装 J…

fseek函数c语言_使用示例的C语言中的fseek()函数

fseek函数c语言 C中的fseek()函数 (fseek() function in C) Prototype: 原型&#xff1a; int feek(FILE *stream, long int offset, int origin);Parameters: 参数&#xff1a; FILE *stream, long int offset, int originReturn type: int 返回类型&#xff1a; int Use o…

c语言feek函数读取中文出现乱码

c语言feek函数读取中文出现乱码 在文件操作的学习中,发现读取文件的中文时会出现乱码 当输入的文字改成英文时则不会出现乱码,于是猜想是否和中文与英文占用的字节有关系,实践得出结论,的确是字节搞的鬼,那么有趣了,如果恰好文件指针指向中文字节的一半时,这个指针指向…

vue element-ui自定义表头,动态添加表头,新增行、新增列、删除行、删除列

vue element-ui表格怎样自定义表头&#xff0c;动态添加表头&#xff0c;新增行、新增列、删除行、删除列 需求描述1.自定义表头&#xff0c;表头里插入输入框2.默认初始化几行几列占位3.新增行4.新增列5.右键点击行&#xff0c;进行删除行6.右键点击表头&#xff0c;进行删除列…

sql delete删除列_现有表操作中SQL DELETE列概述

sql delete删除列 In this article, we will explore the process of SQL Delete column from an existing table. We will also understand the impact of removing a column with defined constraints and objects on it. 在本文中,我们将探讨从现有表中删除SQL列的过程。 …