B-Tree插入和删除的Java实现

一、一颗非空m阶B-Tree的性质

  1. 除根结点以外的每个结点的孩子引用最多存在m个,关键码最多存在m - 1个;除根结点以外的每个结点的孩子引用至少存在⌈m / 2⌉个,关键码至少存在⌈m / 2⌉ - 1个。
  2. 一颗非空B-Tree的根结点至少存在2个孩子引用(注意:一颗非空B-Tree的根结点最少存在的孩子引用数不受m限制,且最少允许存在2个孩子引用!)。
  3. 每个结点的关键码遵循“左小右大”排序存放,即关键码集合中靠左的关键码小于靠右侧的关键码。
  4. 所有叶子结点存在同一层(可以看出B-Tree是一种严格平衡的多路搜索树)。

二、实现一颗可指定阶数的B-Tree(以下B-Tree源代码的插入和删除关键码功能均经过测试,无任何问题,可放心参考!)

import java.util.Arrays;
import java.util.Comparator;
import java.util.Random; /**
* B-Tree
* @param <K>
*/
public final class MultipleSearchTree<K> {
/**
* B-Tree Node
* @param <K>
*/
private static final class BTreeNode<K> {
K[] key; // 关键码数组
BTreeNode<K> parent; // 父结点
BTreeNode<K>[] ptr; // 孩子结点数组
int ptrSize; // 实际存在的孩子数量 BTreeNode(int order) {
// 关键码数组和指针数组都多分配一个存储空间 避免发生上溢时数组访问越界
this.key = (K[])new Object[order];
this.parent = null;
this.ptr = new BTreeNode[order + 1];
this.ptrSize = 1; // 默认至少拥有一个空孩子
}
} private static final int FIELD_LIMIT_ORDER_MINIMUM = 3; // 阶数字段所允许的最小取值
private static final int FIELD_LIMIT_ORDER_MAXIMUM = 65535; // 阶数字段所允许的最大取值
private static final int FIELD_DEFAULT_ORDER = 4; // 阶数字段默认取值 private final Comparator<? super K> comparator; // 关键码比较器
private final int order; // 阶数
private BTreeNode<K> root; // 树根
private int keySize; // 存储的关键码数 public MultipleSearchTree(Comparator<? super K> comparator) {
checkComparator(comparator);
this.comparator = comparator;
this.order = FIELD_DEFAULT_ORDER;
} public MultipleSearchTree(int order, Comparator<? super K> comparator) {
checkComparator(comparator);
checkOrder(order);
this.comparator = comparator;
this.order = order;
} /**
* 阶数检查
* @param order
*/
private static void checkOrder(int order) {
if (order < FIELD_LIMIT_ORDER_MINIMUM) {
System.out.println("阶数的取值过小,最小的阶数取值不能小于" + FIELD_LIMIT_ORDER_MINIMUM + "!");
} else if (order > FIELD_LIMIT_ORDER_MAXIMUM) {
System.out.println("阶数的取值过大,最小的阶数取值不能大于" + FIELD_LIMIT_ORDER_MAXIMUM + "!");
}
} /**
* 比较器检查
* @param comparator
* @param <K>
*/
private static <K> void checkComparator(Comparator<? super K> comparator) {
if (comparator == null) {
throw new NullPointerException("关键码比较器不能为空!");
}
} /**
* 移除关键码
* @param key
* @return 移除成功返回true,否则返回false。
*/
public boolean remove(K key) {
if (key == null || this.root == null) {
return false;
} final Comparator<? super K> comparator = this.comparator;
BTreeNode<K> delItr = this.root; // 关键码搜索器
int delPos = 0; // delItr.key[delPos]即为要删除的关键码 do {
delPos = Arrays.binarySearch(delItr.key, 0, delItr.ptrSize - 1, key, comparator); if (delPos < 0) {
// 不断深入
delItr = delItr.ptr[~delPos];
} else {
break;
}
} while (delItr != null);
// 关键码不存在 故无法删除
if (delItr == null) return false;
// 若关键码所在结点存在直接后继 则将实际删除关键码的结点替换为直接后继
if (delItr.ptr[delPos + 1] != null) {
BTreeNode<K> original = delItr; delItr = delItr.ptr[delPos + 1];
while (delItr.ptr[0] != null) {
delItr = delItr.ptr[0];
}
original.key[delPos] = delItr.key[0];
delPos = 0;
}
// 移除关键码
System.arraycopy(delItr.key, delPos + 1, delItr.key, delPos, (delItr.ptrSize - 1) - (delPos + 1));
// 此处不必移动孩子数组进行覆盖删除 因为删除必然发生在外部结点 故移动空结点是耗时且无意义的
// 更新属性
delItr.ptrSize--;
// 若发生了下溢则有必要进行修复
if (delItr.ptrSize < Math.ceil(this.order / 2)) {
fixUnderflow(delItr);
} this.keySize--; return true;
} /**
* 下溢修复
* @param fixItr
*/
private void fixUnderflow(BTreeNode<K> fixItr) {
final Comparator<? super K> comparator = this.comparator; while (fixItr.ptrSize < Math.ceil(this.order / 2)) {
BTreeNode<K> parent = fixItr.parent; // 若已上升至根节点
if (parent == null) {
// 对于根节点而言不必满足至少拥有ceil(m/2)个孩子
if (fixItr.ptrSize < 2 && fixItr.ptr[0] != null) {
this.root = fixItr.ptr[0];
this.root.parent = null;
}
return;
} int fixItrPos = 0; // 定位fixItr是其父亲的第几个孩子(下标)
while (parent.ptr[fixItrPos] != fixItr) {
++fixItrPos;
} /*
* 注意:这里判断fixItrPos不是一个最左侧孩子又或者不是最右侧孩子的目的是为了防止关键码数组parent.key访问越界!
* 就以下面这个分支"if (0 < fixItrPos)"为例。
* 假设此时左兄弟的关键码数量恰好满足"leftSibling.ptrSize - 1 >= Math.ceil(this.order / 2)"
* 对于这种情况的修复很简单:
* 1、将介于左兄弟leftSibling和fixItr的父节点关键码借给fixItr
* 2、将左兄弟的最大(最右侧)的关键码填补父节点借出关键码的位置
* 3、最后将左兄弟的最右侧孩子结点转移至fixItr作为最左侧孩子即可!
* 但关键就在于第1步的“介于左兄弟leftSibling和fixItr的父节点关键码”的选取
* 貌似咋看好像是parent.key[fixItrPos - 1]和parent.key[fixItrPos]
* 都可以作为中间关键码的选取??但其实并不是 简单思索便可很清楚的知道
* 如果此时我们的fixItrPos恰好指向的位置是一个最右侧的关键码,而此时如果我们使用[fixItrPos]
* 对parent.key进行访问就会恰好超出关键码的最大数组范围一个元素位置,从而引发数组访问越界异常!
* 因此只能是选取parent.key[fixItrPos - 1]作为介于两者之间的中间关键码!
* 即使fixItrPos是这个分支所允许的最小值,那也最小只可能是1。
* 也就是说其左兄弟是parent.ptr[0],此时若使用parent.key[fixItrPos - 1]
* 取中间关键码那也只会取到最靠左侧的关键码,不会发生越界情况!
* (下面的“if (fixItrPos < parent.ptrSize - 1)”情况是对称的,同理!主要是为了防止访问越界!)
*/
// fixItr不是一个最左侧孩子
if (0 < fixItrPos) {
BTreeNode<K> leftSibling = parent.ptr[fixItrPos - 1]; // 若关键码充足
if (leftSibling.ptrSize - 1 >= Math.ceil(this.order / 2)) {
// 父亲借出关键码给fixItr
System.arraycopy(fixItr.key, 0, fixItr.key, 1, fixItr.ptrSize - 1);
fixItr.key[0] = parent.key[fixItrPos - 1]; // 左兄弟的最大关键码填补父亲借出的关键码位置
parent.key[fixItrPos - 1] = leftSibling.key[(leftSibling.ptrSize - 1) - 1]; // 左兄弟多出的最右侧孩子转移至fixItr
System.arraycopy(fixItr.ptr, 0, fixItr.ptr, 1, fixItr.ptrSize);
fixItr.ptr[0] = leftSibling.ptr[leftSibling.ptrSize - 1];
if (fixItr.ptr[0] != null) {
fixItr.ptr[0].parent = fixItr;
} // 更新属性
fixItr.ptrSize++;
leftSibling.ptrSize--; return;
}
} // fixItr不是一个最右侧孩子
if (fixItrPos < parent.ptrSize - 1) {
BTreeNode<K> rightSibling = parent.ptr[fixItrPos + 1]; if (rightSibling.ptrSize - 1 >= Math.ceil(this.order / 2)) {
// 父亲借出关键码给fixItr
fixItr.key[fixItr.ptrSize - 1] = parent.key[fixItrPos]; // 右兄弟的最小关键码填补父亲借出的关键码位置
parent.key[fixItrPos] = rightSibling.key[0];
System.arraycopy(rightSibling.key, 1, rightSibling.key, 0, (rightSibling.ptrSize - 1) - 1); // 右兄弟多出的最左侧孩子转移至fixItr
fixItr.ptr[fixItr.ptrSize] = rightSibling.ptr[0];
System.arraycopy(rightSibling.ptr, 1, rightSibling.ptr, 0, rightSibling.ptrSize - 1);
if (fixItr.ptr[fixItr.ptrSize] != null) {
fixItr.ptr[fixItr.ptrSize].parent = fixItr;
} // 更新属性
fixItr.ptrSize++;
rightSibling.ptrSize--; return;
}
} if (0 < fixItrPos) {
BTreeNode<K> leftSibling = parent.ptr[fixItrPos - 1]; // 父节点关键码转移至左兄弟末尾
leftSibling.key[leftSibling.ptrSize - 1] = parent.key[fixItrPos - 1];
System.arraycopy(parent.key, fixItrPos, parent.key, fixItrPos - 1, (parent.ptrSize - 1) - fixItrPos); // 将fixItr从parent的孩子集合中移除
System.arraycopy(parent.ptr, fixItrPos + 1, parent.ptr, fixItrPos, parent.ptrSize - (fixItrPos + 1));
// 更新属性
parent.ptrSize--; // 将fixItr的全部关键码转移至左兄弟
System.arraycopy(fixItr.key, 0, leftSibling.key, leftSibling.ptrSize, fixItr.ptrSize - 1); // 将fixItr的全部孩子转移至左兄弟
if (fixItr.ptr[0] != null) {
System.arraycopy(fixItr.ptr, 0, leftSibling.ptr, leftSibling.ptrSize, fixItr.ptrSize);
for (int i = 0; i < fixItr.ptrSize; ++i) {
fixItr.ptr[i].parent = leftSibling;
}
} // 更新属性
leftSibling.ptrSize += fixItr.ptrSize;
} else {
BTreeNode<K> rightSibling = parent.ptr[fixItrPos + 1]; // 父节点转移关键码至fixItr
fixItr.key[fixItr.ptrSize - 1] = parent.key[fixItrPos];
System.arraycopy(parent.key, fixItrPos + 1, parent.key, fixItrPos, (parent.ptrSize - 1) - (fixItrPos + 1)); // 将rs从parent的孩子集合中移除
System.arraycopy(parent.ptr, fixItrPos + 2, parent.ptr, fixItrPos + 1, parent.ptrSize - (fixItrPos + 2));
// 更新属性
parent.ptrSize--; // 将右兄弟的全部关键码转移至fixItr
System.arraycopy(rightSibling.key, 0, fixItr.key, fixItr.ptrSize, rightSibling.ptrSize - 1); // 将右兄弟的全部孩子转移至fixItr
if (rightSibling.ptr[0] != null) {
System.arraycopy(rightSibling.ptr, 0, fixItr.ptr, fixItr.ptrSize, rightSibling.ptrSize);
for (int i = 0; i < rightSibling.ptrSize; ++i) {
rightSibling.ptr[i].parent = fixItr;
}
} // 更新属性
fixItr.ptrSize += rightSibling.ptrSize;
} // 上溯
fixItr = parent;
}
} /**
* 添加新的关键码
* @param key 添加的关键码
* @return 添加成功返回true,否则返回false。
*/
public boolean add(K key) {
// 若key先前并不存在当前B-Tree中 则必然会添加成功
if (key == null) {
return false;
} else if (this.root != null) {
final Comparator<? super K> comparator = this.comparator;
BTreeNode<K> iterator = this.root;
BTreeNode<K> insert = null;
int insPos = 0; do {
insPos = Arrays.binarySearch(iterator.key, 0, iterator.ptrSize - 1, key, comparator); if (insPos < 0) {
// 不断深入 并由insert记录最终的插入结点
insert = iterator;
iterator = iterator.ptr[~insPos];
} else {
// 若找到key已存在当前B-Tree中则中断插入操作
return false;
}
} while (iterator != null);
// 取得正确的关键码插入位置
insPos = ~insPos;
// 将insert.key[insPos]起始的往后关键码整体向后移动一位 空出[insPos]位置供插入使用
System.arraycopy(insert.key, insPos, insert.key, insPos + 1, (insert.ptrSize - 1) - insPos);
insert.key[insPos] = key;
// 此处无需将insert.ptr[insPos + 1]起始的往后关键码整体向后移动 因为插入必然发生在外部结点 移动空孩子无意义且费效率
insert.ptrSize++;
// 若有需要需进行上溢修复
if (insert.ptrSize > this.order) {
fixOverflow(insert);
}
} else {
this.root = new BTreeNode<>(this.order);
this.root.key[0] = key;
this.root.ptrSize = 2;
} this.keySize++; return true;
} /**
* 上溢修复
* @param fixItr
*/
private final void fixOverflow(BTreeNode<K> fixItr) {
final Comparator<? super K> comparator = this.comparator; while (fixItr.ptrSize > this.order) {
BTreeNode<K> parent = fixItr.parent; // 父亲
BTreeNode<K> right = new BTreeNode<>(this.order); // 分裂出来的右孩子
int midKeyIndex = (fixItr.ptrSize - 1) >> 1; // fixItr.key的中间关键码位置下标 // fixItr.key[midKeyIndex]作为被提升至父节点的关键码
// fixItr.key从[midKeyIndex + 1]起始的关键码全部转移至right.key
System.arraycopy(fixItr.key, midKeyIndex + 1, right.key, 0, (fixItr.ptrSize - 1) - (midKeyIndex + 1));
// fixItr.ptr从[midKeyIndex + 1]起始的孩子全部转移至right.ptr
if (fixItr.ptr[0] != null) {
System.arraycopy(fixItr.ptr, midKeyIndex + 1, right.ptr, 0, fixItr.ptrSize - (midKeyIndex + 1));
// 重定向父亲引用
for (int i = midKeyIndex + 1; i < fixItr.ptrSize; ++i) {
fixItr.ptr[i].parent = right;
}
}
// 更新属性
right.ptrSize = fixItr.ptrSize - (midKeyIndex + 1);
fixItr.ptrSize -= right.ptrSize; // 若发生上溢的是根节点
if (parent == null) {
this.root = new BTreeNode<>(this.order);
this.root.key[0] = fixItr.key[midKeyIndex];
this.root.ptr[0] = fixItr;
this.root.ptr[1] = right;
fixItr.parent = this.root;
right.parent = this.root;
// 更新属性
this.root.ptrSize = 2;
return;
} // 若发生上溢的非根节点
// 确定中间关键码在parent.key中的提升位置
int risePos = ~Arrays.binarySearch(parent.key, 0, parent.ptrSize - 1, fixItr.key[midKeyIndex], comparator); // 腾出parent.key[risePos]以供存储提升的关键码
System.arraycopy(parent.key, risePos, parent.key, risePos + 1, (parent.ptrSize - 1) - risePos);
parent.key[risePos] = fixItr.key[midKeyIndex];
// 腾出parent.ptr[risePos + 1]以供存储分裂出来的右孩子
System.arraycopy(parent.ptr, risePos + 1, parent.ptr, risePos + 2, parent.ptrSize - (risePos + 1));
parent.ptr[risePos + 1] = right;
right.parent = parent;
// 更新属性
parent.ptrSize++; // 上溯
fixItr = parent;
}
} } final class Run {
public static void main(String[] args) {
MultipleSearchTree<Double> bTree = new MultipleSearchTree<>(4, Double::compareTo);
Random random = new Random();
Double[] key = new Double[100000]; for (int j = 0; j < 100; ++j) {
for (int i = 0; i < key.length; ++i) {
key[i] = random.nextDouble();
}
for (int i = 0; i < key.length; ++i) {
System.out.print("第" + (i + 1) + "轮:");
System.out.println(bTree.add(key[i]) ? "插入成功" : "插入失败");
}
for (int i = 0; i < key.length; ++i) {
System.out.print("第" + (i + 1) + "轮:");
System.out.println(bTree.remove(key[i]) ? "删除成功" : "删除失败");
}
}
}
}

