B树是对m叉树的一种规则约束,它的定义如下:
- 每一个节点最多有 m 个子节点
- 每一个非叶子节点(除根节点)最少有 ⌈m/2⌉ 个子节点
- 如果根节点不是叶子节点,那么它至少有两个子节点
- 有 k 个子节点的非叶子节点拥有 k − 1 个键
- 所有的叶子节点都在同一层
- 除根节点外的其他节点,key的数目至少要有(m-1)/2个
假设m=5,则除根节点外的其他节点,至少要有(5-1)/2=2个key,最多有5-1=4个key
1、添加key
如下图,蓝色格子代表预留出来的一个容量,实际存储时,只有前4个格子可以存储key。蓝色线条也是预留出来的一个孩子指针,实际只有前5个孩子指针会指向子节点。
有了以上概念,我们尝试向BTree添加key,
首先第一步就是要找到key要添加到哪个节点,我们可以通过二分查找快速定位到“10”落入当前节点node的哪个区间。
/**
* 二分查找数组中的key
* @param arr
* @param left
* @param right
* @param key
* @return 如果找到,就将全局变量isFind改为true并且返回key所在的索引;如果没找到,将全局变量isFind设置为false,并且返回key落入哪个区间section的孩子索引
* @param <T>
*/
private <T extends Comparable<T>> int binarySearchIndexOrSection(T[] arr, int left, int right, T key) {
int mid = (left + right) / 2;
int cmp = key.compareTo(arr[mid]);
if (cmp == 0) {
//当前节点找到key,将isFind置为true,并返回key所在的索引
isFind = true;
return mid;
} else if (left >= right) {
//最后一次查找还是找不到key,返回该key落入哪个区间section
isFind = false;
if (cmp < 0) {
//比当前mid要小,说明key落入mid的左边,childIndex应该是mid
return mid;
}else {
//比当前mid要小,说明key落入mid的右边,childIndex应该是mid+1
return mid + 1;
}
} else if (cmp < 0) {
return binarySearchIndexOrSection(arr, left, mid - 1, key);
}else {
return binarySearchIndexOrSection(arr, mid + 1, right, key);
}
}
通过上面的方法,我们就找到了key要添加到哪个节点,接下来将key添加到该节点的keys数组内会有几种情况需要区分:
第①种情况:keys数组本身有空余的位置,如上图,这时候就能直接按顺序将key插入keys数组。
第②种情况:keys数组没有多余的位置,这时候需要将key先插入到keys数组中,然后进行分裂操作。(注意分裂操作不仅仅会在叶子节点,也有可能发生在非叶子节点)
我们假设要分裂的是非叶子节点(这种情况只有叶子节点分裂后导致父节点的不平衡才会出现),这里假设“B”这个叶子节点分裂后,要将“8”添加到父节点,导致父节点不平衡
叶子节点不平衡,间接导致父节点也不平衡,这里注意要把孩子节点顺位后移
此时该非叶子节点也不平衡,需要新增一个父节点和一个右节点,中间的key移到新增的父节点,中间靠右的全部key移到新增的右结点
然后一定要记得处理孩子节点的归属问题,右节点在复制key的同时,也要把孩子节点也带过去
此时就完成了对非叶子节点的分裂操作。
对于叶子节点的分裂操作其实是类似的,只是可以把处理孩子节点的归属问题环节省略不处理,因为叶子节点的孩子全是null,处不处理孩子节点都一样。
/**
* 添加key时,keys数组已满,对节点进行分裂操作
* @return 返回分裂后产生的rightSideNode
*/
public Node<K,V> split(){
//获取中间索引
int midIndex = keyNum / 2;
Node<K, V> rightSideNode = new Node<>(m, keyType, valueType);
//处理父节点
if (this.parent == null) {
Node<K, V> parentNode = new Node<>(m, keyType, valueType);
parentNode.insertKeyAndValue(keys[midIndex], values[midIndex]);
parentNode.getChildren()[0] = this;
parentNode.getChildren()[1] = rightSideNode;
parentNode.setLeaf(false);
//将当前和rightSide节点的父节点设置为parentNode
this.parent = parentNode;
rightSideNode.parent = parentNode;
}else {
//如果已经有父节点,先判断当前节点是父节点的第几个孩子
Node<K, V> parentNode = this.parent;
int childIndex = predicateWhichChild(this);
//先判断父节点有没有满key
if (parentNode.keyNum == m - 1) {
//父节点满key,先给父节点插入keys[midIndex]、values[midIndex],然后处理parentNode与子节点的关系
parentNode.insertKeyAndValue(keys[midIndex], values[midIndex]);
insertChildren(rightSideNode, parentNode, childIndex + 1);
//分裂父节点
Node<K, V> parentRightSideNode = parentNode.split();
//处理parentRightSideNode的子节点关系(本质是把parentNode的key连带着指针复制到parentRightSideNode的指针域)
copyArray(parentNode.getChildren(), midIndex + 1, parentRightSideNode.getChildren(), 0, m - midIndex);
//将parentNode的孩子复制到parentRightSideNode的孩子后,重置parentRightSideNode孩子的parent指针
resetChildrenBelong(parentRightSideNode);
//将parentRightSideNode设置成非叶子节点
parentRightSideNode.setLeaf(false);
}else {
//父节点没满key,调用insertKeyAndValue直接将mid添加到父节点
parentNode.insertKeyAndValue(keys[midIndex], values[midIndex]);
insertChildren(rightSideNode, parentNode, childIndex + 1);
resetChildrenBelong(parentNode);
}
}
removeKeyAndValue(midIndex);
//将当前节点keys数组里,mid后面的元素和传进来的key复制到rightSideNode的keys数组里,[1,2,4,5,null]
copyArray(keys, midIndex + 1, rightSideNode.keys, 0, m - 1 - midIndex);
copyArray(values, midIndex + 1, rightSideNode.values, 0, m - 1 - midIndex);
keyNum = keyNum - (m - 1 - midIndex);
rightSideNode.keyNum = rightSideNode.keyNum + (m - 1 - midIndex);
return rightSideNode;
}
熟悉了对BTree的分裂操作后,我们就可以整合出对BTree的添加操作代码了
/**
* 添加节点
* @param key
* @param value
*/
public void put(K key, V value) {
if (root == null) {
root = new Node<>(m, keyType, valueType);
root.insertKeyAndValue(key, value);
return;
}
//先查找key,如果能直接找到key,就只需要修改key对应的value值
Node<K, V> node = root;
int index;
while (node != null) {
//如果key能在当前节点的keys中找到,index就是key的索引;如果没找到,index就是key落入keys的哪个区间(也就是能直接指示应该找哪个孩子)
index = binarySearchIndexOrSection(node.getKeys(), 0, node.getKeyNum() - 1, key);
if (isFind) {
//当前节点找到key,直接更新key value
node.updateValueByKey(index, value);
return;
}else {
//没找到,判断当前是不是叶子节点
if (!node.isLeaf()) {
//不是叶子节点,就继续找区间内的孩子
node = node.getChildren()[index];
}else {
//是叶子节点,而且该叶子节点还是没有找到key,就要进行添加操作
node.insertKeyAndValue(key,value);
break;
}
}
}
//出循环说明走的是添加key的流程,需要检查该叶子节点是否平衡
if (!node.isBalance()) {
//不平衡,则需要进行分裂操作
node.split();
//分裂可能会产生新的父节点,需要将root重新指向正确的根
if (root.getParent() != null) {
root = root.getParent();
}
}
}
---------------------------------------------------------------------------------------------------------------------------------
2、删除key
删除操作看似复杂,实际上操作上只是添加操作的逆操作。添加操作需要分裂节点,删除操作则需要合并节点。
BTree删除key时,这个key所在的节点有可能是在叶子节点,也有可能是在非叶子节点,但其实最终都会落到删除叶子节点这种情况里。类似于AVL树,删除非叶子节点,就会转而去删除它的前驱或者后继节点(本文找后继节点作为代替),所以我们对BTree删除的第一步就是要判断删除key对应的节点是否是非叶子节点,如果删的是非叶子节点就要去找它的后继节点。
2.1、查找后继节点
如下图,删除的key所处的节点就是非叶子节点,需要找它的后继节点(右子树最左边的孩子中最左边的key)
找到后继key后,将后继key覆盖原本要删除的key,
此时就将删除非叶子节点的情况转化成了删除叶子节点的key的情况。
2.2、删除叶子节点的key
接下来是删除key的几种情况:
第①种情况:keys删除key后,该叶子节点仍然平衡(大于等于最小key数),直接删除key,如下图,直接删除“12”就行。
第②种情况:删除node上的key后,如果node是root(root是叶子节点),不管根节点有没有平衡,都直接删除key就行。
第③种情况:删除node上的key后,节点不平衡,但兄弟节点有多余的key可以借给node,如下图,删除“14”后,它的右兄弟有3个key,可以借给node,
类似于AVL树中旋转的操作,如果兄弟是node的右兄弟,则需要“左旋”,将“16”左旋至node(补充node的key含量),将“18”左旋至父节点,(如果是左兄弟,则需要“右旋”)
第④种情况:删除node上的key后,节点不平衡,兄弟节点也没有多余的key借给node,如下图的情况
将父节点被node和兄弟节点sibling夹着的key移到node,再把sibling的全部key移到node,
然后将父节点的sibling右边的全部孩子节点左移(相当于移除父节点对sibling的引用关系),
此时记得要检查父节点的key借给node后,是否会因此变得不平衡,如果平衡就可以结束方法了,如果像上图一样不平衡,就需要递归父节点进行调整(即父节点可能跳到②③④情况中的任意一种,有可能父节点的兄弟节点能借,也有可能父节点的兄弟节点不能借需要继续合并,等等情况)
如上图这样,将node指向它的父节点,继续递归调整,
显然,现在的node的兄弟节点也没有多余的key可以借给node,所以就走到了第④种情况需要继续合并,
由于node不是叶子节点,所以合并后需要解决孩子的归属问题,只需要顺延leftNode的孩子节点接管rightNode的孩子即可,
此时,只需要将root重新指向正确的根节点,然后将leftNode的parent指针置空即可(这里的情况leftNode是合并后正确的根节点)
此时原本的root,和rightNode就会因为没有引用指向这些节点会被GC自动垃圾回收,
删除key后,重新平衡BTree,
/**
* 重新平衡B树
* @param node 删除key后对应的叶子节点
*/
private void balanceTree(Node<K, V> node) {
Node<K, V> parentNode = node.getParent();
Node<K, V> siblingNode = getMoreChildSibling(node); //兄弟节点
//判断兄弟节点是否有多余的key可外借
if (siblingNode.canLendKey()) {
//兄弟可外借key
//先判断兄弟节点是左兄弟还是右兄弟
if (isLeftSibling(siblingNode)) {
//sibling是node的左兄弟
node.insertKeyAndValue(parentNode.getKeys()[childIndex - 1], parentNode.getValues()[childIndex - 1]);
if (siblingNode.getChildNum() != 0) {
node.insertChildren(siblingNode.getChildren()[siblingNode.getChildNum() - 1], node, 0);
siblingNode.removeChildLeftShiftByIndex(siblingNode.getChildNum() - 1);
}
parentNode.getKeys()[childIndex - 1] = siblingNode.getKeys()[siblingNode.getKeyNum() - 1];
parentNode.getValues()[childIndex - 1] = siblingNode.getValues()[siblingNode.getKeyNum() - 1];
siblingNode.removeKeyAndValueLeftShiftByIndex(siblingNode.getKeyNum() - 1);
}else {
//sibling是node的右兄弟
node.insertKeyAndValue(parentNode.getKeys()[childIndex], parentNode.getValues()[childIndex]);
node.insertChildren(siblingNode.getChildren()[0], node, node.getChildNum());
parentNode.getKeys()[childIndex] = siblingNode.getKeys()[0];
parentNode.getValues()[childIndex] = siblingNode.getValues()[0];
siblingNode.removeKeyAndValueLeftShiftByIndex(0);
siblingNode.removeChildLeftShiftByIndex(0);
}
}else {
//兄弟没的借
//把父节点的key借给node,然后node节点与兄弟节点合并
if (isLeftSibling(siblingNode)) {
mergeNode(siblingNode, node, childIndex - 1);
}else {
mergeNode(node, siblingNode, childIndex);
}
//合并后,要检查父节点是否平衡
if (!parentNode.isBalance()) {
//父节点不平衡
//如果父节点不平衡的是root,就不用再作处理
if (parentNode == root) {
//如果根节点keyNum为0,说明原根节点的最后一个key都拿去给子节点用于合并了,此时root必定只剩下一个孩子,该孩子就是新的root
if (root.getKeyNum() == 0) {
root = root.getChildren()[0];
}
return;
}
balanceTree(parentNode);
}
}
}
至此,BTree最主要的两个添加和删除就完成了。
3、实现代码
最后贴一个BTree的完整代码实现:
3.1、节点类
public class Node<K extends Comparable<K>,V> {
private K[] keys;
private V[] values;
private Node<K,V>[] children; //孩子节点数组
private int m; //树的阶数
private boolean isLeaf = true; //标识是否为叶子节点
private int keyNum; //key的数量
private final int minKeyNum; //最少子节点数
private Node<K,V> parent; //父节点
private Class<K> keyType; //key类型
private Class<V> valueType; //value类型
/**
* 构造器
* @param m 树的阶数
*/
public Node(int m,Class<K> keyType,Class<V> valueType) {
this.keyType = keyType;
this.valueType = valueType;
keys = (K[]) Array.newInstance(keyType, m); //可存m-1个key,容量为m是为了方便插入操作
values = (V[]) Array.newInstance(valueType, m);
children = (Node<K, V>[]) Array.newInstance(Node.class, m + 1); //多加一个容量,方便插入操作
this.m = m;
this.minKeyNum = (int)Math.ceil((double) m/2); //计算最少子节点数
}
public boolean isLeaf() {
return isLeaf;
}
public void setLeaf(boolean leaf) {
isLeaf = leaf;
}
public void setKeyNum(int keyNum) {
this.keyNum = keyNum;
}
public Node<K, V> getParent() {
return parent;
}
public int getKeyNum() {
return keyNum;
}
public K[] getKeys() {
return keys;
}
public V[] getValues() {
return values;
}
public Node<K, V>[] getChildren() {
return children;
}
/**
* 根据key值,按顺序把key和value插入到对应位置
* 调用此方法前,要保证keys数组有空位
* @param key
* @param value
*/
public void insertKeyAndValue(K key, V value) {
int cmp = 0;
//如果keys数组全为null,直接将key,value赋值到第0个位置
if (keys[0] == null) {
keys[0] = key;
values[0] = value;
keyNum++;
return;
}
//keys数组有值
int count = 0;
for (int i = 0; i < keys.length; i++) {
if (keys[i] != null) {
count++;
cmp = key.compareTo(keys[i]);
//传进来的key比keys[i]小,将i后面的所有元素后移,然后插入
if (cmp < 0) {
insertByIndex(keys, key, i);
insertByIndex(values, value, i);
keyNum++;
break;
}
}
}
if (count == keyNum) {
keys[count] = key;
values[count] = value;
keyNum++;
}
}
/**
* 根据索引向目标数组插入元素
* @param targetArray 目标数组
* @param value 要插入的值
* @param index 指定索引
*/
private void insertByIndex(Object[] targetArray,Object value,int index) {
int last = keyNum - 1; //last指向数组的最后一个元素
//将index(包含index)的元素全部后移
while (last >= index) {
targetArray[last + 1] = targetArray[last];
last--;
}
//给index赋值
targetArray[index] = value;
}
/**
* 添加key时,keys数组已满,对节点进行分裂操作
* @return 返回分裂后产生的rightSideNode
*/
public Node<K,V> split(){
//获取中间索引
int midIndex = keyNum / 2;
Node<K, V> rightSideNode = new Node<>(m, keyType, valueType);
//处理父节点
if (this.parent == null) {
Node<K, V> parentNode = new Node<>(m, keyType, valueType);
parentNode.insertKeyAndValue(keys[midIndex], values[midIndex]);
parentNode.getChildren()[0] = this;
parentNode.getChildren()[1] = rightSideNode;
parentNode.setLeaf(false);
//将当前和rightSide节点的父节点设置为parentNode
this.parent = parentNode;
rightSideNode.parent = parentNode;
}else {
//如果已经有父节点,先判断当前节点是父节点的第几个孩子
Node<K, V> parentNode = this.parent;
//判断当前节点是父节点的第几个孩子
int childIndex = predicateWhichChild(this);
//先判断父节点有没有满key
if (parentNode.keyNum == m - 1) {
//父节点满key,先给父节点插入keys[midIndex]、values[midIndex],然后处理parentNode与子节点的关系
parentNode.insertKeyAndValue(keys[midIndex], values[midIndex]);
insertChildren(rightSideNode, parentNode, childIndex + 1);
//分裂父节点
Node<K, V> parentRightSideNode = parentNode.split();
//处理parentRightSideNode的子节点关系(本质是把parentNode的key连带着指针复制到parentRightSideNode的指针域)
copyArray(parentNode.getChildren(), midIndex + 1, parentRightSideNode.getChildren(), 0, m - midIndex);
//将parentNode的孩子复制到parentRightSideNode的孩子后,重置parentRightSideNode孩子的parent指针
resetChildrenBelong(parentRightSideNode);
//将parentRightSideNode设置成非叶子节点
parentRightSideNode.setLeaf(false);
}else {
//父节点没满key,调用insertKeyAndValue直接将mid添加到父节点
parentNode.insertKeyAndValue(keys[midIndex], values[midIndex]);
insertChildren(rightSideNode, parentNode, childIndex + 1);
resetChildrenBelong(parentNode);
}
}
removeKeyAndValue(midIndex);
//将当前节点keys数组里,mid后面的元素和传进来的key复制到rightSideNode的keys数组里,[1,2,4,5,null]
copyArray(keys, midIndex + 1, rightSideNode.keys, 0, m - 1 - midIndex);
copyArray(values, midIndex + 1, rightSideNode.values, 0, m - 1 - midIndex);
keyNum = keyNum - (m - 1 - midIndex);
rightSideNode.keyNum = rightSideNode.keyNum + (m - 1 - midIndex);
return rightSideNode;
}
/**
* 将孩子插入到指定索引位置
* @param childNode 孩子节点
* @param parentNode 父节点
* @param index 插入的索引
*/
public void insertChildren(Node<K,V> childNode,Node<K,V> parentNode,int index){
//先找最后一个子节点
Node<K, V>[] parentChildren = parentNode.getChildren();
int last = parentChildren.length - 1;
while (last >= 0) {
if (parentChildren[last] != null) {
break;
}
last--;
}
if (last >= 0) {
//将index(包含index)索引右边的所有孩子节点右移
while (last >= index) {
parentChildren[last + 1] = parentChildren[last];
last--;
}
parentChildren[index] = childNode;
}
}
/**
* 判断节点是父节点的第几个孩子
* 调用此方法前,必须保证传入的节点有父节点
* @param node
* @return 找到则返回这个节点在父节点中孩子的索引位置,没找到则返回-1
*/
private int predicateWhichChild(Node<K,V> node){
Node<K, V> parentNode = node.parent;
for (int i = 0; i < parentNode.getChildren().length; i++) {
if (node == parentNode.getChildren()[i]) {
return i;
}
}
return -1;
}
/**
* 移除当前节点keys、values数组中对应索引的元素
* @param index
*/
private void removeKeyAndValue(int index){
//直接把index上的值置空即可
keys[index] = null;
values[index] = null;
keyNum--;
}
/**
* 复制数组到另一个数组(会将原数组元素取出)
* @param srcArray 源数组
* @param srcStart 源数组复制的起始位置
* @param targetArray 目标数组
* @param targetStart 目标数组粘贴的起始位置
* @param length 要复制的数组元素个数
* @param <T> 数组类型
*/
public <T> void copyArray(T[] srcArray, int srcStart, T[] targetArray, int targetStart, int length) {
for (int i = 0; i < length; i++) {
targetArray[targetStart++] = srcArray[srcStart];
srcArray[srcStart] = null;
srcStart++;
}
}
/**
* 根据索引,更新value值
* @param value
*/
public void updateValueByKey(int index, V value) {
values[index] = value;
}
/**
* 获取当前节点的孩子数目
* @return
*/
public int getChildNum(){
int childNum = 0;
for (int i = 0; i < children.length; i++) {
if (children[i] != null) {
childNum++;
}
}
return childNum;
}
/**
* 重置孩子节点的归属,使其parent指针指向正确的父节点
* @param node
*/
private void resetChildrenBelong(Node<K, V> node) {
Node<K, V>[] nodeChildren = node.getChildren();
for (int i = 0; i < node.getChildNum(); i++) {
//将该节点的每一个孩子的parent指针都重新指向node
nodeChildren[i].parent = node;
}
}
/**
* 根据index删除key value并将index右边的key value左移
* @param index
*/
public void removeKeyAndValueLeftShiftByIndex(int index){
int start = index;
while (start < keyNum - 1) {
keys[start] = keys[start + 1];
values[start] = values[start + 1];
start++;
}
keys[start] = null;
values[start] = null;
keyNum--;
}
/**
* 根据index删除孩子,并将index右边的孩子全部左移
* @param index
*/
public void removeChildLeftShiftByIndex(int index){
int start = index;
while (start < getChildNum() - 1) {
children[start] = children[start + 1];
start++;
}
children[start] = null;
}
/**
* 判断当前节点是否平衡(只检查最小需求节点)
* @return
*/
public boolean isBalance(){
return keyNum >= ((m - 1) / 2) && keyNum <= m - 1;
}
/**
* 判断当前节点是否有多余的key可外借
* @return
*/
public boolean canLendKey(){
return keyNum > ((m - 1) / 2);
}
}
3.2、BTree类
public class BTree<K extends Comparable<K>,V> {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入B树的阶数:");
int rank = scanner.nextInt();
System.out.println("请输入要插入的个数:");
int num = scanner.nextInt();
BTree<Integer, Integer> tree = new BTree<>(rank, Integer.class, Integer.class);
System.out.println("当前树的阶数是:" + rank);
System.out.println("B树插入" + num + "个数据");
System.out.println("------------------------------------------------------------");
for (int i = 1; i <= num; i++) {
tree.put(i, i * 10);
}
tree.printTree();
while (true) {
System.out.println("------------------------------------------------------------");
System.out.println("输入1展示BTree");
System.out.println("输入2添加节点");
System.out.println("输入3删除节点");
System.out.println("输入4查看节点");
int order = scanner.nextInt();
switch (order) {
case 1 -> tree.printTree();
case 2 -> {
System.out.println("添加请输入key:");
int key = scanner.nextInt();
System.out.println("添加请输入value:");
int value = scanner.nextInt();
tree.put(key, value);
System.out.println("添加节点成功!");
}
case 3 -> {
System.out.println("删除请输入key:");
int key = scanner.nextInt();
tree.remove(key);
}
case 4 -> {
System.out.println("查找请输入key:");
int key = scanner.nextInt();
Integer value = tree.get(key);
if (value != null) {
System.out.println("key [" + key + "] 对应的value是:[" + value + "]");
}
}
}
}
}
private Node<K,V> root;
private Class<K> keyType;
private Class<V> valueType;
private int m; //B树的阶
private int count = 0; //用于遍历节点时计数
private boolean isFind; //用于二分查找时判断是否找到key
private int keyIndex; //调用searchNodeByKey后,会记录key在keys中的位置
private int childIndex; //调用getMoreChildSibling后,会记录node所指向的叶子节点是父节点的第几个孩子
public Node<K, V> getRoot() {
return root;
}
/**
* B树构造器
* @param m 树的阶数
* @param keyType key类型
* @param valueType value类型
*/
public BTree(int m, Class<K> keyType, Class<V> valueType) {
this.m = m;
this.keyType = keyType;
this.valueType = valueType;
}
/**
* 添加节点
* @param key
* @param value
*/
public void put(K key, V value) {
if (root == null) {
root = new Node<>(m, keyType, valueType);
root.insertKeyAndValue(key, value);
return;
}
//先查找key,如果能直接找到key,就只需要修改key对应的value值
Node<K, V> node = root;
int index;
while (node != null) {
//如果key能在当前节点的keys中找到,index就是key的索引;如果没找到,index就是key落入keys的哪个区间(也就是能直接指示应该找哪个孩子)
index = binarySearchIndexOrSection(node.getKeys(), 0, node.getKeyNum() - 1, key);
if (isFind) {
//当前节点找到key,直接更新key value
node.updateValueByKey(index, value);
return;
}else {
//没找到,判断当前是不是叶子节点
if (!node.isLeaf()) {
//不是叶子节点,就继续找区间内的孩子
node = node.getChildren()[index];
}else {
//是叶子节点,而且该叶子节点还是没有找到key,就要进行添加操作
node.insertKeyAndValue(key,value);
break;
}
}
}
//出循环说明走的是添加key的流程,需要检查该叶子节点是否平衡
if (!node.isBalance()) {
//不平衡,则需要进行分裂操作
node.split();
//分裂可能会产生新的父节点,需要将root重新指向正确的根
if (root.getParent() != null) {
root = root.getParent();
}
}
}
/**
* 删除节点
* @param key
* @return 返回删除节点的value值
*/
public V remove(K key) {
//先找到要删除的key对应的节点
Node<K, V> removeNode = searchNodeByKey(key);
if (removeNode == null) {
//整棵树都没有这个key,直接返回null
System.out.println("没有找到key,删除失败!");
return null;
}
//先把key对应的value取出来
V oldValue = removeNode.getValues()[keyIndex];
//判断当前要删除的key所在的节点是否是叶子节点,如果不是叶子节点,就找它的后继key进行替换
//后继节点一定是key右边的那个指针所指向的孩子节点,后继key一定是后继节点的第一个key
Node<K, V> node = removeNode; //node用于辅助找后继key
while (!node.isLeaf()) {
node = node.getChildren()[keyIndex + 1];
//记得更新keyIndex,因为后继key永远是后继节点的第一个key,所以要让keyIndex = 0
keyIndex = 0;
}
removeNode.getKeys()[keyIndex] = node.getKeys()[0];
removeNode.getValues()[keyIndex] = node.getValues()[0];
//退出循环,node必定指向叶子节点
//删除叶子节点keyIndex上的key value(注意要把keyIndex右边的key左移)
node.removeKeyAndValueLeftShiftByIndex(keyIndex);
//如果node指向root,说明root是叶子节点,直接返回oldValue
if (node == root) {
return oldValue;
}
//删除后检查叶子节点是否平衡
if (!node.isBalance()) {
//当前叶子节点不平衡,调用balanceTree重新调整B树至平衡
balanceTree(node);
}
System.out.println("删除[" + key + "]成功!");
return oldValue;
}
/**
* 重新平衡B树
* @param node 删除key后对应的叶子节点
*/
private void balanceTree(Node<K, V> node) {
Node<K, V> parentNode = node.getParent();
Node<K, V> siblingNode = getMoreChildSibling(node); //兄弟节点
//判断兄弟节点是否有多余的key可外借
if (siblingNode.canLendKey()) {
//兄弟可外借key
//先判断兄弟节点是左兄弟还是右兄弟
if (isLeftSibling(siblingNode)) {
//sibling是node的左兄弟
node.insertKeyAndValue(parentNode.getKeys()[childIndex - 1], parentNode.getValues()[childIndex - 1]);
if (siblingNode.getChildNum() != 0) {
node.insertChildren(siblingNode.getChildren()[siblingNode.getChildNum() - 1], node, 0);
siblingNode.removeChildLeftShiftByIndex(siblingNode.getChildNum() - 1);
}
parentNode.getKeys()[childIndex - 1] = siblingNode.getKeys()[siblingNode.getKeyNum() - 1];
parentNode.getValues()[childIndex - 1] = siblingNode.getValues()[siblingNode.getKeyNum() - 1];
siblingNode.removeKeyAndValueLeftShiftByIndex(siblingNode.getKeyNum() - 1);
}else {
//sibling是node的右兄弟
node.insertKeyAndValue(parentNode.getKeys()[childIndex], parentNode.getValues()[childIndex]);
node.insertChildren(siblingNode.getChildren()[0], node, node.getChildNum());
parentNode.getKeys()[childIndex] = siblingNode.getKeys()[0];
parentNode.getValues()[childIndex] = siblingNode.getValues()[0];
siblingNode.removeKeyAndValueLeftShiftByIndex(0);
siblingNode.removeChildLeftShiftByIndex(0);
}
}else {
//兄弟没的借
//把父节点的key借给node,然后node节点与兄弟节点合并
if (isLeftSibling(siblingNode)) {
mergeNode(siblingNode, node, childIndex - 1);
}else {
mergeNode(node, siblingNode, childIndex);
}
//合并后,要检查父节点是否平衡
if (!parentNode.isBalance()) {
//父节点不平衡
//如果父节点不平衡的是root,就不用再作处理
if (parentNode == root) {
//如果根节点keyNum为0,说明原根节点的最后一个key都拿去给子节点用于合并了,此时root必定只剩下一个孩子,该孩子就是新的root
if (root.getKeyNum() == 0) {
root = root.getChildren()[0];
}
return;
}
balanceTree(parentNode);
}
}
}
/**
* 把key value作为中间媒介,将rightNode合并到leftNode
* @param leftNode 左节点
* @param rightNode 右节点
* @param parentKeyIndex 父节点的key索引(leftNode跟rightNode夹着的key)
*/
private void mergeNode(Node<K,V> leftNode,Node<K,V> rightNode,int parentKeyIndex){
Node<K, V> parentNode = leftNode.getParent();
int length = rightNode.getKeyNum(); //要复制的长度
leftNode.insertKeyAndValue(parentNode.getKeys()[parentKeyIndex], parentNode.getValues()[parentKeyIndex]);
leftNode.copyArray(rightNode.getKeys(), 0, leftNode.getKeys(), leftNode.getKeyNum(), length);
leftNode.copyArray(rightNode.getValues(), 0, leftNode.getValues(), leftNode.getKeyNum(), length);
//复制完后记得要更新keyNum
leftNode.setKeyNum(leftNode.getKeyNum() + length);
//记得还要处理leftNode的children数组
leftNode.copyArray(rightNode.getChildren(), 0, leftNode.getChildren(), leftNode.getChildNum(), rightNode.getChildNum());
//把parentNode对rightNode的孩子索引删除,并将右边的孩子全部左移
parentNode.removeChildLeftShiftByIndex(parentKeyIndex + 1);
//把parentNode上parentKeyIndex上的key value删除
parentNode.removeKeyAndValueLeftShiftByIndex(parentKeyIndex);
}
/**
* 返回node节点的所有相邻兄弟节点中,key较多的兄弟节点,左右两兄弟keyNum相等就优先返回右兄弟
* 调用此函数前,必须保证node有父节点
* 返回值不可能返回null,因为node必定有兄弟节点
* @param node
* @return
*/
private Node<K,V> getMoreChildSibling(Node<K,V> node){
Node<K, V> parentNode = node.getParent();
//遍历父节点的children数组,找到node是parentNode的第几个孩子
int childIndex = 0;
for (Node<K, V> child : parentNode.getChildren()) {
if (node == child) {
break;
}
childIndex++;
}
this.childIndex = childIndex; //顺便将childIndex记录到类成员变量,方便后续直接拿来使用
Node<K, V> leftChild = childIndex == 0 ? null : parentNode.getChildren()[childIndex - 1];
Node<K, V> rightChild = parentNode.getChildren()[childIndex + 1];
int leftKeyNum = leftChild == null ? -1 : leftChild.getKeyNum();
int rightKeyNum = rightChild == null ? -1 : rightChild.getKeyNum();
return leftKeyNum > rightKeyNum ? leftChild : rightChild;
}
/**
* 判断兄弟节点是否是左兄弟
* 调用前必须保证类成员变量childIndex是赋过值的
* @param siblingNode
* @return
*/
private boolean isLeftSibling(Node<K,V> siblingNode){
return siblingNode.getParent().getChildren()[childIndex + 1] != siblingNode;
}
/**
* 根据key查找对应节点(重载)
* @param key 要查找的key
* @return
*/
private Node<K, V> searchNodeByKey(K key){
return searchNodeByKey0(root, key);
}
private Node<K, V> searchNodeByKey0(Node<K,V> node,K key) {
//先检查当前节点有没有key
int index = binarySearchIndexOrSection(node.getKeys(), 0, node.getKeyNum() - 1, key);
//当前节点的keys数组不存在key,那么index就是key落入keys的区间位置
if (!isFind) {
if (!node.isLeaf()) {
//如果当前节点不是叶子节点,就去找key对应keys区间的那个孩子节点
return searchNodeByKey0(node.getChildren()[index], key);
}else {
//当前叶子节点还是没有找到key,说明整棵树都没有这个key,直接返回null
return null;
}
}else {
//当前节点的keys中找到了key,返回当前节点,顺便把key所在keys的索引记录一下,这样全程就只需要一次二分查找
keyIndex = index;
return node;
}
}
/**
* 二分查找数组中的key
* @param arr
* @param left
* @param right
* @param key
* @return 如果找到,就将全局变量isFind改为true并且返回key所在的索引;如果没找到,将全局变量isFind设置为false,并且返回key落入哪个区间section的孩子索引
* @param <T>
*/
private <T extends Comparable<T>> int binarySearchIndexOrSection(T[] arr, int left, int right, T key) {
int mid = (left + right) / 2;
int cmp = key.compareTo(arr[mid]);
if (cmp == 0) {
//当前节点找到key,将isFind置为true,并返回key所在的索引
isFind = true;
return mid;
} else if (left >= right) {
//最后一次查找还是找不到key,返回该key落入哪个区间section
isFind = false;
if (cmp < 0) {
//比当前mid要小,说明key落入mid的左边,childIndex应该是mid
return mid;
}else {
//比当前mid要小,说明key落入mid的右边,childIndex应该是mid+1
return mid + 1;
}
} else if (cmp < 0) {
return binarySearchIndexOrSection(arr, left, mid - 1, key);
}else {
return binarySearchIndexOrSection(arr, mid + 1, right, key);
}
}
/**
* 根据key获取对应value值
* @param key
* @return
*/
public V get(K key) {
//获取key所在的对应节点,此时keyIndex已经记录了key所在keys的索引位置
Node<K, V> node = searchNodeByKey(key);
if (node == null) {
System.out.println("没有找到key,获取value失败!");
return null;
}
return node.getValues()[keyIndex];
}
/**
* 打印B树
*/
public void printTree(){
if (root.getKeyNum() == 0) {
System.out.println("当前BTree为空树!");
return;
}
printTree0(root);
count = 0;
}
private void printTree0(Node<K,V> node){
System.out.print("第" + count + "个节点: ");
count++;
for (int i = 0; i < node.getKeyNum(); i++) {
System.out.print(node.getKeys()[i] + " ");
}
System.out.println();
//当前节点不是叶子节点,找子节点
if (!node.isLeaf()){
for (int i = 0; i < node.getChildNum(); i++) {
printTree0(node.getChildren()[i]);
}
}
}
}
4、测试图例
4.1、测试 添加key
插入1到10