作者 | 曹金霖
企鹅杏仁 Java 工程师,正在锻炼自制力的朴素程序猿。
上节回顾
上节内容我们学习了自平衡二叉搜索树 AVL 树,在算出影响平衡的节点后,通过左旋和右旋的组合,使得整个二叉树的左右子树的高度差不大于 1 ;
这节内容就来到了这个系列的关键点了,红黑树的相关内容;
红黑树
红黑树也是一种自平衡的二叉搜索树(以前名为平衡二叉 B 树)
红黑树必须满足 5 条性质
节点是 RED 或者 BLACK
根节点是 BLACK
叶子节点(外部节点,空节点)都是 BLACK
红黑树的节点的度都为 2,为满足这个性质,红黑树需要补全叶子节点中度不为 2 的节点。补全方式很简单,对每个度不为 2 的节点添加一个空的黑节点,如上图中节点数据为 null 的节点。
RED 节点的 子节点 都是 BLACK
由这个性质可以推断出如下的规则
RED 节点的 parent 都是 BLACK
从根节点到叶子节点的所有路径上不能有 2 个连续的 RED 节点
从任一节点到叶子节点的所有路径都包含相同数目的 BLACK 节点
从这些性质上看很难理解为什么满足这些性质的树就是平衡的。红黑树之前的名字叫平衡二叉 B 树。大家都知道只有起错的名字,没有叫错的外号。那红黑树和 B 树又有什么关系呢。所以在深入学习红黑树之前,我们先学习一下 B 树,现在先忘记上面的红黑树性质,进行 B 树的学习;
B 树(B - tree, B-树)
B 树:一种平衡的多路搜索树,多用于文件系统,数据库的实现。
可以看出 B 树有以下特点
1 个节点可以存储超过 2 个元素,可以拥有超过 2 个子节点
拥有二叉搜索树的一些性质
平衡,每个节点的所有子树高度一致
比较矮
m 阶 B 树的性质(m>=2)
每个节点最多只有 m 个子节点
每个非叶节点(根节点除外)至少具有 ⌈ m / 2 ⌉个子节点。( ⌈ -> 向上取整 )
如果根不是叶节点,则根至少有两个子节点
一个有 k 个子节点的非叶子节点包含 k − 1 个元素
所有叶子都出现在同一层。
由上面的性质可以推断出一下结论
假设一个节点存储的元素个数为 x
根节点:1<= x <= m-1 (性质 1,4)
非根节点:⌈m/2⌉ - 1 <= x <=m-1 (性质 1,2,4)
假设一个子节点的个数为 y
根节点上的子节点数:2<= y <= m (性质 1,3)
非根节点上的子节点数:⌈m/2⌉ <= y <= m (性质 1,2)
比如 m = 3 时, 2<=y<=3 因此可以称为(2,3)树,2-3 树 比如 m = 4 时, 2<=y<=4 因此可以称为(2,3,4)树,2-3-4 树
B 树 VS 二叉搜索树
B 树和二叉搜索树在逻辑上是等价的
如下图,当把二叉搜索树的(18,33)节点合并,(23,30)节点合并,(20.21)节点合并,(45,47)节点合并,图 1 的二叉搜索树就是图 2 所展示的 3 阶 B 树。
图 1
图 2
当二叉树搜索树的多代节点合并,就是 B 树中存储多个元素的超级节点。
2 代合并的超级节点,最多拥有 4 个子节点
3 代合并的超级节点,最多拥有 8 个子节点
n 代合并的超级节点,最多拥有 2 ^n个子节点
m 阶 B 树,最多需要 log_2^m 代合并
搜索
既然B树的逻辑和二叉搜索树是等价的,那搜索的逻辑其实也就差不多了
先在节点内部从小到大开始搜索元素
如果命中,搜索结束
如果未命中,再去对应的子节点中搜索元素,重复步骤 1
添加
新添加的元素必定是添加到叶子节点,和二叉搜索的添加类似。不同的是 B 树的一个节点可以存储多个元素。但是节点中可以存储的元素个数是有限制的,具体查看上文中的 m 阶 B 树的性质
这里分别有 2 个 3 阶 B 树的添加例子,对应了 B 树添加的 2 种情况,3 阶 B 树的节点中元素的最大个数为 m-1=2
如果节点拥有的元素数量小于最大值,那么有空间容纳新的元素。将新元素插入到这一节点,且保持节点中元素有序。
否则的话这一节点已经满了,将它平均地分裂成两个节点
从该节点的原有元素和新的元素中选择出中位数
小于这一中位数的元素放入左边节点,大于这一中位数的元素放入右边节点,中位数作为分隔值。
分隔值被插入到父节点中,这可能会造成父节点分裂,分裂父节点时可能又会使它的父节点分裂,以此类推。如果没有父节点(这一节点是根节点),就创建一个新的根节点(增加了树的高度)。
删除
删除非叶子节点中的元素
删除非叶子节点和平衡二叉树的删除度为 2 的节点的方式是一致的
先找到删除节点的前驱或者后继元素,覆盖所需删除元素的值。非叶子节点的元素必定在叶子节点中
再把前驱或后继元素删除
删除叶子节点中元素
直接在所在节点中删除改元素
删除-下溢的解决
以上 2 中情况真正被删除的元素都是在叶子节点中,在删除叶子节点中的元素后,可能会出现删除后的节点中的元素的个数不满足 B 树的性质,即元素的个数小于该节点应该存放元素的最小值 ⌈m/2⌉ -1,这个情况叫(下溢)
如下图 5 阶 B 树中删除非叶子节点
根据 m 阶 B 树的性质,下溢节点的元素数量必然等于 ⌈m/2⌉ - 2
如上图,如果下溢节点临近的兄弟节点有至少 ⌈ m / 2 ⌉ 个元素,可以向其借一个元素
将父节点的元素 B 插入到下溢节点的最小位置
将元素 A 插入到父节点中
这种操作就是旋转
如果下溢节点的临近的兄弟节点只有 ⌈ m / 2 ⌉ -1 个元素
它与一个直接兄弟节点以及父节点中它们的分隔值合并
合并后的节点元素个数等于 ⌈m / 2 ⌉ + ⌈ m / 2⌉ - 2 不超过上限 m-1
这个操作可能会导致父节点下溢
-
-
如果父节点是根节点并且没有元素了,那么释放它并且让合并之后的节点成为新的根节点(树的深度减小)
否则,如果父节点的元素数量小于最小值,重复上述步骤,恢复父节点的平衡
-
总结
上面讲了很多关于 B 树相关的知识,那 B 树到底和红黑树有什么关系呢。
我们看一下 4 阶 B 树的特点:
所有节点(包括根节点和非根节点)能存储的元素个数x :1<= x <=3;
所有非叶子节点的子节点个数y :2 <= y <=4;
下图是个红黑树
当把红色节点和它的黑色父节点放在同一个水平线上后
所以红黑树和 4 阶 B 树(2-3-4树)具有等价性
当 BLACK 节点与它的 RED 子节点融合在一起,形成一个 B 树节点
红黑树中 BLACK 的节点个数与 4 阶 B 树的节点总个数相等
看到这里,大家应该就知道B树和红黑树的关系了。对于 B 树我们没有进行代码说明只是逻辑的展示,下一章真正开始红黑树的时候在进行代码说明。
全文完
以下文章您可能也会感兴趣:
简单说说spring的循环依赖
一个 AOP 缓存失效问题的排查
小程序开发的几个好的实践
从 30 分钟到 1 分钟 - 一个 Scala 项目的编译速度优化
简单聊聊 TCP 的可靠性
延时队列:基于 Redis 的实现
你真的懂 Builder 设计模式吗?论如何实现真正安全的 Builder 模式
我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com 。