原文章:https://blog.csdn.net/qq_44641534/article/details/112126090

B-Tree插入和删除的Java实现的更多相关文章

  1. 二叉搜索树Java实现(查找、插入、删除、遍历)

    由于最近想要阅读下 JDK1.8 中 HashMap 的具体实现,但是由于 HashMap 的实现中用到了红黑树,所以我觉得有必要先复习下红黑树的相关知识,所以写下这篇随笔备忘,有不对的地方请指出- ...

  2. Java创建二叉搜索树,实现搜索,插入,删除操作

    Java实现的二叉搜索树,并实现对该树的搜索,插入,删除操作(合并删除,复制删除) 首先我们要有一个编码的思路,大致如下: 1.查找:根据二叉搜索树的数据特点,我们可以根据节点的值得比较来实现查找,查 ...

  3. Java 集合与队列的插入、删除在并发下的性能比较

    这两天在写一个java多线程的爬虫,以广度优先爬取网页,设置两个缓存: 一个保存已经访问过的URL:vistedUrls 一个保存没有访问过的URL:unVistedUrls 需要爬取的数据量不大,对 ...

  4. 数据结构Java实现03----单向链表的插入和删除

    文本主要内容: 链表结构 单链表代码实现 单链表的效率分析 一.链表结构: (物理存储结构上不连续,逻辑上连续:大小不固定)            概念: 链式存储结构是基于指针实现的.我们把一个数据 ...

  5. 二叉查找树的查找、插入和删除 - Java实现

    http://www.cnblogs.com/yangecnu/p/Introduce-Binary-Search-Tree.html 作者: yangecnu(yangecnu's Blog on ...

  6. Java中数组的插入,删除,扩张

    Java中数组是不可变的,但是可以通过本地的arraycop来进行数组的插入,删除,扩张.实际上数组是没变的,只是把原来的数组拷贝到了另一个数组,看起来像是改变了. 语法: System.arrayc ...

  7. 数据结构Java实现02----单向链表的插入和删除

    文本主要内容: 链表结构 单链表代码实现 单链表的效率分析 一.链表结构: (物理存储结构上不连续,逻辑上连续:大小不固定)            概念: 链式存储结构是基于指针实现的.我们把一个数据 ...

  8. 【线性表基础】顺序表和单链表的插入、删除等基本操作【Java版】

    本文表述了线性表及其基本操作的代码[Java实现] 参考书籍 :<数据结构 --Java语言描述>/刘小晶 ,杜选主编 线性表需要的基本功能有:动态地增长或收缩:对线性表的任何数据元素进行 ...

  9. 二叉平衡树AVL的插入与删除(java实现)

    二叉平衡树 全图基础解释参考链接:http://btechsmartclass.com/data_structures/avl-trees.html 二叉平衡树:https://www.cnblogs ...

