平衡搜索树——B-树小记
创始人
2024-03-02 09:35:54

文章目录

  • B- 树系列
    • 定义
    • 插入规则
    • 代码
      • B-树结点定义
      • 查找key在结点哪个子树
      • 插入+分裂

B- 树系列

定义

B-树是一棵多叉 平衡 搜索树(不是二叉树,B-树中每个结点中可以有多个key,也有多个孩子)

B-树中每个结点在实现时人为规定一个key的上限(KEY_LIMIT = 4)
B-树结点中的key是按照key的顺序进行保存
我们把key的个数记为size,则child的个数—定是size + 1

在这里插入图片描述

插入规则

1.所有插入过程,只能发生在叶子上,不能发生在非叶子结点上
2.插入时,每个结点的kev的数量是有上限的
(1) 未达上限,插入结束
(2) 达到上限,则进行“结点分裂”过程
3.叶子结点的分裂
如下情况中,插入25后,结点中key的数量超过上限4,引发分裂,分裂过程如下:
在这里插入图片描述

步骤:

1. 找到key的中间值(25)把下标记为index

1)不一定就是新插入的结点
2)偶数的话,前一个或者后一个都可以(一般都实现成奇数的形式)

2. 把当前结点分成两个结点(分家)

[0 , index) key留在当前结点中
[index]提到父结点中
(index, size)搬到新结点中

在这里插入图片描述

代码

B-树结点定义

/*** 暂时只考虑:* 1. key 模型而不是 key-value 模型* 2. key 类型定义为 long 类型* 3. public*/
public class BTreeNode {/*** 先规定,一个 B-树结点中,能存的 key 的数量的上限,要求 size(key) <= KEY_LIMIT* 在实际的应用中,一般 KEY_LIMIT 都是 1000 数量级以上的*/public static final int KEY_LIMIT = 4;/*** 去保存结点中所有的 key* 使用数组(线性结构)进行维护,需要维护 key 与 key 之间的有序性* 从空间利用率角度考虑,new long[KEY_LIMIT] 就足够了* 但我们多申请一个空间,方便进行结点分裂代码的编写(用于保存即将分裂状态下的所有 key)*/public long[] keyList = new long[KEY_LIMIT + 1];/*** 记录目前拥有的 key 的个数*/public int size = 0;/*** 去维护结点中所有的 child* 同理,多申请一个空间* 本来,child 就比 key 多一个* new BTreeNode[KEY_LIMIT + 2]*/public BTreeNode[] childList = new BTreeNode[KEY_LIMIT + 2];/*** 我们不维护 child 的个数,因为 其个数一定是 size + 1*//*** 保存该结点的父结点,null 代表该结点是根结点*/public BTreeNode parent = null;@Overridepublic String toString() {return String.format("%d: %s", size, Arrays.toString(Arrays.copyOf(keyList, size)));}
}

在这里插入图片描述

查找key在结点哪个子树

