文章目录
- 1.概述
- 2.实现
- 定义节点
- 多路查找
- 插入 key 和 child
- 定义树
- 插入
- 分裂
- 删除
- 代码
1.概述
- B树(B-Tree)结构是一种高效存储和查询数据的方法。
- B树主要思想是将每个节点扩展成多个子节点,以减少查找所需的次数。
- B树结构非常适合应用于磁盘等大型存储器的高效操作,被广泛应用于关系数据库和文件系统中。
特性
一棵 B-树具有以下性质
特性1:每个节点 x 具有
- 属性 n,表示节点 x 中 key 的个数
- 属性 leaf,表示节点是否是叶子节点
- 节点 key 可以有多个,以升序存储
特性2:每个非叶子节点中的孩子数是 n + 1、叶子节点没有孩子
特性3:最小度数t(节点的孩子数称为度)和节点中键数量的关系如下:
最小度数t | 键数量范围 |
---|---|
2 | 1 ~ 3 |
3 | 2 ~ 5 |
4 | 3 ~ 7 |
… | … |
n | (n-1) ~ (2n-1) |
其中,当节点中键数量达到其最大值时,即 3、5、7 … 2n-1,需要分裂
特性4:叶子节点的深度都相同
问:
B-树为什么有最小度数的限制?
答:
B树中有最小度数的限制是为了保证B树的平衡特性。
在B树中,每个节点都可以有多个子节点,这使得B树可以存储大量的键值,但也带来了一些问题。如果节点的子节点数量太少,那么就可能导致B树的高度过高,从而降低了B树的效率。此外,如果节点的子节点数量太多,那么就可能导致节点的搜索、插入和删除操作变得复杂和低效。
最小度数的限制通过限制节点的子节点数量,来平衡这些问题。在B树中,每个节点的子节点数量都必须在一定的范围内,即t到2t之间(其中t为最小度数)
B-树与 2-3 树、2-3-4 树的关系
- 2-3树是最小度数为2的B树,其中每个节点可以包含2个或3个子节点。
- 2-3-4树是最小度数为2的B树的一种特殊情况,其中每个节点可以包含2个、3个或4个子节点。
- B树是一种更加一般化的平衡树,可以适应不同的应用场景,其节点可以包含任意数量的键值,节点的度数取决于最小度数t的设定。
2.实现
定义节点
static class Node {boolean leaf = true;int keyNumber;int t;int[] keys;Node[] children; public Node(int t) {this.t = t;this.keys = new int[2 * t - 1];this.children = new Node[2 * t];}@Overridepublic String toString() {return Arrays.toString(Arrays.copyOfRange(keys, 0, keyNumber));}
}
- leaf 表示是否为叶子节点
- keyNumber 为 keys 中有效 key 数目
- t 为最小度数,它决定了节点中key 的最小、最大数目,分别是 t-1 和 2t-1
- keys 存储此节点的 key(keys 应当改为 entries 以便同时保存 key 和 value)
- children 存储此节点的 child
- toString 调试和测试
多路查找
节点类添加 get 方法
Node get(int key) {int i = 0;while (i < keyNumber && keys[i] < key) {i++;}if (i < keyNumber && keys[i] == key) {return this;}if (leaf) {return null;}return children[i].get(key);
}
插入 key 和 child
节点类添加 insertKey 和 insertChild 方法
void insertKey(int key, int index) {System.arraycopy(keys, index, keys, index + 1, keyNumber - index);keys[index] = key;keyNumber++;
}void insertChild(Node child, int index) {System.arraycopy(children, index, children, index + 1, keyNumber - index);children[index] = child;
}
作用是向 keys 数组或 children 数组指定 index 处插入新数据,注意
- 由于使用了静态数组,并且不会在新增或删除时改变它的大小,因此需要额外的 keyNumber 来指定数组内有效 key 的数目
- 插入时 keyNumber++
- 删除时减少 keyNumber 的值即可
- children 不会单独维护数目,它比 keys 多一个
- 如果这两个方法同时调用,注意它们的先后顺序,insertChild 后调用,因为它计算复制元素个数时用到了 keyNumber
定义树
public class BTree {final int t;final int MIN_KEY_NUMBER;final int MAX_KEY_NUMBER;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);}
}
插入
public void put(int key) {doPut(null, 0, root, key);
}private void doPut(Node parent, int index, Node node, int key) {int i = 0;while (i < node.keyNumber && node.keys[i] < key) {i++;}if (i < node.keyNumber && node.keys[i] == key) {return;}if (node.leaf) {node.insertKey(key, i);} else {doPut(node, i, node.children[i], key);}if (isFull(node)) {split(parent, index, node);}
}
- 先查找本节点中的插入位置 i,如果没有空位(key 被找到),走更新的逻辑
- 接下来分两种情况
- 如果节点是叶子节点,可以直接插入了
- 如果节点是非叶子节点,需要继续在 children[i] 处继续递归插入
- 无论哪种情况,插入完成后都可能超过节点 keys 数目限制,此时应当执行节点分裂
- 参数中的 parent 和 index 都是给分裂方法用的,代表当前节点父节点,和分裂节点是第几个孩子
判断依据:
boolean isFull(Node node) {return node.keyNumber == MAX_KEY_NUMBER;
}
分裂
void split(Node parent, int index , Node left) {if (parent == null) {Node newRoot = new Node(this.t);newRoot.leaf = false;newRoot.insertChild(root, 0);root = newRoot;parent = newRoot;}Node right = new Node(this.t);right.leaf = left.leaf;right.keyNumber = t - 1;System.arraycopy(left.keys, t, right.keys, 0, t - 1);if (!left.leaf) {System.arraycopy(left.children, t, right.children, 0, t);}left.keyNumber = t - 1;int mid = left.keys[t - 1];parent.insertKey(mid, index);parent.insertChild(right, index + 1);}
两种情况:
- 如果 parent == null 表示要分裂的是根节点,此时需要创建新根,原来的根节点作为新根的 0 孩子
- 否则
- 创建 right 节点(分裂后大于当前 left 节点的),把 t 以后的 key 和 child 都拷贝过去
- t-1 处的 key 插入到 parent 的 index 处,index 指 left 作为孩子时的索引
- right 节点作为 parent 的孩子插入到 index + 1 处
删除
case 1:当前节点是叶子节点,没找到
case 2:当前节点是叶子节点,找到了
case 3:当前节点是非叶子节点,没找到
case 4:当前节点是非叶子节点,找到了
case 5:删除后 key 数目 < 下限(不平衡)
case 6:根节点
代码
package com.itheima.algorithm.btree;import java.util.Arrays;/*** <h3>B-树</h3>*/
@SuppressWarnings("all")
public class BTree {static class Node {int[] keys; // 关键字Node[] children; // 孩子int keyNumber; // 有效关键字数目boolean leaf = true; // 是否是叶子节点int t; // 最小度数 (最小孩子数)public Node(int t) { // t>=2this.t = t;this.children = new Node[2 * t];this.keys = new int[2 * t - 1];}public Node(int[] keys) {this.keys = keys;}@Overridepublic 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==keyNumberif (leaf) {return null;}// 非叶子情况return children[i].get(key);}// 向 keys 指定索引处插入 keyvoid insertKey(int key, int index) {System.arraycopy(keys, index, keys, index + 1, keyNumber - index);keys[index] = key;keyNumber++;}// 向 children 指定索引处插入 childvoid insertChild(Node child, int index) {System.arraycopy(children, index, children, index + 1, keyNumber - index);children[index] = child;}int removeKey(int index) {int t = keys[index];System.arraycopy(keys, index + 1, keys, index, --keyNumber - index);return t;}int removeLeftmostKey() {return removeKey(0);}int removeRightmostKey() {return removeKey(keyNumber - 1);}Node removeChild(int index) {Node t = children[index];System.arraycopy(children, index + 1, children, index, keyNumber - index);children[keyNumber] = null;return t;}Node removeLeftmostChild() {return removeChild(0);}Node removeRightmostChild() {return removeChild(keyNumber);}void moveToLeft(Node left) {int start = left.keyNumber;if (!leaf) {for (int i = 0; i <= keyNumber; i++) {left.children[start + i] = children[i];}}for (int i = 0; i < keyNumber; i++) {left.keys[left.keyNumber++] = keys[i];}}Node leftSibling(int index) {return index > 0 ? children[index - 1] : null;}Node rightSibling(int index) {return index == keyNumber ? null : children[index + 1];}}Node root;int t; // 树中节点最小度数final int MIN_KEY_NUMBER; // 最小key数目final int MAX_KEY_NUMBER; // 最大key数目public BTree() {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);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(root, key, null, 0);}private void doRemove(Node node, int key, Node parent, int index) {int i = 0;while (i < node.keyNumber) {if (node.keys[i] >= key) {break;}i++;}if (node.leaf) {if (notFound(node, key, i)) { // case 1return;}node.removeKey(i); // case 2} else {if (notFound(node, key, i)) { // case 3doRemove(node.children[i], key, node, i);} else { // case 4Node s = node.children[i + 1];while (!s.leaf) {s = s.children[0];}int k = s.keys[0];node.keys[i] = k;doRemove(node.children[i + 1], k, node, i + 1);}}if (node.keyNumber < MIN_KEY_NUMBER) { // case 5balance(node, parent, index);}}private boolean notFound(Node node, int key, int i) {return i >= node.keyNumber || (i < node.keyNumber && node.keys[i] != key);}private void balance(Node node, Node parent, int i) {if (node == root) {if (root.keyNumber == 0 && root.children[0] != null) {root = root.children[0];}return;}Node leftSibling = parent.leftSibling(i);Node rightSibling = parent.rightSibling(i);if (leftSibling != null && leftSibling.keyNumber > MIN_KEY_NUMBER) {rightRotate(node, leftSibling, parent, i);return;}if (rightSibling != null && rightSibling.keyNumber > MIN_KEY_NUMBER) {leftRotate(node, rightSibling, parent, i);return;}if (leftSibling != null) {mergeToLeft(leftSibling, parent, i - 1);} else {mergeToLeft(node, parent, i);}}private void mergeToLeft(Node left, Node parent, int i) {Node right = parent.removeChild(i + 1);left.insertKey(parent.removeKey(i), left.keyNumber);right.moveToLeft(left);}private void rightRotate(Node node, Node leftSibling, Node parent, int i) {node.insertKey(parent.keys[i - 1], 0);if (!leftSibling.leaf) {node.insertChild(leftSibling.removeRightmostChild(), 0);}parent.keys[i - 1] = leftSibling.removeRightmostKey();}private void leftRotate(Node node, Node rightSibling, Node parent, int i) {node.insertKey(parent.keys[i], node.keyNumber);if (!rightSibling.leaf) {node.insertChild(rightSibling.removeLeftmostChild(), node.keyNumber + 1);}parent.keys[i] = rightSibling.removeLeftmostKey();}
}