概念
当我们使用avl树或者红黑树进行数据检索时,虽然树是平衡的,可以保证搜索的效率大概是logN。但是当我们的数据量比较大时,只能在内存中存储数据在硬盘中的指针,这时如果我们要检索数据,最少也需要比较树的高度次。
解决这个问题一方面需要提高io速度,另一方面就是降低树的高度
而要想降低树的高度,就需要让树叉变多,而我们的B-树,就是一颗M阶的树(M>2),其满足如下性质
性质
- 根节点至少有两个孩子
- 非根节点至少有M/2-1个关键字,最多M-1个关键字,并且以升序排列
- 非根节点至少有M/2个孩子,最多有M个孩子
- 关键字key[i]和key[I+1]之间的孩子的值介于两者之间
- 所有的叶子节点都在同一层
也就是说,B-树把所有的数据都存储到叶子上,通过关键字来检索数据,这样就可以减少数据的对比,而只用关键字对比即可
插入过程
简单来说就是当一个节点的数据满了,就需要将其右半边的数据拷贝到新的孩子节点里,把最中间的一个数据拷贝到父亲节点
下面通过图画来演示一下
例如我们需要插入{53, 139, 75, 49, 145, 36, 101}
由于我们会涉及到数组下标越界的问题,因此我们在构建B-树时,一般会多构建一个关键字和孩子节点
首先是插入53,139,75
在插入时需要比较大小,让小的在前面,大的在后面,当我们插入到75时,发现数组满了,那么就需要分裂了
按照上面的原则——将其右半边的数据拷贝到新的孩子节点里,把最中间的一个数据拷贝到父亲节点,由于这个节点是根节点,所以我们不仅要创建一个新的节点,放右侧的数据139,并且还要再创建一个新的根节点,放中间的数据75
然后继续按照上面的原则插入49和145
然后插入36,这时又有一个节点满了,因此需要创建一个节点,将右侧的数据53移动到新的节点,然后将中间的值49移动到其父亲节点处,需要满足从小到大的原则
当我们插入101时,依旧需要分裂节点,将中间的值139移动到根节点时,根节点也满了,因此需要再次分裂,创建一个节点放右侧的值139,再创建一个新的根放75
MyBTree
根据上述思想,我们可以自己写一个B-树,这里分步骤讲解代码,因此代码是零散的,最后会展示完整的代码
节点
节点中不仅要存储关键字数组,孩子指针数组,还要有父亲节点,以及数据的个数
static class BTRNode{
public int[] keys;
public BTRNode[] subs;
public BTRNode parent;
public int usedSize;
public BTRNode(){
//默认多给一个方便分裂
this.keys = new int[M];
this.subs = new BTRNode[M + 1];
}
}
参数
将M设置为3,也就是三叉树,我们上面在创建关键字和孩子数组时,由于需要数据的移动,所以默认多给一个元素的空间,这样可以方便我们移动
public static final int M = 3;
定义树的根节点
public BTRNode root;
插入方法
public boolean insert(int key){
先判断树中有没有数据,如果没有,那么就new一个节点,将数据插进入,把根设置成这个节点
//b树中没有数据
if(root == null){
root = new BTRNode();
root.keys[0] = key;
root.usedSize++;
return true;
}
如果树不为空,我们就需要找到合适的位置进行插入,这里写一个find方法,其参数是key
Pair<BTRNode,Integer> pair = find(key);
返回值是Pair,这个是我们自定义的一个对象,其中的Integer如果是-1,代表没有找到这个对象,而其中的BTRNode则是插入的具体位置
Pair定义如下
public class Pair <K,V>{
private K key;
private V val;
public Pair(K key, V val) {
this.key = key;
this.val = val;
}
public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
public V getVal() {
return val;
}
public void setVal(V val) {
this.val = val;
}
}
然后我们来看一下find方法的实现
find
定义cur,从根开始找,定义parent,其作用是当cur为空时可以返回cur的上一个位置,上一个位置就是需要插入的位置
我们的cur从根的关键字数组0下标开始找,如果cur的key值小于key,说明不在左侧的分支上,如果等于,说明这个节点已经存在了,那么就返回cur和i
而如果大于,说明右侧的分支都大于key的值,所以我们应该向下找cur的孩子指针数组中的对应值,直到cur为空了,说明树中确实没有这个值,那么返回parent和-1,我们就可以在parent这个节点处插入下标了
这么说比较抽象,我们可以用一个具体的例子来说明
当我们要在上面这个树上插入57时,cur先是root,i从0开始遍历,可以发现当i == 1时,cur的keys[i]的值已经大于57了,而49和75之间的孩子分支对应的下标也正好是1,因此我们可以用cur = cur.subs[i]来进行迭代
而当cur遍历完53后,cur会变成cur.subs[1],这时cur就变成空了,那么我们就返回parent,也就是53这个节点
详细代码如下:
public Pair<BTRNode,Integer> find(int key){
BTRNode cur = root;
BTRNode parent = null;
while(cur != null){
int i = 0;
while(i < cur.usedSize){
if(cur.keys[i] == key){
return new Pair<>(cur,i);
} else if(cur.keys[i] < key){
i++;
} else {
break;
}
}
parent = cur;
cur = cur.subs[i];
}
return new Pair<>(parent,-1);
}
继续我们的插入过程
当返回的val不是-1,说明找到了这个值,那么直接返回false
而如果不是-1,那么我们先拿到parent对应的节点,然后从后往前遍历,如果parent.keys[index]的值大于key,那么就把这个值往后挪,直到不大于,我们就让这个值变成key,然后让usedSize++
下面举一个详细的例子,大家可以对照这个图理解代码
//查看当前b树中是否有key
if(pair.getVal() != -1){
return false;
}
BTRNode parent = pair.getKey();
int index = parent.usedSize - 1;
for (; index >= 0 ; index--) {
if(parent.keys[index] >= key){
parent.keys[index + 1] = parent.keys[index];
} else {
break;
}
}
parent.keys[index + 1] = key;
parent.usedSize++;
接下来,我们就要看这个节点的值满没满,如果满了,就需要分裂节点,如果没满,就可以直接返回true了
if(parent.usedSize < M){
//没满
return true;
} else {
//满了,需要分裂
split(parent);
return true;
}
分裂节点
参数是传进来的节点,定义名字为cur
private void split(BTRNode cur) {
首先创建一个新的节点,定义cur的父亲节点parent,然后找到cur的最中间的关键字mid,让i从mid加一开始,j从0开始
让cur的i一直加到usedSize - 1,把这中间的所有值都拷贝到新节点的0到后面的位置,需要注意的是,不仅要拷贝关键字,并且还要拷贝孩子指针
并且,我们还要判断cur的这些孩子节点是否为空,如果不是空,我们需要改变其父亲指针的指向,将其指向新的父亲:newNode
并且,由于subs数组比key数组多一个,我们在for循环结束后还需要再拷贝一次subs数组中的值
最后,把新节点的父节点设置为cur的父节点,然后将cur和newNode的有效数据个数都更改一下,这里cur的值还要再减1,这是因为一会我们还要把最中间的值放到父亲节点
BTRNode newNode = new BTRNode();
BTRNode parent = cur.parent;
int mid = cur.usedSize / 2;
int i = mid + 1;
int j = 0;
//将cur的右侧一半的数据拷贝到新节点
for ( ; i < cur.usedSize; i++){
newNode.keys[j] = cur.keys[i];
newNode.subs[j] = cur.subs[i];
//将cur节点的孩子节点的父亲指针指向新的节点
if(newNode.subs[j] != null){
newNode.subs[j].parent = newNode;
}
j++;
}
newNode.subs[j] = cur.subs[i];
if(newNode.subs[j] != null){
newNode.subs[j].parent = newNode;
}
//新节点的父亲指针指向cur的父亲
newNode.parent = parent;
//更新新节点和cur节点的有效数据个数
newNode.usedSize = j;
cur.usedSize = cur.usedSize - j - 1;
然后,如果我们的cur节点是根节点,那么先创建一个新的根节点,将其关键字数组的第一个元素赋值为cur的最中间的元素,然后将其孩子指针数组的第一个元素指向cur,让其第二个元素指向newNode,最后更改这两个节点的父亲指针为root即可返回
//当节点是根节点
if(cur == root){
root = new BTRNode();
root.keys[0] = cur.keys[mid];
root.subs[0] = cur;
root.subs[1] = newNode;
root.usedSize = 1;
cur.parent = root;
newNode.parent = root;
return;
}
如果我们的cur不是根节点,那么就需要将中间的值移动到cur的父亲节点,先定义endT为parent的最后一个关键字所在的位置,midVal为cur的最中间的关键字的值
通过从后往前遍历parent的关键字数组,找到合适的位置进行插入midVal,并且在移动时不仅要移动关键字,还要移动孩子指针,孩子指针比关键字要多1
然后把parent的usedSize++,如果这时parent的数据也满了,那么就需要继续分裂,我们直接递归即可
//移动父亲节点
int endT = parent.usedSize - 1;
int midVal = cur.keys[mid];
for (; endT >= 0; endT--){
if(parent.keys[endT] >= midVal){
parent.keys[endT + 1] = parent.keys[endT];
parent.subs[endT + 2] = parent.subs[endT + 1];
} else {
break;
}
}
parent.keys[endT + 1] = midVal;
parent.subs[endT + 2] = newNode;
parent.usedSize++;
if (parent.usedSize >= M){
split(parent);
}
测试
大家可以用下面这个代码测试一下写的对不对,如果最终的结果是有序的,那么代码就是正确的
public static void main(String[] args) {
MyBTree myBTree = new MyBTree();
int[] arr = {53, 139, 75, 49, 145, 36, 101};
for (int i = 0; i < arr.length; i++) {
myBTree.insert(arr[i]);
}
myBTree.inorder(myBTree.root);
}
private void inorder(BTRNode root){
if(root == null)
return;
for(int i = 0; i < root.usedSize; ++i){
inorder(root.subs[i]);
System.out.println(root.keys[i]);
}
inorder(root.subs[root.usedSize]);
}
完整代码
public class MyBTree {
public static final int M = 3;
static class BTRNode{
public int[] keys;
public BTRNode[] subs;
public BTRNode parent;
public int usedSize;
public BTRNode(){
//默认多给一个方便分裂
this.keys = new int[M];
this.subs = new BTRNode[M + 1];
}
}
public BTRNode root;
/**
* 插入元素
* @param key
*/
public boolean insert(int key){
//b树中没有数据
if(root == null){
root = new BTRNode();
root.keys[0] = key;
root.usedSize++;
return true;
}
Pair<BTRNode,Integer> pair = find(key);
//查看当前b树中是否有key
if(pair.getVal() != -1){
return false;
}
BTRNode parent = pair.getKey();
int index = parent.usedSize - 1;
for (; index >= 0 ; index--) {
if(parent.keys[index] >= key){
parent.keys[index + 1] = parent.keys[index];
} else {
break;
}
}
parent.keys[index + 1] = key;
parent.usedSize++;
if(parent.usedSize < M){
//没满
return true;
} else {
//满了,需要分裂
split(parent);
return true;
}
}
/**
* 分裂
* @param cur
*/
private void split(BTRNode cur) {
BTRNode newNode = new BTRNode();
BTRNode parent = cur.parent;
int mid = cur.usedSize / 2;
int i = mid + 1;
int j = 0;
//将cur的右侧一半的数据拷贝到新节点
for ( ; i < cur.usedSize; i++){
newNode.keys[j] = cur.keys[i];
newNode.subs[j] = cur.subs[i];
//将cur节点的孩子节点的父亲指针指向新的节点
if(newNode.subs[j] != null){
newNode.subs[j].parent = newNode;
}
j++;
}
newNode.subs[j] = cur.subs[i];
if(newNode.subs[j] != null){
newNode.subs[j].parent = newNode;
}
//新节点的父亲指针指向cur的父亲
newNode.parent = parent;
//更新新节点和cur节点的有效数据个数
newNode.usedSize = j;
cur.usedSize = cur.usedSize - j - 1;
//当节点是根节点
if(cur == root){
root = new BTRNode();
root.keys[0] = cur.keys[mid];
root.subs[0] = cur;
root.subs[1] = newNode;
root.usedSize = 1;
cur.parent = root;
newNode.parent = root;
return;
}
//移动父亲节点
int endT = parent.usedSize - 1;
int midVal = cur.keys[mid];
for (; endT >= 0; endT--){
if(parent.keys[endT] >= midVal){
parent.keys[endT + 1] = parent.keys[endT];
parent.subs[endT + 2] = parent.subs[endT + 1];
} else {
break;
}
}
parent.keys[endT + 1] = midVal;
parent.subs[endT + 2] = newNode;
parent.usedSize++;
if (parent.usedSize >= M){
split(parent);
}
}
public Pair<BTRNode,Integer> find(int key){
BTRNode cur = root;
BTRNode parent = null;
while(cur != null){
int i = 0;
while(i < cur.usedSize){
if(cur.keys[i] == key){
return new Pair<>(cur,i);
} else if(cur.keys[i] < key){
i++;
} else {
break;
}
}
parent = cur;
cur = cur.subs[i];
}
return new Pair<>(parent,-1);
}
public static void main(String[] args) {
MyBTree myBTree = new MyBTree();
int[] arr = {53, 139, 75, 49, 145, 36, 101};
for (int i = 0; i < arr.length; i++) {
myBTree.insert(arr[i]);
}
myBTree.inorder(myBTree.root);
}
private void inorder(BTRNode root){
if(root == null)
return;
for(int i = 0; i < root.usedSize; ++i){
inorder(root.subs[i]);
System.out.println(root.keys[i]);
}
inorder(root.subs[root.usedSize]);
}
}