Java——B-树

概念

当我们使用avl树或者红黑树进行数据检索时,虽然树是平衡的,可以保证搜索的效率大概是logN。但是当我们的数据量比较大时,只能在内存中存储数据在硬盘中的指针,这时如果我们要检索数据,最少也需要比较树的高度次。

解决这个问题一方面需要提高io速度,另一方面就是降低树的高度

而要想降低树的高度,就需要让树叉变多,而我们的B-树,就是一颗M阶的树(M>2),其满足如下性质

性质

  1. 根节点至少有两个孩子
  2. 非根节点至少有M/2-1个关键字,最多M-1个关键字,并且以升序排列
  3. 非根节点至少有M/2个孩子,最多有M个孩子
  4. 关键字key[i]和key[I+1]之间的孩子的值介于两者之间
  5. 所有的叶子节点都在同一层

也就是说,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]);
    }
}

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值