当数据规模大到内存已不足以容纳时, 常规的平衡二叉搜索树的效率将大打折扣。其原因在于,查找过程对外存的访问次数过多。
磁盘等外部存储器的一个特点是: 读取物理地址连续的一千个字节,与读取单个字节所消耗的时间几乎是没有区别的。
此时,可以使用多路搜索树。多路搜索树可以由二叉搜索树变换得到。如,
此时,搜索过程每下降一层,都以“大节点”为单位从外存中读取一组(而不再是一个)关键码。更为重要的是,这组关键码在逻辑上与物理上都彼此相邻,故可以批量方式从外存一次性读出,且所需时间与读取单个关键码几乎一样。
B 树
- 所谓 m 阶 B-树 , 即 m 路平衡搜索树()。
- 所有外部节点的深度相等。
- 设一个内部节点存有 n 个关键码, 以及用以指示对应分支的 n+1 个指针。除根以外的所有内部节点,都应满足: ,而在非空的 B-树中,根节点应满足:
- 每个内部节点中的关键码按顺序排列。
- 由于各节点的分支数介于 至 之间,故m阶B-树也称 -树 。(关键码数介于 至 之间)
B+ 树 与 B 树 的区别
- B+ 树 的内部节点只存放关键码,不存放具体的数据,所有数据都放在叶子节点。而 B 树的每个节点都存放有数据。
- B+ 树的叶子节点中存有一个指向下一个叶子节点的指针(关键码小的指向关键码大的)
- B+ 树中同一个关键码可以在不同节点中重复出现,而 B树中任何一个关键码只能出现在一个结点中。
树定义
public class BTree<T extends Comparable<? super T>> {
// B 树节点
private static class BNode<T> {
BNode<T> parent;
List<T> datas; // 存放的数据(含关键码)
List<BNode<T>> children; // 指向孩子节点的指针
// 初始时有0个关键码和1个空孩子指针
BNode() {
datas = new LinkedList<>();
children = new LinkedList<>();
children.add(null);
}
}
private int order; // B 树的阶次
private BNode<T> root; // 树根
public BTree(int order) {
this.order = order;
root = new BNode<>();
}
// 在 datas 中查找 e 的位置,或 e 应该插入的位置
private int find(List<T> datas, T e) {
for (int i = 0; i < datas.size(); i++) {
// 等于:就是找到了
// 小于:就是没有找到,但 e 应该插入此处
if (e.compareTo(datas.get(i)) <= 0) {
return i;
}
}
return datas.size();
}
...
}
搜索关键码
对于活跃的B-树,其根节点会常驻于内存; 此外,任何时刻通常只有另一节点(称作当前节点)留驻于内存。
查找过程
B-树的查找过程, 与二叉搜索树的查找过程基本类似:
首先以根节点作为当前节点,若在当前节点(所包含的一组关键码)中能够找到目标关键码,则成功返回。否则,则必可在当前节点中确定某一个分支(“失败”位置) ,并通过它转至逻辑上处于下一层的另一节点。若该节点不是外部节点, 则将其载入内存,并更新为当前节点,然后继续重复上述过程。
效率
对于高度为 h 的B-树,外存访问不超过 O(h - 1) 次。
存有 N 个关键码的 m 阶B-树的高度 。
// 用于表示搜索结果(作为内部类)
private static class SearchResult<T> {
BNode<T> target; // 目标节点
BNode<T> parent; // 目标节点之父
int index; // 目标数据在目标节点中的位置
// getter & setter ...
}
// 查找关键码 e
public SearchResult<T> search(T e) {
SearchResult<T> result = new SearchResult<>();
BNode<T> current = root, parent = null;
int index = -1;
while (current != null) {
List<T> datas = current.datas;
// 在当前节点的关键码中查找,要么命中,要么获知下一步走哪个分支
index = find(datas, e);
// 找到了
if (index < datas.size() && e.compareTo(datas.get(index)) == 0) {
break;
}
// 没有找到,进入下一层
parent = current;
current = current.children.get(index);
}
result.target = current;
result.parent = parent;
result.index = index;
return result;
}
插入关键码
对于 m 介 B-树,设关键码插入的节点为 v 。
若插入新的关键码之后, v 的分支数等于 m+1,则违背了 B-树的限定条件,此时需要分裂节点 v 。
分裂节点
此时,节点 v 恰包含有 m 个关键码(即 m+1 个分支)。
取分裂点位置 ,则可将 v 的关键码分为 3部分:[0…s)、s、[s+1…) 。其中,[0…s) 部分作为左节点、[s+1…) 部分作为右节点,s 处的关键码则被提升至父节点中。
至于,被提升的关键码,有 3种可能的情况:
- 父节点未饱和,则将关键码插到合适的位置即可;
- 父节点已饱和,强行插入关键码之后,父节点也需要分裂;
- 若上溢传递至树根,则可令被提升的关键码自成一个节点, 并作为新的树根。
// 插入:不允许存在重复关键码
public void insert(T e) {
// 先确定关键码是否存在
SearchResult<T> result = search(e);
// 存在
if (result.target != null) {
return;
}
// 插到合适的位置
BNode<T> node = result.parent;
int index = find(node.datas, e);
node.datas.add(index, e);
// 添加相应的孩子指针
node.children.add(index + 1, null);
// 必要时分裂节点
solveOverflow(node);
}
// 处理节点上溢情况
private void solveOverflow(BNode<T> node) {
// 未上溢
if (node.children.size() <= order) {
return;
}
// 分裂点
int s = order >> 1;
// node 分裂为左右两个节点,原节点继续用作分裂后的左节点,rightNode 用作分裂后的右节点
// 注意:新节点已有一个空孩子
BNode<T> rightNode = new BNode<>();
rightNode.children.remove(0);
// rightNode:存放 node.datas[s+1:] 和 node.children[s+1:]
for (int i = 0; i < order - s - 1; i++) {
rightNode.datas.add(node.datas.remove(s + 1));
rightNode.children.add(node.children.remove(s + 1));
}
// 最右侧的那个指针
rightNode.children.add(node.children.remove(s + 1));
// 如果 rightNode 的孩子非空,则让它们认新爹
if (rightNode.children.get(0) != null) {
for (int i = 0; i < order - s; i++) {
rightNode.children.get(i).parent = rightNode;
}
}
// node 之父
BNode<T> parent = node.parent;
// node 为树根:被提升的关键码要作为新的树根
if (parent == null) {
root = parent = new BNode<>();
parent.children.set(0, node);
node.parent = parent;
}
// parent 中指向 rightNode 的指针之位置
int index = find(parent.datas, node.datas.get(0));
// 分裂点处的关键码上升
parent.datas.add(index, node.datas.remove(s));
parent.children.add(index + 1, rightNode);
rightNode.parent = parent;
// 有可能父节点也已上溢
solveOverflow(parent);
}
删除关键码
设节点 t 包含待删除的关键码 k 。若 t 不是叶子节点,则需要沿着适当的分支向下,直到叶子节点,并在该叶子节点中找到 k 的后继 k2 。然后,更新节点 t 中的 k 为 k2;最后,删除叶子节点中的 k2 。
对于 m 介 B-树,设叶子节点 V 包含待删除的关键码。若删除关键码之后,V 包含的分支数等于 ,则违背了 B-树的限定条件。
解决下溢问题
根据节点 V 的左、右兄弟所包含的关键码数,可分为 3种情况处理:
注:V 的左、右兄弟至少有一个不为空。(如果左右兄弟都为空,则说明节点 V 之父只有一个孩子,即只有一个分支、0个关键码)
(1)左兄弟存在,且至少包含 个关键码
设 V 之左兄弟为 L,L 与 V 分别是父节点 P 中关键码 y 的左右孩子,L 中的最大的关键码为 x 。
将 y 从节点 P 转移至节点 V 中(作为最小关键码),再将 x 从 L 转移至 P 中(取代原关键码 y)。至此,局部乃至整树都重新满足B-树条件,下溢修复完毕。
(2)右兄弟存在,且至少包含 个关键码
与前一种情况对称。
设 V 之右兄弟为 R,V 与 R 分别是父节点 P 中关键码 y 的左右孩子,R 中的最小的关键码为 x 。
将 y 从节点 P 转移至节点 V 中(作为最大关键码),再将 x 从 R 转移至 P 中(取代原关键码 y)。至此,局部乃至整树都重新满足B-树条件,下溢修复完毕。
(3)左右兄弟要么不存在,要么包含的关键码数少于
当然,左右兄弟不可能同时不存在。
不失一般性,假设 V 之左兄弟 L 存在,则 L 此时应该恰好包含 个关键码。
从父节点 P 中抽出介于 L 和 V 之间的关键码 y, 并通过该关键码将节点 L 和 V “粘接” 成一个节点。合并得到的新节点,其含有的关键码数量为 。
当左兄弟不存在时,右兄弟必定存在,可按照类似方法合并节点。
// 删除
public void remove(T e) {
// 先确定关键码是否存在
SearchResult<T> result = search(e);
// 不存在
if (result.target == null) {
return;
}
// 关键码所在的节点及索引
BNode<T> target = result.target;
int index = result.index;
// 若 target 不是叶子节点,则需要在叶子节点中找到 e 的后继,然后用其更新 e,再让后继替死
if (target.children.get(0) != null) {
// 在右子树中,一直往左,即可找到后继
// 类似于二叉搜索树(实际上,可将该 B-树展开为一棵二叉搜索树)
BNode<T> node = target.children.get(index + 1);
while (node.children.get(0) != null) {
node = node.children.get(0);
}
// 更新 target 中的 e 为后继
target.datas.set(index, node.datas.get(0));
// 实际被删除的关键码所在的节点及其索引
target = node;
index = 0;
}
// 此时,target 必然位于最底层
target.datas.remove(index);
target.children.remove(index + 1);
// 如果下溢,则解决之
solveUnderflow(target);
}
// 解决下溢情况
private void solveUnderflow(BNode<T> node) {
// 未下溢
if (node.children.size() >= ((order + 1) >> 1)) {
return;
}
// node 之父
BNode<T> parent = node.parent;
// 已至树根:没有孩子下限
if (parent == null) {
// 若树根 node 不含关键码,但却含有(唯一)一个非空孩子,则其可被删除
if (node.datas.size() == 0 && node.children.get(0) != null) {
root = node.children.get(0);
root.parent = null;
node.children.set(0, null);
}
return;
}
// 确定 node 是 parent 的第几个孩子
int index = 0;
while (parent.children.get(index) != node) {
index++;
}
// 情况1:向左兄弟借关键码
if (index > 0) {
BNode<T> leftSilbing = parent.children.get(index - 1);
// 若左兄弟足够胖
if (leftSilbing.children.size() > ((order + 1) >> 1)) {
// parent 借关键码给 node
// 左兄弟借关键码给 parent
node.datas.add(0, parent.datas.get(index - 1));
parent.datas.add(index - 1, leftSilbing.datas.remove(leftSilbing.datas.size() - 1));
node.children.add(0, leftSilbing.children.remove(leftSilbing.children.size() - 1));
// 新爹
if (node.children.get(0) != null) {
node.children.get(0).parent = node;
}
return;
}
}
// 至此,左兄弟要么为空,要么太“瘦”
// 情况2:向右兄弟借关键码
if (index < parent.children.size() - 1) {
BNode<T> rightSibling = parent.children.get(index + 1);
// 若右兄弟足够胖
if (rightSibling.children.size() > ((order + 1) >> 1)) {
// parent 借关键码给 node
// 右兄弟借关键码给 parent
node.datas.add(node.datas.size(), parent.datas.get(index));
parent.datas.add(index, rightSibling.datas.remove(0));
node.children.add(node.children.size(), rightSibling.children.remove(0));
// 新爹
if (node.children.get(node.children.size() - 1) != null) {
node.children.get(node.children.size() - 1).parent = node;
}
return;
}
}
// 情况3:左、右兄弟要么为空(但不可能同时),要么都太“瘦” ——合并两者
if (index > 0) { // 与左兄弟合并
BNode<T> leftSibling = parent.children.get(index - 1);
leftSibling.datas.add(leftSibling.datas.size(), parent.datas.remove(index - 1));
parent.children.remove(index);
// 将 node 中的数据、指针移到左兄弟
leftSibling.children.add(leftSibling.children.size(), node.children.remove(0));
if (leftSibling.children.get(leftSibling.children.size() - 1) != null) {
leftSibling.children.get(leftSibling.children.size() - 1).parent = leftSibling;
}
while (!node.datas.isEmpty()) {
leftSibling.datas.add(leftSibling.datas.size(), node.datas.remove(0));
leftSibling.children.add(leftSibling.children.size(), node.children.remove(0));
if (leftSibling.children.get(leftSibling.children.size() - 1) != null) {
leftSibling.children.get(leftSibling.children.size() - 1).parent = leftSibling;
}
}
}
else { // 与右兄弟合并
BNode<T> rightSibling = parent.children.get(index + 1);
rightSibling.datas.add(0, parent.datas.remove(index));
parent.children.remove(index);
// 将 node 中的数据、指针移到右兄弟
rightSibling.children.add(0, node.children.remove(node.children.size() - 1));
if (rightSibling.children.get(0) != null) {
rightSibling.children.get(0).parent = rightSibling;
}
while (!node.datas.isEmpty()) {
rightSibling.datas.add(0, node.datas.remove(node.datas.size() - 1));
rightSibling.children.add(0, node.children.remove(node.children.size() - 1));
if (rightSibling.children.get(0) != null) {
rightSibling.children.get(0).parent = rightSibling;
}
}
}
// 可能父节点也下溢了
solveUnderflow(parent);
}
来源:CSDN
作者:W.T.F.
链接:https://blog.csdn.net/fcku_88/article/details/104070009