🙊前言:本文章为瑞_系列专栏之《数据结构与算法》的B树篇。由于博主是从B站黑马程序员的《数据结构与算法》学习到的相关知识,所以本系列专栏主要针对该课程进行笔记总结和拓展,文中的部分原理及图解也是来源于黑马提供的资料。本文仅供大家交流、学习及研究使用,禁止用于商业用途,违者必究!
1 什么是B树
1.1 B树的背景
B树(B-Tree)结构是一种高效存储和查询数据的方法,它的历史可以追溯到1970年代早期。B树的发明人Rudolf Bayer和Edward M. McCreight分别发表了一篇论文介绍了B树。这篇论文是1972年发表于《ACM Transactions on Database Systems》中的,题目为"Organization and Maintenance of Large Ordered Indexes"。
这篇论文提出了一种能够高效地维护大型有序索引的方法,这种方法的主要思想是将每个节点扩展成多个子节点,以减少查找所需的次数。B树结构非常适合应用于磁盘等大型存储器的高效操作,被广泛应用于关系数据库和文件系统中。
B树结构有很多变种和升级版,例如B+树,B*树和SB树等。这些变种和升级版本都基于B树的核心思想,通过调整B树的参数和结构,提高了B树在不同场景下的性能表现。
总的来说,B树结构是一个非常重要的数据结构,为高效存储和查询大量数据提供了可靠的方法。它的历史可以追溯到上个世纪70年代,而且在今天仍然被广泛应用于各种场景。
瑞:本系列介绍过的自平衡的树如AVL树、红黑树的增删改查效率在内存中能达到对数级别,属于高效率的,但是并不适合做磁盘(IO)上的增删改查,而B树就比较适合,如MySQL底层是使用的B树结构升级版B+树。
对树进行增删改查操作的效率主要取决于树的高度,树的高度越低,比较次数越少,反之亦然。如果是100W的数据,用AVL树存储,树的高度会达到20,因为log2(1000000)约等于20。最糟糕的情况下,要比较到叶子节点即20次才能找到数据,那在磁盘上要做20次读写,这是非常耗时的操作,导致效率低下。但是同样的100W数据,存储在B-树中(最小度数是500),树的高度约为3,平均效率有显著提升,所以在对于磁盘上的增删改查操作,B树更为受到欢迎。
所以B树大个特点就是:树的高度能降到非常的低,具体见下文分析
1.2 B 的含义
B-树的名称是由其发明者Rudolf Bayer提出的。Bayer和McCreight从未解释B代表什么,人们提出了许多可能的解释,比如Boeing、balanced、between、broad、bushy和Bayer等。但McCreight表示,越是思考B-trees中的B代表什么,就越能更好地理解B-trees
瑞:本文中出现的B-树和B树是同一个概念的不同称呼,并没有实质性的区别。通常,当我们提到B树时,可能会看到它被写作B-tree、B tree或者B_tree,这些都是指同一种数据结构。B树的变种如B+树和B*树,这些才是不同的概念。BTree又叫多路平衡搜索树。
1.3 B-树的度和阶
- 度(degree):指树中节点孩子数
- 阶(order):指所有节点孩子数最大值
如上图:度:节点4、2都是有两个孩子,所以度数为2;节点1、3、5、7、9 10都是没有孩子,所以度数为0;节点6 8有3个孩子,所以度数为3。而阶是指上图中树的所有节点中孩子最多的那个值,就是节点6 8有3个孩子,所以上图中树的阶数为3。
1.4 B-树的特性
一颗m叉的BTree特性如下 :
- 树中每个节点最多包含 m 个孩子,其中 m 称为B-树的阶。
- 除根节点与叶子节点外,其它每个节点至少有 ceil(m/2) 个孩子。
- 若根节点不是叶子节点,则至少有两个孩子。
- 所有的叶子节点都在同一层。
- 每个非叶子节点由 n 个关键字 key 与 n+1 个指针组成,其中 ceil(m/2)-1 <= n <= m-1 。
- 关键字按非降序排列,即节点中的第 i 个关键字大于等于第 i-1 个关键字。
- 指针 P[ i ] 指向关键字值位于第 i 个关键字和第 i+1 哥关键字之间的子树。
瑞:
根据第2条,中间节点的孩子数目是有下限的(阶数除以2,向上取整)。
第5点中的n+1个指针就是n+1个孩子即孩子数目比该节点key的数目多1,如节点2有两个孩子(节点1、节点3)、节点6 8有3个孩子(节点5、节点7、节点9 10)
最小度数要大于等于2(小于2意味着孩子只有一个,违反了B树的规则)B树中有最小度数的限制是为了保证B树的平衡特性
最小度数 * 2 = 可能拥有的最多孩子的个数
在B树中,每个节点都可以有多个子节点,这使得B树可以存储大量的键值,但也带来了一些问题。如果节点的子节点数量太少,那么就可能导致B树的高度过高,从而降低了B树的效率。此外,如果节点的子节点数量太多,那么就可能导致节点的搜索、插入和删除操作变得复杂和低效。
最小度数的限制通过限制节点的子节点数量,来平衡这些问题。在B树中,每个节点的子节点数量都必须在一定的范围内,即t到2t之间(其中t为最小度数)
一棵 B-树具有以下性质:
特性1️⃣:每个节点 x 具有
1️⃣➖1️⃣ 属性 n,表示节点 x 中 key 的个数
1️⃣➖2️⃣ 属性 leaf,表示节点是否是叶子节点
1️⃣➖3️⃣ 节点 key 可以有多个,以升序存储
特性2️⃣:每个非叶子节点中的孩子数是 n + 1、叶子节点没有孩子
特性3️⃣:最小度数t(节点的孩子数称为度)和节点中键数量的关系如下:
Column 1 | Column 2 |
---|---|
最小度数t | 键数量范围 |
2 | 1 ~ 3 |
3 | 2 ~ 5 |
4 | 3 ~ 7 |
… | … |
n | (n-1) ~ (2n-1) |
其中,当节点中键数量达到其最大值时,即 3、5、7 … 2n-1,需要分裂
特性4️⃣:叶子节点的深度都相同
瑞:即所有的叶子节点都在同一层
B-树与 2-3 树、2-3-4 树的关系
它们之间的关系:
- 2-3树是最小度数为2的B树,其中每个节点可以包含2个或3个子节点。
- 2-3-4树是最小度数为2的B树的一种特殊情况,其中每个节点可以包含2个、3个或4个子节点。
- B树是一种更加一般化的平衡树,可以适应不同的应用场景,其节点可以包含任意数量的键值,节点的度数取决于最小度数t的设定。
1.5 B-树演变过程示例
以5叉BTree为例(度为5)为例,演示B树的演变过程
key的数量:可以根据公式推导ceil(m/2)-1<=n<=m-1。因为m为5,所以得2<=n<=4,即当n>4时,中间节点分裂到父节点,两边节点分裂。
BTree可视化网站,点击进入https://www.cs.usfca.edu/~galles/visualization/BTree.html
瑞:该网站是一个旧金山大学计算机科学系的可视化项目网站,专门用于展示和解释B树(B-tree)数据结构及其操作的可视化模型。
下面就通过使用该网站对插入CNGAHEKQMFWLTZDPRXYS
数据进行演示
演变过程如下:
进入网页后,选择Max. Degree = 5选项。本例模拟的是度数为5的情况下插入CNGAHEKQMFWLTZDPRXYS
数据,后续网址和下半部分的配置不会再进行截取(均不会更改)
1️⃣ 依次插入前4个字母CNGA
后,B树演变为下图,此时B树为 4 个关键字 key(ACGN) 与 n+1 = 5 个指针组成
2️⃣ 插入H,此时 n>4,中间元素G字母向上分裂到新的节点,分裂动图如下所示(后续分裂均是同理,过程可以到网站内自己感受,后续不再放置动图)
此时B树各个key和指针的关系如下图所示(每一个节点指针比key的数量多1,后续不再展示带指针图)
3️⃣ 插入E、K、Q不需要分裂
4️⃣ 插入M,中间元素M字母向上分裂到父节点G
5️⃣ 插入F、W、L、T不需要分裂
6️⃣ 插入Z,中间元素T向上分裂到父节点中
7️⃣ 插入D,中间元素D向上分裂到父节点中,然后插入P、R、X、Y不需要分裂
8️⃣ 最后插入S,NPQR节点n>5,中间节点Q向上分裂,但分裂后父节点DGMT的n>5,中间节点M向上分裂
最终如下B树演化结果如下:
2 B-树的Java实现
1️⃣➖1️⃣ 内部节点类Node
中含有属性:
- 关键字
- 孩子们
- 有效关键字个数
- 是否是叶子节点
- 最小度数 (最小孩子数)
瑞:由于本例关键字使用基本数组
int[]
存储,所以需要有效关键字个数属性对关键字个数进行记录,如果关键字使用集合(如List
)则可以通过size
方法获取到有效关键字个数
1️⃣➖2️⃣ 内部节点类Node
中含有方法:
- 多路查找get(int key)
- 向指定索引处插入key,insertKey(int key, int index)
- 向指定索引处插入child,insertChild(Node child, int index)
1️⃣➖3️⃣ 内部节点类Node
中含有内部工具方法:
- 移除指定index处的关键字removeKey(int index)
- 移除最左边的关键字removeLeftmostKey()
- 移除最右边的关键字removeRightmostKey()
- 移除指定index处的孩子removeChild(int index)
- 移除最左边的孩子removeLeftmostChild()
- 移除最右边的孩子removeRightmostChild()
- 获取index孩子处左边的兄弟childLeftSibling(int index)
- 获取index孩子处右边的兄弟childRightSibling(int index)
- 复制当前节点的所有key和child到目标节点moveToTarget(Node target)
2️⃣➖1️⃣ B数类BTree
中含有属性:
- 根节点(Node)
- 树中节点最小度数
- 最小key数目
- 最大key数目
2️⃣➖2️⃣ B数类BTree
中含有方法:
- 是否存在某索引contains(int key)
- 新增put(int key)
- 分裂split(Node left, Node parent, int index)
- 删除remove(int key)
- 平衡balance(Node parent, Node x, int i)
2.1 B树节点类Node
实际 keys
应当改为 entries
以便同时保存 key
和 value
,本文主要是为了学习B树的思想,所以做简化实现
static class Node {
/**
* 关键字
*/
int[] keys;
/**
* 孩子们
*/
Node[] children;
/**
* 有效关键字个数(keys 中有效 key 数目)
*/
int keyNumber;
/**
* 是否是叶子节点
*/
boolean leaf = true;
/**
* 最小度数 (最小孩子数),它决定了节点中key 的最小、最大数目,分别是 t-1 和 2t-1
*/
int t;
/**
* 构造方法(给最小度数赋值
*
* @param t 最小度数(t>=2)
*/
public Node(int t) {
this.t = t;
// 最小度数*2就是最多孩子数
this.children = new Node[2 * t];
this.keys = new int[2 * t - 1];
}
public Node(int[] keys) {
this.keys = keys;
}
/**
* 打印有效Key,为了方便调试和测试,非必须
*/
@Override
public String toString() {
return Arrays.toString(Arrays.copyOfRange(keys, 0, keyNumber));
}
// 多路查找
Node get(int key) {
int i = 0;
while (i < keyNumber) {
if (keys[i] == key) {
return this;
}
if (keys[i] > key) {
break;
}
i++;
}
// 执行到此时 keys[i]>key 或 i== keyNumber
if (leaf) {
return null;
}
// 非叶子情况
return children[i].get(key);
}
// 向 keys 指定索引处插入 key
void insertKey(int key, int index) {
System.arraycopy(keys, index, keys, index + 1, keyNumber - index);
keys[index] = key;
keyNumber++;
}
// 向 children 指定索引处插入 child
void insertChild(Node child, int index) {
System.arraycopy(children, index, children, index + 1, keyNumber - index);
children[index] = child;
}
}
2.1.1 实现多路查找方法get(int key)
// 多路查找
Node get(int key) {
int i = 0;
while (i < keyNumber) {
if (keys[i] == key) {
return this;
}
if (keys[i] > key) {
break;
}
i++;
}
// 执行到此时 keys[i]>key 或 i== keyNumber
if (leaf) {
return null;
}
// 非叶子情况
return children[i].get(key);
}
2.1.2 实现插入key方法insertKey(int key, int index)
// 向 keys 指定索引处插入 key
void insertKey(int key, int index) {
System.arraycopy(keys, index, keys, index + 1, keyNumber - index);
keys[index] = key;
keyNumber++;
}
2.1.3 实现插入child方法insertChild(Node child, int index)
// 向 children 指定索引处插入 child
void insertChild(Node child, int index) {
System.arraycopy(children, index, children, index + 1, keyNumber - index);
children[index] = child;
}
insertKey
和insertChild
方法的作用是向 keys 数组或 children 数组指定 index 处插入新数据
注意:
- 由于使用了静态数组,并且不会在新增或删除时改变它的大小,因此需要额外的 keyNumber 来指定数组内有效 key 的数目
- 插入时 keyNumber++
- 删除时减少 keyNumber 的值即可
- children 不会单独维护数目,它比 keys 多一个
- 如果这两个方法同时调用,注意它们的先后顺序,insertChild 后调用,因为它计算复制元素个数时用到了 keyNumber
2.1.4 实现节点类内部工具方法
// 移除指定 index 处的 key
int removeKey(int index) {
int t = keys[index];
System.arraycopy(keys, index + 1, keys, index, --keyNumber - index);
return t;
}
// 移除最左边的 key
int removeLeftmostKey() {
return removeKey(0);
}
// 移除最右边的 key
int removeRightmostKey() {
return removeKey(keyNumber - 1);
}
// 移除指定 index 处的 child
Node removeChild(int index) {
Node t = children[index];
System.arraycopy(children, index + 1, children, index, keyNumber - index);
children[keyNumber] = null; // help GC
return t;
}
// 移除最左边的 child
Node removeLeftmostChild() {
return removeChild(0);
}
// 移除最右边的 child
Node removeRightmostChild() {
return removeChild(keyNumber);
}
// index 孩子处左边的兄弟
Node childLeftSibling(int index) {
return index > 0 ? children[index - 1] : null;
}
// index 孩子处右边的兄弟
Node childRightSibling(int index) {
return index == keyNumber ? null : children[index + 1];
}
// 复制当前节点的所有 key 和 child 到 target
void moveToTarget(Node target) {
int start = target.keyNumber;
if (!leaf) {
for (int i = 0; i <= keyNumber; i++) {
target.children[start + i] = children[i];
}
}
for (int i = 0; i < keyNumber; i++) {
target.keys[target.keyNumber++] = keys[i];
}
}
2.2 B树类BTree
定义树⬇️
public class BTree {
final int t; // 树中节点最小度数
final int MIN_KEY_NUMBER; // 最小key数目
final int MAX_KEY_NUMBER; // 最大key数目
Node root; // 根节点
public BTree() {
this(2);
}
public BTree(int t) {
this.t = t;
MIN_KEY_NUMBER = t - 1;
MAX_KEY_NUMBER = 2 * t - 1;
root = new Node(t);
}
}
2.2.1 实现是否存在某索引方法contains(int key)
// 1. 是否存在
public boolean contains(int key) {
return root.get(key) != null;
}
2.2.2 实现新增方法put(int key)
思路:
- 首先查找本节点中的插入位置 i,如果没有空位(key 被找到),应该走更新的逻辑,目前什么没做
- 接下来分两种情况
- 如果节点是叶子节点,可以直接插入了
- 如果节点是非叶子节点,需要继续在 children[i] 处继续递归插入
- 无论哪种情况,插入完成后都可能超过节点 keys 数目限制,此时应当执行节点分裂
- 参数中的 parent 和 index 都是给分裂方法用的,代表当前节点父节点,和分裂节点是第几个孩子
判断依据为:
boolean isFull(Node node) {
return node.keyNumber == MAX_KEY_NUMBER;
}
代码实现:
// 2. 新增
public void put(int key) {
doPut(root, key, null, 0);
}
private void doPut(Node node, int key, Node parent, int index) {
int i = 0;
while (i < node.keyNumber) {
if (node.keys[i] == key) {
return; // 更新
}
if (node.keys[i] > key) {
break; // 找到了插入位置,即为此时的 i
}
i++;
}
if (node.leaf) {
node.insertKey(key, i);
} else {
doPut(node.children[i], key, node, i);
}
if (node.keyNumber == MAX_KEY_NUMBER) {
split(node, parent, index);
}
}
2.2.3 实现分裂方法split(Node left, Node parent, int index)
分两种情况:
- 如果 parent == null 表示要分裂的是根节点,此时需要创建新根,原来的根节点作为新根的 0 孩子
- 否则
- 创建 right 节点(分裂后大于当前 left 节点的),把 t 以后的 key 和 child 都拷贝过去
- t-1 处的 key 插入到 parent 的 index 处,index 指 left 作为孩子时的索引
- right 节点作为 parent 的孩子插入到 index + 1 处
/**
* <h3>分裂方法</h3>
*
* @param left 要分裂的节点
* @param parent 分裂节点的父节点
* @param index 分裂节点是第几个孩子
*/
void split(Node left, Node parent, int index) {
// 分裂的是根节点
if (parent == null) {
Node newRoot = new Node(t);
newRoot.leaf = false;
newRoot.insertChild(left, 0); // @TODO keyNumber 的维护(新节点没有孩子,应该不会有问题)
this.root = newRoot;
parent = newRoot;
}
// 1. 创建 right 节点,把 left 中 t 之后的 key 和 child 移动过去
Node right = new Node(t);
right.leaf = left.leaf;
System.arraycopy(left.keys, t, right.keys, 0, t - 1);
// 分裂节点是非叶子的情况
if (!left.leaf) {
System.arraycopy(left.children, t, right.children, 0, t);
for (int i = t; i <= left.keyNumber; i++) {
left.children[i] = null;
}
}
right.keyNumber = t - 1;
left.keyNumber = t - 1;
// 2. 中间的 key (t-1 处)插入到父节点
int mid = left.keys[t - 1];
parent.insertKey(mid, index);
// 3. right 节点作为父节点的孩子
parent.insertChild(right, index + 1);
}
2.2.4 实现删除方法remove(int key)
注意本删除方法是删除B树中某个节点的某个key,不是把整个节点删了
主要分为以下情况:
case 1:当前节点是叶子节点,没找到,直接返回
case 2:当前节点是叶子节点,找到,因为是叶子节点,所以直接删
case 3:当前节点是非叶子节点,没找到,要继续到孩子节点查找
case 4:当前节点是非叶子节点,找到,再接着找到其后继key,替换被删除节点,然后删除后继key
case 5:删除后 key 数目 < 下限 [ t - 1 ](不平衡),进行平衡性调整(具体见 2.2.4)
case 6:根节点
// 3. 删除
public void remove(int key) {
doRemove(null, root, 0, key);
}
private void doRemove(Node parent, Node node, int index, int key) {
int i = 0;
while (i < node.keyNumber) {
if (node.keys[i] >= key) {
break;
}
i++;
}
// i 找到:代表待删除 key 的索引
// i 没找到: 代表到第i个孩子继续查找
if (node.leaf) {
if (!found(node, key, i)) { // case1
return;
} else { // case2
node.removeKey(i);
}
} else {
if (!found(node, key, i)) { // case3
doRemove(node, node.children[i], i, key);
} else { // case4
// 1. 找到后继 key
Node s = node.children[i + 1];
while (!s.leaf) {
s = s.children[0];
}
int skey = s.keys[0];
// 2. 替换待删除 key
node.keys[i] = skey;
// 3. 删除后继 key
doRemove(node, node.children[i + 1], i + 1, skey);
}
}
if (node.keyNumber < MIN_KEY_NUMBER) {
// 调整平衡 case 5 case 6
balance(parent, node, index);
}
}
2.2.4 实现平衡方法balance(Node parent, Node x, int i)
删除后平衡主要分为以下情况:
case 5-1:左边富裕,右旋
case 5-2:右边富裕,左旋
case 5-3:两边都不够借,向左合并
2.2.4.1 右旋
一颗平衡的B树,如下
4
/ \
1,2,3 5,6
删除key 5后变为
4
/ \
1,2,3 6
此时节点6的key的数目小于下限,为了让节点6平衡,需要对key的数目+1,此时左边兄弟的节点富裕,所以可以通过右旋保持平衡
1️⃣ 先把父节点4旋转下去,如下
4
/ \
1,2,3 4,6
2️⃣ 再把左边兄弟节点的3旋转上去,B树恢复平衡,如下
3
/ \
1,2 4,6
2.2.4.2 左旋
一颗平衡的B树,如下
3
/ \
1,2 4,5,6
删除key 1后变为
3
/ \
2 4,5,6
此时节点2的key的数目小于下限,为了让节点2平衡,需要对key的数目+1,此时右边兄弟的节点富裕,所以可以通过左旋保持平衡
1️⃣ 先把父节点3旋转下去,如下
3
/ \
2,3 4,5,6
2️⃣ 再把右边兄弟节点的4旋转上去,B树恢复平衡,如下
4
/ \
2,3 5,6
2.2.4.3 合并
一颗平衡的B树,如下
3
/ \
1,2 4,5
删除key 4后变为
3
/ \
1,2 5
此时节点5的key的数目小于下限,为了让节点5平衡,需要对key的数目+1。但此时左边或右边的兄弟节点并不富裕,不能通过旋转的方式重新平衡,所以这类情况要使用合并(向左合并)
1️⃣ 先把父节点3合并到最右边,如下
null
/ \
1,2,3 5
2️⃣ 再把右边的兄弟节点元素合并到最右边,如下
null
/
1,2,3,5
3️⃣ 最后把0号孩子替换根节点,如下
1,2,3,5
2.2.4.4 代码实现
// 平衡
private void balance(Node parent, Node x, int i) {
// case 6 根节点
if (x == root) {
if (root.keyNumber == 0 && root.children[0] != null) {
root = root.children[0];
}
return;
}
Node left = parent.childLeftSibling(i);
Node right = parent.childRightSibling(i);
// case 5-1 左边富裕,右旋
if (left != null && left.keyNumber > MIN_KEY_NUMBER) {
// a) 父节点中前驱key旋转下来
x.insertKey(parent.keys[i - 1], 0);
if (!left.leaf) {
// b) left中最大的孩子换爹
x.insertChild(left.removeRightmostChild(), 0);
}
// c) left中最大的key旋转上去
parent.keys[i - 1] = left.removeRightmostKey();
return;
}
// case 5-2 右边富裕,左旋
if (right != null && right.keyNumber > MIN_KEY_NUMBER) {
// a) 父节点中后继key旋转下来
x.insertKey(parent.keys[i], x.keyNumber);
// b) right中最小的孩子换爹
if (!right.leaf) {
x.insertChild(right.removeLeftmostChild(), x.keyNumber);
}
// c) right中最小的key旋转上去
parent.keys[i] = right.removeLeftmostKey();
return;
}
// case 5-3 两边都不够借,向左合并
if (left != null) {
// 向左兄弟合并
parent.removeChild(i);
left.insertKey(parent.removeKey(i - 1), left.keyNumber);
x.moveToTarget(left);
} else {
// 向自己合并
parent.removeChild(i + 1);
x.insertKey(parent.removeKey(i), x.keyNumber);
right.moveToTarget(x);
}
}
3 B树Java实现代码完整版(复制粘贴用)
import java.util.Arrays;
/**
* <h3>B-树</h3>
*/
@SuppressWarnings("all")
public class BTree {
static class Node {
/**
* 关键字
*/
int[] keys;
/**
* 孩子们
*/
Node[] children;
/**
* 有效关键字个数(keys 中有效 key 数目)
*/
int keyNumber;
/**
* 是否是叶子节点
*/
boolean leaf = true;
/**
* 最小度数 (最小孩子数),它决定了节点中key 的最小、最大数目,分别是 t-1 和 2t-1
*/
int t;
/**
* 构造方法(给最小度数赋值
*
* @param t 最小度数(t>=2)
*/
public Node(int t) {
this.t = t;
// 最小度数*2就是最多孩子数
this.children = new Node[2 * t];
this.keys = new int[2 * t - 1];
}
public Node(int[] keys) {
this.keys = keys;
}
/**
* 打印有效Key,为了方便调试和测试,非必须
*/
@Override
public String toString() {
return Arrays.toString(Arrays.copyOfRange(keys, 0, keyNumber));
}
// 多路查找
Node get(int key) {
int i = 0;
while (i < keyNumber) {
if (keys[i] == key) {
return this;
}
if (keys[i] > key) {
break;
}
i++;
}
// 执行到此时 keys[i]>key 或 i== keyNumber
if (leaf) {
return null;
}
// 非叶子情况
return children[i].get(key);
}
// 向 keys 指定索引处插入 key
void insertKey(int key, int index) {
System.arraycopy(keys, index, keys, index + 1, keyNumber - index);
keys[index] = key;
keyNumber++;
}
// 向 children 指定索引处插入 child
void insertChild(Node child, int index) {
System.arraycopy(children, index, children, index + 1, keyNumber - index);
children[index] = child;
}
// 移除指定 index 处的 key
int removeKey(int index) {
int t = keys[index];
System.arraycopy(keys, index + 1, keys, index, --keyNumber - index);
return t;
}
// 移除最左边的 key
int removeLeftmostKey() {
return removeKey(0);
}
// 移除最右边的 key
int removeRightmostKey() {
return removeKey(keyNumber - 1);
}
// 移除指定 index 处的 child
Node removeChild(int index) {
Node t = children[index];
System.arraycopy(children, index + 1, children, index, keyNumber - index);
children[keyNumber] = null; // help GC
return t;
}
// 移除最左边的 child
Node removeLeftmostChild() {
return removeChild(0);
}
// 移除最右边的 child
Node removeRightmostChild() {
return removeChild(keyNumber);
}
// index 孩子处左边的兄弟
Node childLeftSibling(int index) {
return index > 0 ? children[index - 1] : null;
}
// index 孩子处右边的兄弟
Node childRightSibling(int index) {
return index == keyNumber ? null : children[index + 1];
}
// 复制当前节点的所有 key 和 child 到 target
void moveToTarget(Node target) {
int start = target.keyNumber;
if (!leaf) {
for (int i = 0; i <= keyNumber; i++) {
target.children[start + i] = children[i];
}
}
for (int i = 0; i < keyNumber; i++) {
target.keys[target.keyNumber++] = keys[i];
}
}
}
final int t; // 树中节点最小度数
final int MIN_KEY_NUMBER; // 最小key数目
final int MAX_KEY_NUMBER; // 最大key数目
Node root; // 根节点
public BTree() {
// 最小度数默认是2
this(2);
}
public BTree(int t) {
this.t = t;
root = new Node(t);
MAX_KEY_NUMBER = 2 * t - 1;
MIN_KEY_NUMBER = t - 1;
}
// 1. 是否存在
public boolean contains(int key) {
return root.get(key) != null;
}
// 2. 新增
public void put(int key) {
doPut(root, key, null, 0);
}
private void doPut(Node node, int key, Node parent, int index) {
int i = 0;
while (i < node.keyNumber) {
if (node.keys[i] == key) {
return; // 更新
}
if (node.keys[i] > key) {
break; // 找到了插入位置,即为此时的 i
}
i++;
}
if (node.leaf) {
node.insertKey(key, i);
} else {
doPut(node.children[i], key, node, i);
}
if (node.keyNumber == MAX_KEY_NUMBER) {
split(node, parent, index);
}
}
/**
* <h3>分裂方法</h3>
*
* @param left 要分裂的节点
* @param parent 分裂节点的父节点
* @param index 分裂节点是第几个孩子
*/
void split(Node left, Node parent, int index) {
// 分裂的是根节点
if (parent == null) {
Node newRoot = new Node(t);
newRoot.leaf = false;
newRoot.insertChild(left, 0); // @TODO keyNumber 的维护(新节点没有孩子,应该不会有问题)
this.root = newRoot;
parent = newRoot;
}
// 1. 创建 right 节点,把 left 中 t 之后的 key 和 child 移动过去
Node right = new Node(t);
right.leaf = left.leaf;
System.arraycopy(left.keys, t, right.keys, 0, t - 1);
// 分裂节点是非叶子的情况
if (!left.leaf) {
System.arraycopy(left.children, t, right.children, 0, t);
for (int i = t; i <= left.keyNumber; i++) {
left.children[i] = null;
}
}
right.keyNumber = t - 1;
left.keyNumber = t - 1;
// 2. 中间的 key (t-1 处)插入到父节点
int mid = left.keys[t - 1];
parent.insertKey(mid, index);
// 3. right 节点作为父节点的孩子
parent.insertChild(right, index + 1);
}
// 3. 删除
public void remove(int key) {
doRemove(null, root, 0, key);
}
private void doRemove(Node parent, Node node, int index, int key) {
int i = 0;
while (i < node.keyNumber) {
if (node.keys[i] >= key) {
break;
}
i++;
}
// i 找到:代表待删除 key 的索引
// i 没找到: 代表到第i个孩子继续查找
if (node.leaf) {
if (!found(node, key, i)) { // case1
return;
} else { // case2
node.removeKey(i);
}
} else {
if (!found(node, key, i)) { // case3
doRemove(node, node.children[i], i, key);
} else { // case4
// 1. 找到后继 key
Node s = node.children[i + 1];
while (!s.leaf) {
s = s.children[0];
}
int skey = s.keys[0];
// 2. 替换待删除 key
node.keys[i] = skey;
// 3. 删除后继 key
doRemove(node, node.children[i + 1], i + 1, skey);
}
}
if (node.keyNumber < MIN_KEY_NUMBER) {
// 调整平衡 case 5 case 6
balance(parent, node, index);
}
}
// 平衡
private void balance(Node parent, Node x, int i) {
// case 6 根节点
if (x == root) {
if (root.keyNumber == 0 && root.children[0] != null) {
root = root.children[0];
}
return;
}
Node left = parent.childLeftSibling(i);
Node right = parent.childRightSibling(i);
// case 5-1 左边富裕,右旋
if (left != null && left.keyNumber > MIN_KEY_NUMBER) {
// a) 父节点中前驱key旋转下来
x.insertKey(parent.keys[i - 1], 0);
if (!left.leaf) {
// b) left中最大的孩子换爹
x.insertChild(left.removeRightmostChild(), 0);
}
// c) left中最大的key旋转上去
parent.keys[i - 1] = left.removeRightmostKey();
return;
}
// case 5-2 右边富裕,左旋
if (right != null && right.keyNumber > MIN_KEY_NUMBER) {
// a) 父节点中后继key旋转下来
x.insertKey(parent.keys[i], x.keyNumber);
// b) right中最小的孩子换爹
if (!right.leaf) {
x.insertChild(right.removeLeftmostChild(), x.keyNumber);
}
// c) right中最小的key旋转上去
parent.keys[i] = right.removeLeftmostKey();
return;
}
// case 5-3 两边都不够借,向左合并
if (left != null) {
// 向左兄弟合并
parent.removeChild(i);
left.insertKey(parent.removeKey(i - 1), left.keyNumber);
x.moveToTarget(left);
} else {
// 向自己合并
parent.removeChild(i + 1);
x.insertKey(parent.removeKey(i), x.keyNumber);
right.moveToTarget(x);
}
}
private boolean found(Node node, int key, int i) {
return i < node.keyNumber && node.keys[i] == key;
}
// 遍历树结构并打印节点的键值
public void travel() {
doTravel(root);
}
public void doTravel(Node node) {
if (node == null) {
return;
}
int i = 0;
for (; i < node.keyNumber; i++) {
doTravel(node.children[i]);
System.out.println(node.keys[i]);
}
doTravel(node.children[i]);
}
}
如果觉得这篇文章对您有所帮助的话,请动动小手点波关注💗,你的点赞👍收藏⭐️转发🔗评论📝都是对博主最好的支持~