二叉查找树回顾
//二叉排序树结点
typedef struct BSTNode{
int key;
struct BSTNode *lchild, *rchild;
}BSTNode, *BSTree
能不能变成m叉查找树?
五叉查找树
与二叉查找树的做法一致,就是在 ( − ∞ , + ∞ ) (-\infty,+\infty) (−∞,+∞)初始范围内加断点,将其分为多个区间。如上图5叉查找树:
- 22将 ( − ∞ , + ∞ ) (-\infty,+\infty) (−∞,+∞)划分为 ( − ∞ , 22 ) ∪ ( 22 , + ∞ ) (-\infty,22)\cup(22,+\infty) (−∞,22)∪(22,+∞)
- 在22结点的左孩子中,又插入两个值5和11,将 ( − ∞ , 22 ) (-\infty,22) (−∞,22)划分为 ( − ∞ , 5 ) ∪ ( 5 , 11 ) ∪ ( 11 , 22 ) (-\infty,5)\cup(5,11)\cup(11,22) (−∞,5)∪(5,11)∪(11,22)
- 其余以此推类
//5叉排序树的结点定义
struct Node{
ElemType keys[4]; //最多4个关键字
struct Node *child[5]; //最多5个孩子
int num; //当前结点中有几个关键字
}
Notes:
- 在5叉查找树中,最少一个关键字,2个分叉。最多4个关键字,5个分叉。
- 结点内关键字有序。
- 结点内可以用折半查找和顺序查找
如何保证查找效率
现在考虑这样一个问题,如果5叉查找树中每个结点只保存一个关键字(如下图所示),那么这样一棵树相等于一颗二叉查找树,此时两者有什么不同?
若每个结点内关键字太少,导致树变高,要查更多层结点,效率低。为了解决这个问题,我们可以提出一个策略:m叉查找树中,规定除了根结点外,任何结点至少有 ⌈ m 2 ⌉ \lceil\frac{m}{2} \rceil ⌈2m⌉个分叉,即至少含有 ⌈ m 2 ⌉ − 1 \lceil\frac{m}{2} \rceil -1 ⌈2m⌉−1个关键字。
例如:对于一个5叉排序树,规定除了根结点外,任何结点都至少有3个分叉,2个关键字。
那么为什么规定除了根结点以外呢?
原因:如果整个树只有1个元素,根结点只有两个分叉。
如下图所示,这样一颗满足上述要求的5叉查找树是否优秀呢?
不够‘平衡’,树会很高,要查很多层结点。为了解决这样一个问题,我们添加一个策略:
m叉查找树中,规定对于任何一个结点,其所有子树的高度都要相同。
那么,满足了之前提出的两个策略的树即为B树。上图为一个五阶B树。
B树的定义
B树,又称多路平衡查找树,B树中所有结点的孩子个数的最大值称为B树的阶,通常用m表示一棵m阶B树或为空树,或为满足如下特性的m叉树:
1)树中每个结点至多有m棵子树,即至多含有m-1个关键字。
2)若根结点不是终端结点,则至少有两棵子树。(要保证每个结点的绝对平衡)
3)除根结点外的所有非叶结点至少有 ⌈ m 2 ⌉ \lceil\frac{m}{2} \rceil ⌈2m⌉棵子树,即至少含有 ⌈ m 2 ⌉ − 1 \lceil\frac{m}{2} \rceil-1 ⌈2m⌉−1个关键字。
4)所有非叶结点的结构如下:
其中,
K
i
(
i
=
1
,
2
,
.
.
.
,
n
)
K_i~(i = 1,2,...,n)
Ki (i=1,2,...,n)为结点的关键字,且满足KaTeX parse error: Double subscript at position 7: K_1<_K_̲2<...<K_n; P_i …为指向子树根结点的指针,且指针
P
i
−
1
P_{i-1}
Pi−1所指子树中所有结点的关键字均小于
K
i
,
P
i
K_i,P_i
Ki,Pi所指子树中所有结点的关键字均大于
K
i
,
n
(
⌈
m
2
⌉
−
1
≤
n
≤
m
−
1
)
K_i,n(\lceil \frac{m}{2} \rceil-1 \leq n\leq m-1)
Ki,n(⌈2m⌉−1≤n≤m−1)为结点中关键字的个数。
5)所有的叶结点都出现在同一层次上,并且不带信息(可以视为外部结点或类似于折半查找判定树的查找失败结点,实际上这些结点不存在,指向这些结点的指针为空)。
m阶B树的核心特性:
- 根结点的子树数
∈
[
2
,
m
]
\in[2,m]
∈[2,m],关键字数
∈
[
1
,
m
−
1
]
\in[1,m-1]
∈[1,m−1]。
其他结点的子树数 ∈ [ ⌈ m 2 ⌉ , m ] \in[\lceil \frac{m}{2} \rceil,m] ∈[⌈2m⌉,m];关键字数 ∈ [ ⌈ m 2 ⌉ − 1 , m − 1 ] \in[\lceil \frac{m}{2} \rceil-1,m-1] ∈[⌈2m⌉−1,m−1] - 对任一结点,其所有子树高度都相同。
- 关键字的值:子树0<关键字1<子树1<关键字2<子树2<…(类比二叉查找树 左<中<右)
B树的高度
⚠️B树的高度不包括叶子结点(失败节点)
问题:含n个关键字的m阶B树,最小高度、最大高度是多少?
最小高度—让每个结点尽可能的满,有m-1个关键字,m个分叉,则有
n
≤
(
m
−
1
)
(
1
+
m
+
m
2
+
⋅
⋅
⋅
+
m
h
−
1
)
=
m
h
−
1
,
因
此
h
≥
l
o
g
m
(
n
+
1
)
n\leq(m-1)(1+m+m^2+···+m^{h-1})=m^h-1,因此h \geq log_m(n+1)
n≤(m−1)(1+m+m2+⋅⋅⋅+mh−1)=mh−1,因此h≥logm(n+1)
最大高度—让各层的分叉尽可能的少,即根结点只有2个分叉,其他结点只有
⌈
m
2
⌉
\lceil \frac{m}{2} \rceil
⌈2m⌉个分叉
各层结点至少有:第一层1、第二层2、第三层
2
⌈
m
2
⌉
2\lceil \frac{m}{2} \rceil
2⌈2m⌉、…、第h层
2
(
⌈
m
2
⌉
)
h
−
2
2(\lceil \frac{m}{2} \rceil)^{h-2}
2(⌈2m⌉)h−2
第h+1层共有叶子结点(失败节点)
2
(
⌈
m
2
⌉
)
h
−
1
2(\lceil \frac{m}{2} \rceil)^{h-1}
2(⌈2m⌉)h−1个
n个关键字的B树必有n+1个叶子结点,则
n
+
1
≥
2
(
⌈
m
2
⌉
)
h
−
1
n+1\geq 2(\lceil \frac{m}{2} \rceil)^{h-1}
n+1≥2(⌈2m⌉)h−1,即
h
≤
l
o
g
⌈
m
/
2
⌉
n
+
1
2
+
1
h \leq log_{\lceil m/2 \rceil}\frac{n+1}{2}+1
h≤log⌈m/2⌉2n+1+1
l
o
g
m
(
n
+
1
)
≤
h
≤
l
o
g
⌈
m
/
2
⌉
n
+
1
2
+
1
log_m(n+1) \leq h \leq log_{\lceil m/2 \rceil}\frac{n+1}{2}+1
logm(n+1)≤h≤log⌈m/2⌉2n+1+1
B树的插入
假设我们从0创建一个5阶B树,则结点关键字个数由公式
⌈
m
/
2
⌉
−
1
≤
n
≤
m
−
1
\lceil m/2 \rceil -1\leq n \leq m-1
⌈m/2⌉−1≤n≤m−1
得,
2
≤
n
≤
4
2\leq n \leq 4
2≤n≤4。此处省略失败节点。\
首先,依次插入25、38、49、60、80五个关键字:
可以看出此时80这个关键字已经超出了一个根结点的范围,所以需要将这个结点分裂成两个结点:
在插入key后,若导致原结点关键字数超过上限,则从中间位置 ⌈ m / 2 ⌉ \lceil m/2 \rceil ⌈m/2⌉将其中的关键字分成两部分,左部分包含的关键字放在原结点中,右部分包含的关键字放到新结点中,中间位置 ⌈ m / 2 ⌉ \lceil m/2 \rceil ⌈m/2⌉的结点插入原结点的父结点。
新元素一定是插入到最底层“终端结点”,用“查找”来确定插入位置
接下来继续插入一个关键字:90,步骤:首先与根结点的关键字比较,因为90>49且49右边已经没有关键字,所以需要在49右边指针指向的结点中插入,接下来依次与60、80比较可知90需要放在80的右边(如下图),并且此时没有超出结点的范围。
错误示范:失败节点已经不属于同一层了。
继续插入88、99两个关键字,通过“查找”可以将两个关键字放入指定位置,如下图
此时最右边的终端结点中的关键字再一次超出范围,所以跟刚才一样找到
⌈
m
/
2
⌉
\lceil m/2 \rceil
⌈m/2⌉这个位置,左部分包含的关键字放在原结点中,右部分包含的关键字放到新结点中,中间位置
⌈
m
/
2
⌉
\lceil m/2 \rceil
⌈m/2⌉的结点插入原结点的父结点。如下图
88为中间位置的元素,依据规则放入原结点的父结点,也就是根结点中。90和99两个关键字组成的新结点原本就比88要大,所以用88右边的指针指向新结点,这样就拆分成功了。
继续插入70、83、87三个关键字,如下图:
可以看出又有一个结点需要分裂,这里思考一个问题:80要放到父节点中,而放在哪个位置合适?
答案是:将80放在49与88之间。
这里可以总结一点:当前结点需要分裂时,需要将中间位置的元素放入该结点的父结点用于存储指向该结点指针的右边一个位置,然后将其余元素右移一位即可。
继续插入92、93、94三个关键字:
按照规则需要将结点分裂为以下结果:
继续插入73、74、75三个关键字:
这个结点又需要分裂了,分裂结果如下:
此时可以看出,因为73插入根结点,导致根结点也需要分裂了,那么这种情况如何处理呢?
若此时导致其父结点的关键字个数也超过了上限,则继续进行这种分裂操作,直至这个过程传到根结点为止,进而导致B树高度增加1。
即将根结点的中间位置的元素80放入新的根结点中,并且将分裂后的左右两个部分放在新根结点中80的左右两边,如下图所示:
新的B树仍然满足B树的所有要求。
B树插入的总结
核心要求
①对m阶B树除根节点外,结点关键字个数
⌈
m
/
2
⌉
−
1
≤
n
≤
m
−
1
\lceil m/2 \rceil -1\leq n \leq m-1
⌈m/2⌉−1≤n≤m−1
②子树0<关键字1<子树1<关键字2<子树2<…
新元素一定是插入到最底层“终端节点,用“查找”来确定插入位置
在插入key后,若导致原结点关键字数超过上限,则从中间位置 ⌈ m / 2 ⌉ \lceil m/2 \rceil ⌈m/2⌉将其中的关键字分为两部分,左部分包含的关键字放在原结点中,右部分包含的关键字放到新结点中,中间位置 ⌈ m / 2 ⌉ \lceil m/2 \rceil ⌈m/2⌉的点插入原结点的父结点。若此时导致其父结点的关键字个数也超过了上限,则继续进行这种分裂操作,直至这个过程传到根结点为止,进而导致B树高度增1。
B树的删除
若被删除关键字在终端结点,则直接删除该关键字(要注意结点关键字个数是否低于下限 ⌈ m / 2 ⌉ − 1 \lceil m/2 \rceil -1 ⌈m/2⌉−1)
比如要删除的元素为60,因为60所在结点为终端结点,所以直接删除60即可。
若被删除关键字在非终端结点,则用直接前驱或直接后继来替代被删除的关键字
直接前驱:当前关键字左侧指针所指子树中“最右下”的元素
直接后继:当前关键字右侧指针所指子树中“最左下”的元素
假设此时要删除80这个元素,因为80为根结点中的唯一元素,所以需要用80的直接前驱或者直接后继来顶替80的位置,这里我们用80的直接前驱77来替代80的位置,结果如下:
接下来删除77这个元素,并用77的直接后继82顶替:
对非终端结点的删除操作都可以转换成对终端结点的操作。
以上的删除操作都满足删除后的结点仍旧满足结点的要求,接下来删除一个结点,导致该结点的关键字个数已经不满足要求,此时应该怎么处理呢?
假设删除关键字38,树如下:
解决方法一:
兄弟够借若被删除关键字所在结点删除前的关键字个数低于下限,且与此结点右(或左)兄弟结
点的关键字个数还很宽裕,则需要调整该结点、右(或左)兄弟结点及其双亲结点(父子换位法)
我们会让右兄弟结点贡献出一个元素,但不是直接将贡献出的元素插入原结点中,而是将原结点在父结点中指针右侧的元素插入原结点中,并用贡献出的元素替代这个位置。如下图,用25所在结点的父结点对应指针右侧的元素49插入原结点中,并且右兄弟结点贡献出的70这个元素弥补父结点缺失元素的位置。
总结为一句话就是,当右兄弟很宽裕时,用当前结点的后继、后继的后继来填补空缺
接下来看一个用向左兄弟借一个元素的例子,比如要删除的元素为90,则需要用90的前驱和前驱的前驱来填补空缺,即用90的前驱88放入原结点,用88的前驱87填补88原来所在位置,如下图:
当左兄弟很宽裕时,用当前结点的前驱、前驱的前驱来填补空缺
经过上述删除操作的B树如下图所示:
继续删除关键字49,此时原结点关键字不足,并且右兄弟结点的关键字也没有宽裕。那么应该将原结点和右兄弟结点合并,同时还要把两个结点中间那个元素也同时合并。
兄弟不够借。若被删除关键字所在结点删除前的关键字个数低于下限,且此时与该结点相邻的左、右兄弟结
点的关键字个数均=
⌈
m
/
2
⌉
−
1
\lceil m/2 \rceil-1
⌈m/2⌉−1,则将关键字删除后与左(或右)兄弟点及双亲结点中的关键字进行合并
此时可以看出前两层的结点也需要合并,所以按照上述的要求同样合并结点,结果如下:
因为根结点在合并后已经没有关键字了,所以可以把根结点删除。
B树的删除总结
兄弟不够借。若被删除关键字所在结点删除前的关键字个数低于下限,且此时与该结点相邻的左、右兄弟结点的关键字个数均= ⌈ m / 2 ⌉ − 1 \lceil m/2 \rceil-1 ⌈m/2⌉−1,则将关键字删除后与左(或右)兄弟结点及双亲结点中的关键字进行合并
在合并过程中,双亲结点中的关键字个数会减。若其双亲结点是根结点且关键字个数减少至0(根结点关键字个数为时,有2棵子树),则直接将根结点删除,合并后的新结点成为根若双亲结点不是根结点,且关键字个数减少到 ⌈ m / 2 ⌉ − 2 \lceil m/2 \rceil-2 ⌈m/2⌉−2,则又要与它自己的兄弟结点进行调整或合并操作,并重复上述步骤,直至符合B树的要求为止。