B-Tree插入和删除的Java实现
B-Tree插入和删除的Java实现
一、一颗非空m阶B-Tree的性质
- 除根结点以外的每个结点的孩子引用最多存在m个,关键码最多存在m - 1个;除根结点以外的每个结点的孩子引用至少存在⌈m / 2⌉个,关键码至少存在⌈m / 2⌉ - 1个。
- 一颗非空B-Tree的根结点至少存在2个孩子引用(注意:一颗非空B-Tree的根结点最少存在的孩子引用数不受m限制,且最少允许存在2个孩子引用!)。
- 每个结点的关键码遵循“左小右大”排序存放,即关键码集合中靠左的关键码小于靠右侧的关键码。
- 所有叶子结点存在同一层(可以看出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实现的更多相关文章
- 二叉搜索树Java实现(查找、插入、删除、遍历)
由于最近想要阅读下 JDK1.8 中 HashMap 的具体实现,但是由于 HashMap 的实现中用到了红黑树,所以我觉得有必要先复习下红黑树的相关知识,所以写下这篇随笔备忘,有不对的地方请指出- ...
- Java创建二叉搜索树,实现搜索,插入,删除操作
Java实现的二叉搜索树,并实现对该树的搜索,插入,删除操作(合并删除,复制删除) 首先我们要有一个编码的思路,大致如下: 1.查找:根据二叉搜索树的数据特点,我们可以根据节点的值得比较来实现查找,查 ...
- Java 集合与队列的插入、删除在并发下的性能比较
这两天在写一个java多线程的爬虫,以广度优先爬取网页,设置两个缓存: 一个保存已经访问过的URL:vistedUrls 一个保存没有访问过的URL:unVistedUrls 需要爬取的数据量不大,对 ...
- 数据结构Java实现03----单向链表的插入和删除
文本主要内容: 链表结构 单链表代码实现 单链表的效率分析 一.链表结构: (物理存储结构上不连续,逻辑上连续:大小不固定) 概念: 链式存储结构是基于指针实现的.我们把一个数据 ...
- 二叉查找树的查找、插入和删除 - Java实现
http://www.cnblogs.com/yangecnu/p/Introduce-Binary-Search-Tree.html 作者: yangecnu(yangecnu's Blog on ...
- Java中数组的插入,删除,扩张
Java中数组是不可变的,但是可以通过本地的arraycop来进行数组的插入,删除,扩张.实际上数组是没变的,只是把原来的数组拷贝到了另一个数组,看起来像是改变了. 语法: System.arrayc ...
- 数据结构Java实现02----单向链表的插入和删除
文本主要内容: 链表结构 单链表代码实现 单链表的效率分析 一.链表结构: (物理存储结构上不连续,逻辑上连续:大小不固定) 概念: 链式存储结构是基于指针实现的.我们把一个数据 ...
- 【线性表基础】顺序表和单链表的插入、删除等基本操作【Java版】
本文表述了线性表及其基本操作的代码[Java实现] 参考书籍 :<数据结构 --Java语言描述>/刘小晶 ,杜选主编 线性表需要的基本功能有:动态地增长或收缩:对线性表的任何数据元素进行 ...
- 二叉平衡树AVL的插入与删除(java实现)
二叉平衡树 全图基础解释参考链接:http://btechsmartclass.com/data_structures/avl-trees.html 二叉平衡树:https://www.cnblogs ...
随机推荐
- Metasploit Framework(MSF)的使用
目录 Metasploit 安装Metasploit 漏洞利用(exploit) 攻击载荷(payload) Meterpreter MS17_010(永恒之蓝) 辅助模块(探测模块) 漏洞利用模块 ...
- hdu5251最小矩形覆盖
题意(中问题直接粘吧)矩形面积 Problem Description 小度熊有一个桌面,小度熊剪了很多矩形放在桌面上,小度熊想知道能把这些矩形包围起来的面积最小的矩形的面积是多少. Input ...
- Intel汇编程序设计-整数算术指令(中)
7.3 移位和循环移位的应用 7.3.1 多双字移位 要对扩展精度整数(长整数)进行移位操作,可把它划分为字节数组.字数组或双字数组,然后再对该数组进行移位操作.在内存中存储数字时通常采用的方式是 ...
- 神经网络与机器学习 笔记—LMS(最小均方算法)和学习率退火
神经网络与机器学习 笔记-LMS(最小均方算法)和学习率退火 LMS算法和Rosenblatt感知器算法非常想,唯独就是去掉了神经元的压制函数,Rosenblatt用的Sgn压制函数,LMS不需要压制 ...
- JVM垃圾回收器总结
常见七种垃圾回收器以及使用的垃圾回收算法总结:
- 10-10-12分页机制(xp)
虚拟地址到物理地址 虚拟地址空间就是32位系统的那4GB,这4GB空间的地址称为虚拟地址.虚拟地址经过分段机制后转化为线性地址,一般虚拟地址都等于线性地址,因为大多数段寄存器的基地址都为0,只有FS段 ...
- 1.初级篇——最基础的"穷竭搜索”
A.Lake Counting(POJ 2386) 题意: 由于最近的降雨,农夫约翰田地的各个地方都有水汇聚,用N x M(1 <= N <= 100; 1 <= M <= 1 ...
- [并发编程 - 多线程:信号量、死锁与递归锁、时间Event、定时器Timer、线程队列、GIL锁]
[并发编程 - 多线程:信号量.死锁与递归锁.时间Event.定时器Timer.线程队列.GIL锁] 信号量 信号量Semaphore:管理一个内置的计数器 每当调用acquire()时内置计数器-1 ...
- Spring循环依赖问题的解决
循环依赖问题 一个bean的创建分为如下步骤: 当创建一个简单对象的时候,过程如下: 先从单例池中获取bean,发现无 a 创建 a 的实例 为 a 赋值 把 a 放到单例池中 当创建一个对象并且其中 ...
- who -b
~]# who -b 系统引导 2020-05-03 19:57[root@localhost ~]# who -r 运行级别 5 2020-05-03 19:58