随机推荐

  1. 使用BurpSuite抓取HTTPS网站的数据包

    昨天面试,技术官问到了我如何使用BurpSuite抓取https网站的数据包,一时间没能回答上来(尴尬!).因为以前https网站的数据包我都是用Fiddler抓取的,Fiddlert自动帮我们配置好 ...

  2. 基于MXNET框架的线性回归从零实现(房价预测为例)

    1.基于MXNET框架的线性回归从零实现例子 下面博客是基于MXNET框架下的线性回归从零实现,以一个简单的房屋价格预测作为例子来解释线性回归的基本要素.这个应用的目标是预测一栋房子的售出价格(元). ...

  3. tp 创建文件并写入数据

    代码:1.$url = Env::get('root_path').'application/admin/test.txt'; //定义创建路径 $file = fopen($url,"w& ...

  4. 源码简析XXL-JOB的注册和执行过程

    一,前言 XXL-JOB是一个优秀的国产开源分布式任务调度平台,他有着自己的一套调度注册中心,提供了丰富的调度和阻塞策略等,这些都是可视化的操作,使用起来十分方便. 由于是国产的,所以上手还是比较快的 ...

  5. DLL注入技术(输入法注入)

    输入法注入原理 IME输入法实际就是一个dll文件(后缀为ime),此dll文件需要导出必要的接口供系统加载输入法时调用.我们可以在此ime文件的DllMain函数的入口通过调用LoadLibrary ...

  6. es6.4.0安装和配置IK+拼音插件 实现非全拼搜索

    安装IK分词器 一.进入到es的plugins文件夹创建文件夹analysis-ikmkdir analysis-ik二.下载ik压缩包文件wget https://github.com/medcl/ ...

  7. UML类关系:依赖,关联,聚合和组合

    UML图示例:(可使用StartUML来画图,小巧^_^) http://www.blogjava.net/lukangping/archive/2010/08/01/327693.html 聚合:表 ...

  8. 【BUAA软工】结对编程作业

    项目 内容 课程:2020春季软件工程课程博客作业(罗杰,任健) 博客园班级链接 作业:BUAA软件工程结对编程项目作业 作业要求 课程目标 学习大规模软件开发的技巧与方法,锻炼开发能力 作业目标 完 ...

  9. Python设计模式知多少

    设计模式 设计模式是前辈们经过相当长的一段时间的试验和错误总结出来的最佳实践.我找到的资料列举了以下这些设计模式:工厂模式.抽象工厂模式.单例模式.建造者模式.原型模式.适配器模式.桥接模式.过滤器模 ...

  10. Spring 实现策略模式--自定义注解方式解耦if...else

    策略模式 定义 定义一簇算法类,将每个算法分别封装起来,让他们可以互相替换,策略模式可以使算法的变化独立于使用它们的客户端 场景 使用策略模式,可以避免冗长的if-else 或 switch分支判断 ...