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 ...
随机推荐
- PowerShell-2.解决禁止本地执行脚本
现象 直接找到XXX.ps1右键 使用PowerShell运行没反应,然后打开PowerShel把脚本推进去显示这个: 原因是因为脚本执行权限默认是Restricted,这个是禁止执行任何本地脚本. ...
- c#-全局键盘钩子
using System; using System.Collections.Generic; using System.Text; using System.Windows.Forms; using ...
- Win64 驱动内核编程-21.DKOM隐藏和保护进程
DKOM隐藏和保护进程 主要就是操作链表,以及修改节点内容. DKOM 隐藏进程和保护进程的本质是操作 EPROCESS 结构体,不同的系统用的时候注意查下相关定义,确定下偏移,下面的数据是以win7 ...
- Python练习3-XML-RPC实现简单的P2P文件共享
XML-RPC实现简单的P2P文件共享 先来个百度百科: XML-RPC的全称是XML Remote Procedure Call,即XML(标准通用标记语言下的一个子集)远程过程调用.它是一套允许运 ...
- 深入浅出带你玩转sqlilabs(二)
MYSQL高权限注入 mysql跨库注入 详情请看上一篇:深入浅出带你玩转sqlilabs(一) mysql文件操作注入-sqlilabs less7 可能用到的函数: into outfile()函 ...
- Linux安装与使用FTP服务-vsftpd
简介 vsftpd 是"very secure FTP daemon"的缩写,安全性是它的一个最大的特点.vsftpd 是一个 UNIX 类操作系统上运行的服务器的名字,它可以运行 ...
- SpringBoot 项目 部署 jar方式
SpringBoot部署-jar方式 步骤1部署方式 Springboot 和我们之前学习的web 应用程序不一样,其本质上是一个 Java 应用程序,那么又如何部署呢? 通常来说,Springbo ...
- 21.Quick QML-FileDialog、FolderDialog对话框
1.FileDialog介绍 Qt Quick中的FileDialog文件对话框支持的平台有: 笔者使用的是Qt 5.8以上的版本,模块是import Qt.labs.platform 1.1. 它的 ...
- OO随笔之追求完美的第三单元——初试JML
前言 这一章的JML比较简单,那么大家的关注点自然地移到了性能优化上.于是大家一股脑地去利用各种数据结构去做时间上的优化(当然很多人最后还是倒在了正确性上),故称追求完美的一单元.当然这也是得益于JM ...
- Linux_搭建NFS服务(基础)
[RHEL8]-NFSserver :[Centos7]-NFSclient !!!测试环境我们首关闭防火墙和selinux(NFSserver和NFSclient都需要) [root@localho ...