B-Tree(B树)插入实现以及B树性能
插入过程:
- 如果树为空,直接插入新节点中,该节点为树的根节点
- 树非空,找待插入元素在树中的插入位置(注意:找到的插入节点位置一定在叶子节点中)
- 检测是否找到插入位置(假设树中的key唯一,即该元素已经存在时则不插入)
- 按照插入排序的思想将该元素插入到找到的节点中
- 检测该节点是否满足B-树的性质:即该节点中的元素个数是否等于M,如果小于则满足
- 如果插入后节点不满足B树的性质,需要对该节点进行分裂:
申请新节点
找到该节点的中间位置
将该节点中间位置右侧的元素以及其孩子搬移到新节点中
将中间位置元素以及新节点往该节点的双亲节点中插入,即继续4 - 如果向上已经分裂到根节点的位置,插入结束
B-树的节点设计
class BTreeNode {
int[] keys; // 存放当前节点中的元素key
BTreeNode[] subs; // 存放当前节点的孩子节点
BTreeNode parent; // 在分裂节点后可能需要继续向上插入,为实现简单增加parent域
int size; // 当前节点中有效节点的个数
// 参数M代表B-Tree为M叉树
BTreeNode(int M) {
// M叉树:即一个节点最多有M个孩子,M-1个数据域
// 为实现简单期间,数据域与孩子与多增加一个(原因参见上文对插入过程的分析)
keys = new int[M];
subs = new BTreeNode[M + 1]; // 注意:孩子比元素多一个
size = 0;
}
}
插入key的过程
// 采用插入排序的思想插入在cur节点中插入key以及分列出的sub孩子
void insertKey(BTreeNode cur,int key,BTreeNode sub){
int end=cur.size-1;
while(end>=0&&key<cur.keys[end]){
// 将该位置元素以及其右侧孩子往右搬移一个位置
cur.keys[end+1]=cur.keys[end];
cur.subs[end+2]=cur.subs[end+1];
end--;
}
// 插入元素和孩子节点,更新size
cur.keys[end+1]=key;
cur.subs[end+2]=sub;
cur.size++;
if(sub!=null)
sub.parent=cur;
}
B-树的插入实现
boolean insert(int key) {
if (null == root) {
root = new BTreeNode(M);
root.keys[0] = key;
root.size = 1;
return true;
}
// 查找当前元素的插入位置
// 如果返回的键值对的value不等于-1,说明该元素已经存在,则不插入
Pair<BTreeNode, Integer> ret = find(key);
if (-1 != ret.getValue())
return false;
// 注意:在B-Tree中找到的待插入的节点都是叶子节点
BTreeNode cur = ret.getKey();
int k = key;
BTreeNode sub = null; // 主要在分列节点时起作用
while (true) {
insertKey(cur, k, sub);
// 元素插入后,当前节点可以放的下,不需要分列
if (cur.size < M)
break;
// 新节点插入后,cur节点不满足B-Tree的性质,需要对节点进行分列
// 具体分列的方式
// 1. 找到节点的中间位置
// 2. 将中间位置右侧的元素以及孩子插入到分列的新节点中
// 3. 将中间位置的元素以及分列出的新节点向当前分列节点的双亲中继续插入
int mid = (cur.size >> 1);
BTreeNode newNode = new BTreeNode(M);
// 将中间位置右侧的所有元素以及孩子搬移到新节点中
int i = 0;
int index = mid + 1; // 中间位置的右侧
for (; index < cur.size; ++index) {
// 搬移元素
newNode.keys[i] = cur.keys[index];
// 搬移元素对应的孩子
newNode.subs[i++] = cur.subs[index];
// 孩子被搬移走了,需要重新更新孩子双亲
if (cur.subs[index] != null)
cur.subs[index].parent = newNode;
}
// 注意:孩子要比双亲多搬移一个
newNode.subs[i] = cur.subs[index];
if (cur.subs[index] != null)
cur.subs[index].parent = newNode;
// 更新newNode以及cur节点中剩余元素的个数
// cur中的i个元素搬移到了newNode中
// cur节点的中间位置元素还要继续向其双亲中插入
newNode.size = i;
cur.size = cur.size - i - 1;
k = cur.keys[mid];
// 说明分列的cur节点刚好是根节点
if (cur == root) {
root = new BTreeNode(M);
root.keys[0] = k;
root.subs[0] = cur;
root.subs[1] = newNode;
root.size = 1;
cur.parent = root;
newNode.parent = root;
break;
} else {
// 继续向双亲中插入
sub = newNode;
cur = cur.parent;
}
}
return true;
}
B-树的性能分析
对于一棵节点为N度为M的B-树,查找和插入需要
l
o
g
M
−
1
N
log{M-1}N
logM−1N~
l
o
g
M
/
2
N
log{M/2}N
logM/2N次比较,这个很好证明:对于度为M的B-树,每一个节点的子节点个数为M/2 ~(M-1)之间,因此树的高度应该在要
l
o
g
M
−
1
N
log{M-1}N
logM−1N和
l
o
g
M
/
2
N
log{M/2}N
logM/2N之间,在定位到该节点后,再采用二分查找的方式可以很快的定位到该元素。
B-树的效率是很高的,对于N = 62*1000000000个节点,如果度M为1024,则
l
o
g
M
/
2
N
log_{M/2}N
logM/2N <= 4,即在620亿个元素中,如果这棵树的度为1024,则需要小于4次即可定位到该节点,然后利用二分查找可以快速定位到该元素,大大减少了读取磁盘的次数。