文章目录
1 概述
我们前面学习过关于二叉树的算法已经能够很好地用于许多应用中,但他们在对坏情况下的性能很糟糕。比如将有序数列插入二叉查找树中,二叉查找树实际退化为单链表,时间复杂度为线性。在一棵含有N个结点的树中,我们希望树高为 ∼ lg N \sim\lg N ∼lgN,这样我们就能保证查找都能在 ∼ lg N \sim\lg N ∼lgN次比较内结束,就和二分查找一样。那么这里我们会介绍一种二分查找树并能保证物理如何构造它,它的运行时间都是对数级别的。
2-3树是最简单的B-树(或-树)结构,其每个非叶节点都有两个或三个子女,而且所有叶都在统一层上。2-3树不是二叉树,其节点可拥有3个孩子。不过,2-3树与满二叉树相似。高为h的2-3树包含的节点数大于等于高度为h的满二叉树的节点数,即至少有 2 h − 1 2^h-1 2h−1个节点。
一棵2-3查找树为一棵空树或者由以下节结点构成:
- 2-结点,含有一个键(及其对应的值)和两条链接,左链接指向的2-3树中的结点都小于该结点,有链接指向的2-3树中的结点都大于该结点。
- 3-结点:含有两个键(及其对应的值)和3条链接,左链接指向的2-3树中的结点都小于该结点中的较小建,中链接指向的2-3树中的结点位于该结点的2个键之间,右链接指向的2-3树中的结点都大于该结点中的较大键。
指向一棵空树的链接称为空链接。
2-3查找树示意图如下1-1所示:
一棵完美平衡的2-3查找树中的所有空链接(或叶结点)到根结点的距离都是相同的。
2 查找
将二叉查找树的插在算法一般化我们就能够得到2-3树的查找算法。要判断一个键是否在树中,我们先将它和根结点中的键比较。如果它和其中任一键相等,直接命中;否则我们就根据比较的结果找到链接对应的区间,并在其指向的子树中循环(或递归)查找。直到空链接,说明未命中,键不在树中。
命中和未命中查找,如下图2-1所示:
3 插入
要在2-3树中插入一个新结点,我们可以和二叉查找树一样先进行一次未命中查找,然后把新结点挂载树的底部。但这样的话树无法保持完美平衡性。我们使用2-3树的主要原因就在于它能够在插入后保持完美平衡性。
3.1 向2-结点中插入新键
如果未命中查找结束于2-结点,我们只需要把该2-结点替换为3-结点,把要插入的键值保持在该结点即可,如下图3.1-1所示:
3.2 向一个只含有一个3-结点的树中插入新键
如果未命中查找结束一个3-结点,情况相对来说有点复杂。在考虑一般情况之前,我们先假设当前只有一个3-结点即根节点,步骤如下:
- 把该3-结点扩展为4-结点,把键值对放入合适位置
- 该4-结点向上生长(拆分)为3个2-结点,拆分规则如下:
- 跟结点包含中键
- 根结点的左结点包含较小的键
- 根结点右结点包含较大的键
这棵新的2-3树在保证有序的情况下,也保证了平衡性,因为所有的空结点到根结点的距离是相等的。这个例子很简单但值得学习,它说明了2-3树是如何生长的,即向上生长,如下图3.2-1所示:
树的高度变化:
- 插入前1
- 插入后+1=2
- 2-3树只会向上生长,只有根结点的生长使高度+1
3.3 向一个父结点为2-结点的3-结点中插入新键
假设未命中的查找结束于3-结点n3,它的父结点为一个2-结点p2,插入执行流程如下:
-
把n3 3-结点扩展为4-结点,键值插入合适位置,使该结点保持有序性。
-
把该4-结点分解,其中中键移动至其父结点中,插入父结点的合适位置,使之有序。
- 父结点为2-结点有空间,扩展为3-结点
-
4-结点的剩余小大键分别作为其父结点的子结点,根据有序性链接到合适区间。
这些操作,中键移入父结点中,插入新键、中键分解、中键加入结点,父结点链接调整,这些操作都进行有序性调整,保证有序性;且保证了平衡性,空结点到根节点的距离依然不变。插入后2-3树依然是完美平衡的,如下图3.3-1所示:
3.4 向一个父结点为3-结点的3-结点中插入新键
现在假设未命中查找结束于一个父结点也是3-结点的3-结点,插入流程如下:
- 把3-结点扩展为4-结点,键值插入合适位置,使该结点保持有序性。
- 中键分解移入父结点,因为父结点也是3-结点,执行同步骤1一样的操作
- 剩余小大键根据有序性,链入父结点合适位置
- 重复执行步骤1~3,直至父结点为2-结点或者根节点为3-结点
- 父结点为2-结点,参考3.3操作
- 父结点为3-根结点,参考3.2操作
推广到一般情况,我们一直向上分解临时4-结点把中键插入更高的父结点,直至遇到2-结点或者根节点为3-结点,分解根结点,依然保持了2-3树的完美平衡性,如图3.4-1所示:
4 分析
4.1 局部变换
将一个4-结点分解为一棵2-3树的可能有6种情况。这个4-结点可能是根节点,可能是一个2-结点的左子结点或者右子结点,也可能是一个3-结点的左子结点、中子结点或者右子结点。2-3树的插入算法的根本在于这些变换都是局部的:除了相关的结点和链接之外不必修改或者检查树的其他部分。每次变换,变更的链接不会超过一个很小的常数。需要特别指出的是,不光是在树的底部,树中的其他任何地方只要符合相应的模式,变换都可进行。每个变换都会将4-结点中的一个键送入它的父结点中,并重构相应的链接而不必涉及树的其他部分。
4.2 全局性质
这些局部变换不会影响全局有序性和平衡性:任意空链接到根结点的路径长度都是相等的。
不同于标准的二叉查找树,2-3树的生长是由下向上的。
2-3树的分析和二叉查找树大不相同,因为我们主要感兴趣的的最坏情况下的性能而非一般情况。在符号表的实现中,一般我们无法控制用例会按照什么顺序向表中插入键,因此对最坏情况的分析是唯一提供性能保证的办法。
命题F。在一棵大小为N的2-3树中,查找和插入操作访问结点的比如不超过 lg N \lg N lgN个。
证明。一棵含有N个结点的2-3树高度h的范围为 ⌊ lg N ⌋ ≥ h ≥ ⌊ log 3 N ⌋ \lfloor\lg N\rfloor\ge h\ge \lfloor \log_3N\rfloor ⌊lgN⌋≥h≥⌊log3N⌋
虽然我们分析了2-3树的性质特点和一些操作步骤,但是要 实现很复杂。包括维护2种类型的结点,将被查找的键和结点中的每个键进行比较,将链接和其他信息从一种结点复制到另外一种结点等等。实现这些不仅需要大量的代码,而且他们所产生的额外开销可能会使算法比标准的二叉查找树更慢。那我们为什么还要研究它呢?因为它是接下来我们要学习其他一些树形结构的理论基础。下面我们只需要一点点代价就能用一种统一的方式完成所有变换,即我们要学习的红黑树。
5 后记
如果小伙伴什么问题或者指教,欢迎交流。
❓QQ:806797785
⭐️源代码仓库地址:https://gitee.com/gaogzhen/algorithm
[1][美]Robert Sedgewich,[美]Kevin Wayne著;谢路云译.算法:第4版[M].北京:人民邮电出版社,2012.10