红黑树完全攻略

1. 前言

这次重新看《后会无期》的时候,反复思考了「听过很多道理,依然过不好这一生」这句话的正确性。反复思考的问题有三个:

  • 道理是否适用于自己? —> 道理是所有通用场景下,抽象出来的共性,不具有个体特征。

  • 什么场景下道理是成立的?—> 道理成立的场景是什么。

  • 如何定义好的标准? —> thinking

注:「一分耕耘一分收获」只有在绝对理想的情况下才能成立,在气候、价格有任何波动的情况下都不会完美的呈现正比例关系。

2. 旋转

为什么要写个前言 ,是因为笔者突然意识到思考是比做更重要的事情。99% 写红黑树文章介绍的,都告诉你要做旋转,要左旋、要右旋。但请记得所有不告诉你理由,但是要求必须这么做的都是在「耍流氓」。

2.1 为什么旋转

问:红黑树是一种什么树?

答:平衡二叉搜索树,提取定语平衡二叉搜索树

  • 二叉搜索树:左孩子比根小,右孩子比根大,详情请戳链接。
  • 平衡二叉搜索树:在满足二叉搜索树性质的基础上增加叶结点的高度差不超过 1 的限制,详情请戳链接。

2.2 基础旋转

假设现在只存在三个节点,插入可选方式如下:

  • 根 -> 左 -> 右 (未违反平衡的性质)
  • 根 -> 左 -> 左、根 -> 左 -> 右、根 -> 右 -> 右、根 -> 右 -> 左 ( 违反平衡性质

违反性质的四种情况如下图所示,在满足平衡二叉搜索树的性质的情况下,只有两种旋转是合法的:
在这里插入图片描述
问:如何将不合法的旋转变的合法化呢?

既然只有 根 -> 左 -> 左、根 -> 右 -> 右 这两种情况可以做旋转,那么先定两个小目标:

  • 将 根 -> 左 -> 右 变成 根 -> 左 -> 左
    • 左旋可解:根的右孩子变为根,根的变为根的左孩子
  • 将 根 -> 右 -> 左 变成 根 -> 右 -> 右
    • 右旋可解:根的左孩子变成根,根变成根的右孩子

进行上述步骤的调整后,所有情况都变为可做旋转的两种情况,然后继续旋转就好啦。
在这里插入图片描述
喜大普奔,现在所有可枚举的插入情况,均能够平衡化。

2.3 复杂旋转

上面的基础旋转是所有节点只存在一个孩子的情况,那如果有两个孩子要怎么旋转呢?

这里其实不必死记硬背,记住「出来混迟早要还的」就好。以右旋为例:

  • 根 10 会变成左孩子 8 的右孩子,那么根 10 的左孩子现在处于缺失状态
  • 左孩子 8 的右孩子会变成根 10 ,那么跟 8 的右孩子 9 会处于游离状态
  • 咦,这里直接把游离的右孩子 9 跟变为根 10 的左孩子,一起就又会恢复平静了
    在这里插入图片描述

3. 红黑树的性质

在这里插入图片描述

  1. 节点是红色或黑色。
  2. 根是黑色。
  3. 所有叶子都是黑色(叶子是NIL节点)。
  4. 从每个叶子到根的所有路径上不能有两个连续的红色节点。
  5. 从任一节点到其每个叶子的所有不包含重复点的路径都包含相同数目的黑色节点。

注:

请读 10 遍,然后死记硬背下来(ps 人家就是这么规定的,我也不知道为啥?就好像 red 和 black 为啥这么拼写一样……

红黑树并没有要求绝对的平衡性

4. 插入

敲黑板划重点,虽然性质是人家规定的,但是想法是自己的不是。

请不要先想任何性质上的规则,要想下现在面临的问题:

  1. 每次插入的节点是红色好还是黑色好呢?
  • 做为一个成熟的年轻人,我猜你一定会选红色,为什么呢?因为黑色的话,你每插入一个节点都会违反性质 5 ,也就意味着你每次都必须做旋转。

    注:看了这么久,思考个问题吧 —— 你说是木棍打头痛,还是铁棍打头痛呢?(ps 单纯皮一下,因为我盲猜你一定困了……

  1. 先想下插入的可枚举情况有:
  • 插入根节点 : 置该节点为黑色,无需调整
  • 插入非根节点:
    • 该节点的父亲是黑色,无需调整
    • 该节点的父亲是红色,需要调整,那怎么调整呢?
      • 简单将父亲节点置为黑色可以嘛( ps 太傻太天真,人家可是红黑树啊,怎么会这么简单呢……

4.1 插入论证

上述两种简单的插入情况,笔者就不写了,我确信以及相信你们肯定都能理解清楚。

先看下 too young too simple too naive 的想法为什么不行,毕竟笔者是个讲道理的人。
在这里插入图片描述
让我们先想一想矛盾点是什么

  • 为了满足性质 4 ,需要将插入节点的父节点置为黑色,但能推导出来的结论是:

    • 所有以该父节点为根的子树仍满足红黑树性质
    • 所有从根节点经过该父节点的路径,黑色节点个数都会加 1,造成违反性质 5

    注: 这段有点绕,但其实就是上图的翻译,理解不清楚的话可以看图多想一会会

划重点了,在红红是父子关系的时候,我们不能随意的将父节点置为黑色,即不能随意将某条路径上黑色节点的个数加 1 ,要秉承着「一荣俱荣,一损俱损」的观点,翻译过来就是:

  • 左边黑节点个数加 1 可以,右边你也得给我加 1
  • 左边黑节点个数减 1 可以,右边你也得给我减 1

4.2 复杂插入

知易行难,所以该怎么做的呢?别急,让我们在思考下,我们现在能够判断出来的内容:

  • N 和父亲 P 均为红色,在红红不为父子的限制下,N 的祖父一定为黑色
  • N 的叔叔 U 可红色可黑色

又到了枚举情况的环节,所有可列举情况如下

4.2.1 U 为红色的场景

核心将祖父节点 G 的颜色跟他的孩子 P 、U 颜色互换。
在这里插入图片描述

注: 这里设置 G 为红色,需判断 G 的父亲是否为红色,若是,则需要递归跳转,若否,则整棵树满足性质。

4.2.2 U 为黑色的场景

4.2.2.1 插入节点 N 在 P 的左边(符合可右旋的条件)
在这里插入图片描述
思考:为什么步骤 2 是右旋而不是将 G 节点的父亲置为黑色呢?

  • 笔者认为原因有两个:
    • 设置一个节点为黑色,必定会违反性质 5 ,导致整棵树都要做旋转。
    • 父子节点交换颜色后,右旋就可以直接使得该子树满足红黑性质,不必对整棵树旋转。

4.2.2.2 插入的节点 N 在 P 的右边(不符合右旋条件)

没有条件也要创造条件,办法总比困难多

  • 先左旋,为后面的右旋打基础 —> 转化为 4.2.2.1 的情况
  • 交换父子颜色 + 右旋,所有性质都满足啦
    在这里插入图片描述

4.3 复杂插入总结

在这里插入图片描述
注: 在 U 为非红色,以及插入节点 N 在 P 节点右侧的情况下是最复杂的,如果没看懂怎么办呢?亲亲,这边建议您多看两遍哟……

5. 删除

絮絮叨叨的笔者终于把插入写完了,但是明显删除才是更难的啊,那咋办呢?

不妨不妨,来日方长,你以为我要收尾了吧,其实我主要就是想写下「不妨不妨,来日方长」这句话。(ps 没办法,就是这么皮呀

5.1 删除问题转化

枚举大法是真香呀!
在这里插入图片描述

  • 删除无孩子节点 —— 直接删除不用多想

  • 删除只有一个孩子节点 —— 用孩子替代删除节点

  • 删除有两个孩子节点

    • 找到它右子树最小节点 M
    • 将 M 值与待删除节点替换
    • 删除 M

    注:

    M 一定只有一个孩子,因为他是右子树最小的节点啊,所以 M 的右孩子一定指向哨兵节点

    替代节点 M 也可以是左子树最大的节点,并且也会只有一个孩子

结论: 问题转化为如何删除没有孩子的节点或者是删除只有一个孩子的节点啦。

5.2 删除情况枚举

删除在加上颜色限制以后的可枚举情况:

  • 删除节点为红色 — 不违反任何红黑树性质,直接删除即可

  • 删除节点为黑色

  • 删除的黑色节点有一个红色的孩子 — 用他的孩子替代他,然后将孩子的颜色置为黑色

  • 删除的黑色节点有一个黑色的孩子—讲真这是最不想看到的情况,照例单独讨论吧,因为真的太复杂了,笔者已经哭晕了……

注: 这么想要了解的知识,怎么可能知道一半就放弃呢……

5.3 复杂删除

删除黑色节点,本质就是造成某条路径上黑色节点的个数少 1 ,导致红黑树不再满足性质 5。来,别慌,跟我再复习一次。结点删除要秉承着「一荣俱荣,一损俱损」的观点,翻译过来就是:

  • 左边黑节点个数加 1 可以,右边你也得给我加 1
  • 左边黑节点个数减 1 可以,右边你也得给我减 1

鉴于笔者写到此处时脑子已经一团浆糊(其实是我自己构造不出来,想象中的场景),以下所有的删除都是局部视图,并且均已完成删除节点替换的步骤

5.3.1 N 有个红色的兄弟 U

在这里插入图片描述

5.3.2 N 有个黑色兄弟 U

黑色的兄弟 U 是个好兄弟,因为不仅他自己是黑色的,他的两个儿子也都是黑色的,笔芯。
在这里插入图片描述

注:U 置为红色后可能会违反红红不为父子的情况,若父节点 P 非红色,树满足性质;若父节点 P 为红色,递归调整。

5.3.3 N 有个黑色兄弟 U 续

黑色的兄弟 U 还蛮省心的因为他有个还算乖巧、不调皮捣蛋的儿子。

等等,先枚举下 U 儿子的可能性?

  • 黑黑 — 上面已经讨论过啦,写太多弊端就是读到下面会忘记上面,记不住的话我们回去复习复习呗

  • 红红 — U 的左儿子 Ul 是个乖儿子,因为它是红色的

  • 红黑 — U 的左儿子 Ul 是个乖儿子,因为它是红色的

  • 黑红 — 这两个是个不省心的儿子,我们后面再继续讨论
    在这里插入图片描述

5.3.4 N 有个黑色的兄弟 U 续续

醒醒,我们就快结束了,喝完这一杯,没有下一杯了……,所以亲亲,这边建议你戒骄戒躁,耐心看完呦!

复习下现在的场景,黑色的兄弟 U 有一对不省心的儿子,左儿子 Ul 是黑色,右儿子 Ur 红色。
在这里插入图片描述

5.3 复杂删除总结

鉴于笔者脑子已经一团浆糊 + 非常困,复杂删除的总结图,我们就简单一点画好啦。
在这里插入图片描述

6. 实现

昔日战国七雄争霸,秦于长平之战斩杀赵军约 45 万。而此战失败的主要原因是,赵王遂弃用名将廉颇,而起用赵括代替廉颇,而赵括只会熟读兵书,但缺乏战场经验,不懂得灵活应变。

为什么写上面这段呢?可能是要反思自己不能只看书,不思考,不实现吧(ps 毕竟我是个天马行空的笔者……。

原则上讲,也不是我实现的,是我理解了思想后照抄了 nginx 的 rbtree 实现的源码。实现思路完全按照上述介绍的步骤。

go 实现的源码地址

注:笔者忙着总结文章,这里的代码有很多细节还木有打磨好,各位大侠请轻喷啊

6.1 如何测试

「有东西让你抄就很简单了」,复杂的是怎么确认自己抄的是否正确呢?

  • 笔者大概看了下其他人红黑树的测试,大概都是单纯的测试下插入+ 先序遍历结果,总觉得不是很符合预期
  • 后来想了下,测试其实应该测试各种插入、删除后是否仍满足红黑树的性质 (ps 不能吐槽我……)

7. 参考资料

在使用Python来安装geopandas包时,由于geopandas依赖于几个其他的Python库(如GDAL, Fiona, Pyproj, Shapely等),因此安装过程可能需要一些额外的步骤。以下是一个基本的安装指南,适用于大多数用户: 使用pip安装 确保Python和pip已安装: 首先,确保你的计算机上已安装了Python和pip。pip是Python的包管理工具,用于安装和管理Python包。 安装依赖库: 由于geopandas依赖于GDAL, Fiona, Pyproj, Shapely等库,你可能需要先安装这些库。通常,你可以通过pip直接安装这些库,但有时候可能需要从其他源下载预编译的二进制包(wheel文件),特别是GDAL和Fiona,因为它们可能包含一些系统级的依赖。 bash pip install GDAL Fiona Pyproj Shapely 注意:在某些系统上,直接使用pip安装GDAL和Fiona可能会遇到问题,因为它们需要编译一些C/C++代码。如果遇到问题,你可以考虑使用conda(一个Python包、依赖和环境管理器)来安装这些库,或者从Unofficial Windows Binaries for Python Extension Packages这样的网站下载预编译的wheel文件。 安装geopandas: 在安装了所有依赖库之后,你可以使用pip来安装geopandas。 bash pip install geopandas 使用conda安装 如果你正在使用conda作为你的Python包管理器,那么安装geopandas和它的依赖可能会更简单一些。 创建一个新的conda环境(可选,但推荐): bash conda create -n geoenv python=3.x anaconda conda activate geoenv 其中3.x是你希望使用的Python版本。 安装geopandas: 使用conda-forge频道来安装geopandas,因为它提供了许多地理空间相关的包。 bash conda install -c conda-forge geopandas 这条命令会自动安装geopandas及其所有依赖。 注意事项 如果你在安装过程中遇到任何问题,比如编译错误或依赖问题,请检查你的Python版本和pip/conda的版本是否是最新的,或者尝试在不同的环境中安装。 某些库(如GDAL)可能需要额外的系统级依赖,如地理空间库(如PROJ和GEOS)。这些依赖可能需要单独安装,具体取决于你的操作系统。 如果你在Windows上遇到问题,并且pip安装失败,尝试从Unofficial Windows Binaries for Python Extension Packages网站下载相应的wheel文件,并使用pip进行安装。 脚本示例 虽然你的问题主要是关于如何安装geopandas,但如果你想要一个Python脚本来重命名文件夹下的文件,在原始名字前面加上字符串"geopandas",以下是一个简单的示例: python import os # 指定文件夹路径 folder_path = 'path/to/your/folder' # 遍历文件夹中的文件 for filename in os.listdir(folder_path): # 构造原始文件路径 old_file_path = os.path.join(folder_path, filename) # 构造新文件名 new_filename = 'geopandas_' + filename # 构造新文件路径 new_file_path = os.path.join(folder_path, new_filename) # 重命名文件 os.rename(old_file_path, new_file_path) print(f'Renamed "{filename}" to "{new_filename}"') 请确保将'path/to/your/folder'替换为你想要重命名文件的实际文件夹路径。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值