public BTreeNode findKey(long key) {if (key > keyList[size - 1]) {// child 比 key 多一个// keyList 的最后一个元素位于 [size - 1]// childList 的最后一个元素位于 [size]return childList[size];}for (int i = 0; i < size; i++) {if (key == keyList[i]) {// key 就在当前结点中return this;} else if (key < keyList[i]) {// 由于我们是从左向右遍历的// 所以 keyList[i] 中的元素是 "第一个" 大于 key 的关键字元素return childList[i];} else {// key > keyList[i] 的情况下,我们什么都不需要做,继续遍历查找// 第一个 大于 key 的元素}}// 我们的代码组织方式使得,理论上,不应该走到这个位置// 但 java 编译器分析不出来,所以,为了让编译器不报错,我们随便返回一个值// 这个 return 没有任何执行的意义return null;}

测试查找Key的代码
包含测试用例分析

public static void testFindKey() {BTreeNode node = new BTreeNode();node.keyList[0] = 10;node.keyList[1] = 20;node.keyList[2] = 30;node.keyList[3] = 40;node.size = 4;BTreeNode 甲 = new BTreeNode();BTreeNode 乙 = new BTreeNode();BTreeNode 丙 = new BTreeNode();BTreeNode 丁 = new BTreeNode();BTreeNode 戊 = new BTreeNode();node.childList[0] = 甲;node.childList[1] = 乙;node.childList[2] = 丙;node.childList[3] = 丁;node.childList[4] = 戊;/*** 测试用例:* 执行动作           期望结果* < 10              期望   甲* == 10             期望   node* == 20             期望   node* (10, 20)          期望   乙* (20, 30)          期望   丙* (30, 40)          期望   丁* > 40              期望   戊* == 40             期望   node*/// 引用1 == 引用2: 判断两个引用是否指向同一个对象System.out.println(node.findKey(3) == 甲);System.out.println(node.findKey(10) == node);System.out.println(node.findKey(20) == node);System.out.println(node.findKey(13) == 乙);System.out.println(node.findKey(29) == 丙);System.out.println(node.findKey(31) == 丁);System.out.println(node.findKey(300) == 戊);System.out.println(node.findKey(40) == node);}

插入+分裂

插入步骤:

将key插入结点的keyList中,并且维持有序性 ——类似插入排序的做法
在这里插入图片描述

/*** 用来作为插入的返回值类型的类*/public static class InsertResult {public long key;        // 分裂出的 keypublic BTreeNode node;  // 分裂出的新结点}/*** 将指定关键字 key,插入叶子结点中* @param key 关键字* @return* null: 插入之后不需要分裂* !null: 发生了分裂*/public InsertResult insertKey(long key) {// 1. 如果 key 按序插入到 keyList 中insertIntoKeyList(key);size++;// 2. 判断是否需要进行分裂// 3. 如果有必要,进行结点分裂if (shouldSplit()) {return splitLeaf();}return null;}/*** 将指定关键字 key 和 chid,插入非叶子结点中* @param key 关键字* @param child > key 的孩子* @return*/public InsertResult insertKeyWithChild(long key, BTreeNode child) {// 1. 如果 key 按序插入到 keyList 中int index = insertIntoKeyList(key);// 2. 根据 index 进行 child 的插入,暂时没有进行 size 的变动// child[0, size]// [0, index] 不动// [index + 1] 要插入 child 的下标// [index + 1, size] 搬到 [index + 2, size + 1]  元素个数 size - (index + 1) + 1 == size - indexSystem.arraycopy(childList, index + 1, childList, index + 2, size - index);childList[index + 1] = child;// 这个时候再让 size++size++;// 2. 判断是否需要进行分裂// 3. 如果有必要,进行结点分裂if (shouldSplit()) {return splitNotLeaf();}return null;}public InsertResult splitNotLeaf() {// 因为一会儿 this.size 在中途会变化,先提前记录int size = this.size;InsertResult result = new InsertResult();BTreeNode node = new BTreeNode();// 1. 找到 keyList 的中间位置int index = size / 2;// 2. 保存需要分裂出的 keyresult.key = keyList[index];/*** 3. 处理 key 的分裂* 哪些 key 应该留在当前结点中      [0, index)    一共有 index 个* 哪个 key 是分裂出的 key         [index]* 哪些 key 应该在新结点中         (index, size)  一共有 size - index - 1*/// 先把 (index, size) 位置的 key 搬到新结点中System.arraycopy(keyList, index + 1, node.keyList, 0, size - index - 1);node.size = size - index - 1;// 把 [index, size) 所有 key 重置为默认值 (0),重置 sizeArrays.fill(keyList, index, size, 0);this.size = index;/*** 4. 我们对非叶子结点进行分裂,非叶子结点有孩子,所以也进行分裂*/System.arraycopy(this.childList, index + 1, node.childList, 0, size - index);Arrays.fill(this.childList, index + 1, size + 1, null);for (int i = 0; i < size - index; i++) {node.childList[i].parent = node;}/*** 5. 处理分裂的 parent 的问题* 1) this.parent 不变,分裂不影响父子关系* 2) node.parent 和 this 是同一个父亲*/node.parent = this.parent;result.node = node;return result;}public InsertResult splitLeaf() {// 因为一会儿 this.size 在中途会变化,先提前记录int size = this.size;InsertResult result = new InsertResult();BTreeNode node = new BTreeNode();// 1. 找到 keyList 的中间位置int index = size / 2;// 2. 保存需要分裂出的 keyresult.key = keyList[index];/*** 3. 处理 key 的分裂* 哪些 key 应该留在当前结点中      [0, index)    一共有 index 个* 哪个 key 是分裂出的 key         [index]* 哪些 key 应该在新结点中         (index, size)  一共有 size - index - 1*/// 先把 (index, size) 位置的 key 搬到新结点中System.arraycopy(keyList, index + 1, node.keyList, 0, size - index - 1);node.size = size - index - 1;// 把 [index, size) 所有 key 重置为默认值 (0),重置 sizeArrays.fill(keyList, index, size, 0);this.size = index;/*** 4. 我们对叶子结点进行分裂,叶子结点没有孩子 childList[...] = null,不需要分裂*//*** 5. 处理分裂的 parent 的问题* 1) this.parent 不变,分裂不影响父子关系* 2) node.parent 和 this 是同一个父亲*/node.parent = this.parent;result.node = node;return result;}public boolean shouldSplit() {return size > KEY_LIMIT;}public int insertIntoKeyList(long key) {// keyList 中有效的下标范围是 [0, size)// 从后向前遍历 [size - 1, 0]int i;for (i = size - 1; i >= 0; i--) {if (keyList[i] < key) {// 第一次碰到了 keyList[i] < keybreak;}// 没有 keyList[i] == key// 继续遍历查找,同时将数据向后搬一格keyList[i + 1] = keyList[i];}// 1. i == -1// 2. keyList[i] < key// 不管怎么样,key 都应该放在 [i + 1]keyList[i + 1] = key;return i + 1;}
import btree.BTreeNode.InsertResult;/*** B-树定义*/
public class BTree {/*** B-树的根结点*/public BTreeNode root = null;/*** B-树中的 key 的个数* 备注:和二叉搜索树不同,B-树的 key 的个数 不等于 结点的个数*/public int size = 0;public boolean insert(long key) {if (insertWithoutSize(key)) {size++;return true;}return false;}public boolean insertWithoutSize(long key) {// 1. 是否是空树if (root == null) {// root = 构建一个包含 key 的结点;root = new BTreeNode();root.keyList[0] = key;root.size = 1;return true;}// 2. 不是空树的情况BTreeNode current = root;while (true) {BTreeNode node = current.findKey(key);if (node == current) {// key 就在 current 结点中// 不运行重复 key 的插入,插入失败return false;} else if (node == null) {// current 就是叶子结点// current 就是接下来要插入 key 的结点break;} else {// 找到了一个孩子 并且 current 不是叶子// 规则:插入必须发生在叶子结点上current = node;}}// 进行 key 的插入// current 就是我们要进行 key 插入的结点// current 此时一定是叶子InsertResult r = current.insertKey(key);while (true) {if (r == null) {// 插入过程中,没有发生结点分裂,直接退出return true;}// 说明发生分裂了// 需要把新分裂出的 key 和 分裂出的结点,插入到 current.parent 中BTreeNode parent = current.parent;if (parent == null) {// 说明 current 是根结点// 根结点发生分裂了// 需要一个新的根结点,来保存分裂出的 keyroot = new BTreeNode();root.keyList[0] = r.key;root.size = 1;root.childList[0] = current;root.childList[1] = r.node;// 由于原来 current 是根结点,所以其 parent == null// 自然分裂出的结点,跟着也是 null// 所以为他们设置新的父结点current.parent = r.node.parent = root;return true;}// 不是根结点分裂了r = parent.insertKeyWithChild(r.key, r.node);current = parent;}}
}

相关内容

热门资讯

北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...