一、概述
1. 历史
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年代,而且在今天仍然被广泛应用于各种场景。
2. B的含义
B树的名称是由其发明者Rudolf Bayer提出的。Bayer和McCreight从未解释B代表什么,人们提出了许多可能的解释,比如Boeing、balance、between、broad、bushy和Bayer等。但McCreight表示,越是思考B-trees中的B代表什么,就越能更好地理解B-trees。
3. 特性
一棵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:叶子节点的深度都相同
问题1:B-树为什么有最小度数的限制?
答:B树种有最小度数的限制是为了保证B树的平衡特性。
在B树中,每个节点都可以有多个子节点,这使得B树可以存储大量的键值,但也带来了一些问题。如果节点的子节点数量太少,那么就可能导致B树的高度过高,从而降低了B树的效率。此外,如果节点的子节点数量太多,那么就可能导致节点的搜索、插入和删除操作变得复杂和低效。
最小度数的限制通过限制节点的子节点数量,来平衡这些问题。在B树种,每个节点的子节点数量都必须在一定的范围内,即t到2t之间(其中t为最小度数)
4. B-树与2-3树、2-3-4树的关系
可以这样总计它们之间的关系:
- 2-3树是最小度数为2的B树,其中每个节点可以包含2个或3个子节点
- 2-3-4树是最小度数为2的B树的一种特殊情况,其中每个节点可以包含2个、3个或4个子节点
- B树是一种更加一般化的平衡树,可以适应不同的应用场景,其节点可以包含任意数量的键值,节点的度数取决于最小度数t的设定。
二、实现
1. 定义节点
java
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];
}
@Override
public String toString() {
return Arrays.toString(Arrays.copyOfRange(keys, 0, keyNumber));
}
}
- leaf表示是否是叶子节点
- keyNumber为keys中有效key数目
- t为最小度数,它决定了节点中key的最小、最大数目,分别是t - 1 和 2t - 1
- keys存储此节点的key
- children存储此节点的child
- toString只是为了方便调试和测试,非必须
- 实际keys应当改为entries以便同时保存key和value,刚开始简化实现
2. 多路查找
为上面节点添加get方法
java
/**
* 多路查找
* @param key
* @return
*/
public 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);
}
3. 插入key和child
为上面节点类添加insertKey和insertChild方法
java
/**
* 向keys指定索引处插入key
* @param key
* @param index
*/
public void insertKey(int key, int index) {
System.arraycopy(keys, index, keys, index + 1, keyNumber - index);
keys[index] = key;
keyNumber++;
}
/**
* 向children指定索引处插入child
* @param child
* @param index
*/
public 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
4. 定义树
java
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);
}
}
5. 插入
java
/**
* 新增
* @param key
*/
public void put(int key) {
doPut(root, key, null, 0);
}
private void doPut(Node node, int key, Node parent, int index) {
// 1. 查找本节点的插入位置i
int i = 0;
while(i < node.keyNumber) {
if(node.keys[i] == key) {
// 更新
return;
}
if(node.keys[i] > key) {
break; // 找到插入位置,即为此时的i
}
i++;
}
// 2. 如果节点是叶子节点,可以直接插入了
if(node.leaf) {
node.insertKey(key, i);
// 上限
}
// 3. 如果节点是非叶子节点,需要在children[i]处继续递归插入
else {
doPut(node.children[i], key, node, i);
// 上限
}
if(isFull(node)) {
split(node, parent, index);
}
}
boolean isFull(Node node) {
return node.keyNumber == MAX_KEY_NUMBER;
}
首先查找本节点中的插入位置i,如果没有空位(key被找到),应该走更新的逻辑,目前什么没做
接下来分两种情况:
- 如果节点是叶子节点,可以直接插入了
- 如果节点是非叶子节点,需要在children[i]处继续递归插入
无论哪种情况,插入完成后都可能超过节点keys数目限制,此时应当执行节点分裂
- 参数中的parent和index都是给分裂方法用的,代表当前节点父节点,和分裂节点都是第几个孩子
判断依据为:
java
boolean isFull(Node node) {
return node.keyNumber == MAX_KEY_NUMBER;
}
6. 分裂
java
/**
* 分裂
* @param left 要分裂的节点
* @param parent 分裂节点的父节点
* @param index 分裂节点是第几个孩子
*/
private 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);
}
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);
}
分为两种情况:
①如果parent == null,表示要分裂的是根节点,此时需要创建新根,原来的根节点作为新根的0孩子
②否则
- 创建right节点(分裂后大于当前left节点的)把t以后的key和child都拷贝过去
- t - 1处的key插入到parent的index处,index指left作为孩子时的索引
- right节点作为parent的孩子插入到index+1处
7. 删除
case 1:当前节点是叶子节点,没找到
case 2:当前节点是叶子节点,找到了
case 3:当前节点是非叶子节点,没找到
case 4:当前节点是非叶子节点,找到了
case 5:删除后key数目 < 下限(不平衡)
case 6:根节点
Node节点类添加一些方法:
java
/**
* 向keys指定索引处插入key
* @param key
* @param index
*/
public void insertKey(int key, int index) {
System.arraycopy(keys, index, keys, index + 1, keyNumber - index);
keys[index] = key;
keyNumber++;
}
/**
* 向children指定索引处插入child
* @param child
* @param index
*/
public void insertChild(Node child, int index) {
System.arraycopy(children, index, children, index + 1, keyNumber - index);
children[index] = child;
}
/**
* 移除指定index处的key
* @param index
* @return
*/
int removeKey(int index) {
int t = keys[index];
System.arraycopy(keys, index + 1, keys, index, --keyNumber - index);
return t;
}
/**
* 移除最左边的key
* @return
*/
public int removeLeftmostKey() {
return removeKey(0);
}
/**
* 移除最右边的key
* @return
*/
public int removeRightmostKey() {
return removeKey(keyNumber - 1);
}
/**
* 移除指定index处的child
* @param index
* @return
*/
public 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
* @return
*/
public Node removeLeftmostChild() {
return removeChild(0);
}
/**
* 移除最右边的child
* @return
*/
public Node removeRightmostChild() {
return removeChild(keyNumber);
}
/**
* index 孩子处左边的兄弟
* @param index
* @return
*/
public Node childLeftSibling(int index) {
return index > 0 ? children[index - 1] : null;
}
/**
* index 孩子处右边的兄弟
* @param index
* @return
*/
public Node childRightSibling(int index) {
return index == keyNumber ? null : children[index + 1];
}
/**
* 复制当前节点的所有key和child到target
* @param target
*/
public 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];
}
}
删除代码:
java
/**
* 删除key
* @param key
*/
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++;
}
// 情况1:找到,i代表待删除key的索引
// 情况2:没找到,i代表到第i个孩子继续查找
if (node.leaf) { // 当前节点是叶子节点
if(!found(node,key, i)) { // case 1 没找到
return;
} else { // case 2 找到了
node.removeKey(i);
}
} else { // 当前节点不是叶子节点
if(!found(node,key, i)) { // case 3 没找到
// 到孩子节点继续查找
doRemove(node, node.children[i], i, key);
} else { // case 4 找到了
// 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);
}
}
// 删除后key数目小于下限
if(node.keyNumber < MIN_KEY_NUMBER) {
// 调整平衡 case 5 case 6
balance(parent, node, index);
}
}
/**
* 是否找到key
* @param node
* @param key
* @param i
* @return
*/
private boolean found(Node node, int key, int i) {
return i < node.keyNumber && node.keys[i] == key;
}
/**
* 调整平衡
* @param parent 父节点
* @param x 待调整节点
* @param i 索引
*/
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);
if(left != null && left.keyNumber > MIN_KEY_NUMBER) {
// case 5-1 左边富裕,右旋
// a) 父节点中前驱key旋转下来
x.insertKey(parent.keys[i - 1], 0);
if(!left.leaf) {
// b) 左边的兄弟不是叶子节点,把最右侧的孩子过继给被调整的节点
x.insertChild(left.removeRightmostChild(), 0);
}
// c) 删除左边兄弟的最右节点,移到父节点(旋转上去)
parent.keys[i - 1] = left.removeRightmostKey();
return;
}
if(right != null && right.keyNumber > MIN_KEY_NUMBER) {
// case 5-2 右边富裕,左旋
// a) 父节点中后去key旋转下来
x.insertKey(parent.keys[i], x.keyNumber);
if(!right.leaf) {
// b) 右边的兄弟不是叶子节点,把最左侧的孩子过继给被调整的节点
x.insertChild(right.removeLeftmostChild(), x.keyNumber + 1);
}
// c) 删除右边兄弟的最左节点,移到父节点(旋转上去)
parent.keys[i] = right.removeLeftmostKey();
return;
}
// case 5-3 两边都不富裕,向左合并
if(left != null) {
// 向左兄弟合并
// 将待删除节点从父节点移除
parent.removeChild(i);
// 从父节点合并一个key到左兄弟
left.insertKey(parent.removeKey(i - 1), left.keyNumber);
// 将待删除节点的剩余节点和孩子移到到左边
x.moveToTarget(left);
} else {
// 没有左兄弟,向自己合并
// 把它的右兄弟移除
parent.removeChild(i + 1);
// 父节点移除一个key,插入到待删除节点
x.insertKey(parent.removeKey(i), x.keyNumber);
// 将右兄弟合并过来
right.moveToTarget(x);
}
}
8. 完整代码
java
package com.itheima.datastructure.BTree;
import java.util.Arrays;
public class BTree {
static class Node {
int[] keys; // 关键字
Node[] children; // 孩子
int keyNumber; // 有效关键字数目
boolean leaf = true; // 是否是叶子节点
int t; // 最小度数
public Node(int t) { // t >= 2
this.t = t;
this.children = new Node[2 * t];
this.keys = new int[2 * t - 1];
}
public Node(int[] keys) {
this.keys = keys;
}
@Override
public String toString() {
return Arrays.toString(Arrays.copyOfRange(keys, 0, keyNumber));
}
/**
* 多路查找
* @param key
* @return
*/
public 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
* @param key
* @param index
*/
public void insertKey(int key, int index) {
System.arraycopy(keys, index, keys, index + 1, keyNumber - index);
keys[index] = key;
keyNumber++;
}
/**
* 向children指定索引处插入child
* @param child
* @param index
*/
public void insertChild(Node child, int index) {
System.arraycopy(children, index, children, index + 1, keyNumber - index);
children[index] = child;
}
/**
* 移除指定index处的key
* @param index
* @return
*/
int removeKey(int index) {
int t = keys[index];
System.arraycopy(keys, index + 1, keys, index, --keyNumber - index);
return t;
}
/**
* 移除最左边的key
* @return
*/
public int removeLeftmostKey() {
return removeKey(0);
}
/**
* 移除最右边的key
* @return
*/
public int removeRightmostKey() {
return removeKey(keyNumber - 1);
}
/**
* 移除指定index处的child
* @param index
* @return
*/
public 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
* @return
*/
public Node removeLeftmostChild() {
return removeChild(0);
}
/**
* 移除最右边的child
* @return
*/
public Node removeRightmostChild() {
return removeChild(keyNumber);
}
/**
* index 孩子处左边的兄弟
* @param index
* @return
*/
public Node childLeftSibling(int index) {
return index > 0 ? children[index - 1] : null;
}
/**
* index 孩子处右边的兄弟
* @param index
* @return
*/
public Node childRightSibling(int index) {
return index == keyNumber ? null : children[index + 1];
}
/**
* 复制当前节点的所有key和child到target
* @param target
*/
public 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];
}
}
}
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;
}
/**
* key是否存在
* @param key
* @return
*/
public boolean contains(int key) {
return root.get(key) != null;
}
/**
* 新增
* @param key
*/
public void put(int key) {
doPut(root, key, null, 0);
}
private void doPut(Node node, int key, Node parent, int index) {
// 1. 查找本节点的插入位置i
int i = 0;
while(i < node.keyNumber) {
if(node.keys[i] == key) {
// 更新
return;
}
if(node.keys[i] > key) {
break; // 找到插入位置,即为此时的i
}
i++;
}
// 2. 如果节点是叶子节点,可以直接插入了
if(node.leaf) {
node.insertKey(key, i);
// 上限
}
// 3. 如果节点是非叶子节点,需要在children[i]处继续递归插入
else {
doPut(node.children[i], key, node, i);
// 上限
}
if(isFull(node)) {
split(node, parent, index);
}
}
boolean isFull(Node node) {
return node.keyNumber == MAX_KEY_NUMBER;
}
/**
* 分裂
* @param left 要分裂的节点
* @param parent 分裂节点的父节点
* @param index 分裂节点是第几个孩子
*/
private 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);
}
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);
}
/**
* 删除key
* @param key
*/
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++;
}
// 情况1:找到,i代表待删除key的索引
// 情况2:没找到,i代表到第i个孩子继续查找
if (node.leaf) { // 当前节点是叶子节点
if(!found(node,key, i)) { // case 1 没找到
return;
} else { // case 2 找到了
node.removeKey(i);
}
} else { // 当前节点不是叶子节点
if(!found(node,key, i)) { // case 3 没找到
// 到孩子节点继续查找
doRemove(node, node.children[i], i, key);
} else { // case 4 找到了
// 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);
}
}
// 删除后key数目小于下限
if(node.keyNumber < MIN_KEY_NUMBER) {
// 调整平衡 case 5 case 6
balance(parent, node, index);
}
}
/**
* 是否找到key
* @param node
* @param key
* @param i
* @return
*/
private boolean found(Node node, int key, int i) {
return i < node.keyNumber && node.keys[i] == key;
}
/**
* 调整平衡
* @param parent 父节点
* @param x 待调整节点
* @param i 索引
*/
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);
if(left != null && left.keyNumber > MIN_KEY_NUMBER) {
// case 5-1 左边富裕,右旋
// a) 父节点中前驱key旋转下来
x.insertKey(parent.keys[i - 1], 0);
if(!left.leaf) {
// b) 左边的兄弟不是叶子节点,把最右侧的孩子过继给被调整的节点
x.insertChild(left.removeRightmostChild(), 0);
}
// c) 删除左边兄弟的最右节点,移到父节点(旋转上去)
parent.keys[i - 1] = left.removeRightmostKey();
return;
}
if(right != null && right.keyNumber > MIN_KEY_NUMBER) {
// case 5-2 右边富裕,左旋
// a) 父节点中后去key旋转下来
x.insertKey(parent.keys[i], x.keyNumber);
if(!right.leaf) {
// b) 右边的兄弟不是叶子节点,把最左侧的孩子过继给被调整的节点
x.insertChild(right.removeLeftmostChild(), x.keyNumber + 1);
}
// c) 删除右边兄弟的最左节点,移到父节点(旋转上去)
parent.keys[i] = right.removeLeftmostKey();
return;
}
// case 5-3 两边都不富裕,向左合并
if(left != null) {
// 向左兄弟合并
// 将待删除节点从父节点移除
parent.removeChild(i);
// 从父节点合并一个key到左兄弟
left.insertKey(parent.removeKey(i - 1), left.keyNumber);
// 将待删除节点的剩余节点和孩子移到到左边
x.moveToTarget(left);
} else {
// 没有左兄弟,向自己合并
// 把它的右兄弟移除
parent.removeChild(i + 1);
// 父节点移除一个key,插入到待删除节点
x.insertKey(parent.removeKey(i), x.keyNumber);
// 将右兄弟合并过来
right.moveToTarget(x);
}
}
}