Java 实现 B树(通俗易懂)

目录

一.概念

二.节点定义

三.插入操作

1.查找位置

2.插入

3.分裂

四.B+树和B*树

1.B+树

2.B*树


一.概念

B树是一颗多叉平衡树,空树也是多叉平衡树。

一颗M阶的B树要满足以下条件:

1.根节点至少有两个孩子;

2.每个非根节点至少(上取整)个关键字,至多个关键字,并且以升序排列;

3.每个非根节点至少(上取整)个孩子,至多个孩子;

4.key[i]和key[i+1]之间的孩子节点的值介于key[i]、key[i+1]之间;

5.所有的叶子节点 都在同一层

解读一下上面的条件:一颗M阶树的意思是一个节点可以存储多个值,最多存储M-1个,像二叉树使用right和left来存储子树的位置,B树用sub来存储位置,不过这个要存储很多个,最多是M个。就是说每一个存在节点的值都有一个左孩子和右孩子,他们遵从条件4。像下面这个图,连起来的黑线表示是父子关系。

二.节点定义

B树与二叉树不同的是其有多个值,且有多个孩子,这些要开一个数组储存;同时我们还要记录节点的父节点是那个;我们还需要存储数组已使用的空间的数量,方便我们后序操作。

关于数组大小的定义,正常来说一个根能存储的关键字(值)是M-1个的,能存储的地址是M个,这里为什么都多出一个呢?这里是为了后面的分裂操作做准备。等到了后面的分裂操作大家就能感受到这么设计的巧妙之处了。

补充:flag是用于后面判断是否找到要插入位置时使用的,m是M阶B树的M

java 复制代码
static class TreeNode{
    public int[] key;
    public TreeNode[] sub;
    public TreeNode parent;
    public int usedSize;

    public TreeNode(){
        this.key=new int[m];
        this.sub=new TreeNode[m+1];
        usedSize=0;
    }
}
TreeNode root;
boolean flag=false;
public static final int m=3;

三.插入操作

1.查找位置

要正确插入首先要找到位置。

B树的查找与二叉树还是相似的。先找当前节点内的值,如果我们要插入的val值比数组中的某个值大,那么我们要继续往后比较;如果val比数组中的某个值小,那么我们就进入它的左子树中继续比较;如果val等于数组中的某个值,那么就无法插入。

总结:1.val<key[i] 循环继续;2.val>key[i] 循环结束进入子树;3. val=key[i] 直接返回。

在定义节点的时候我们定义了一个flag,当我们在遇到情况3时,可以让flag=true。这个技巧在很多地方能够用到,如算法题。

java 复制代码
public TreeNode find(int val){
    TreeNode cur=root;
    TreeNode parent = null;
    flag=false;

    while(cur!=null){
        int i=0;
        while(i< cur.usedSize){
            if(cur.key[i]>val){
                break;
            }else if(cur.key[i]<val){
                i++;
            }else{
                flag=true;
                return null;
            }
        }
        parent=cur;
        cur=cur.sub[i];
    }

    return parent;
}

为什么cur=cur.sub[i]呢?下图可以解释:

比如我们要插入4,4<5,我们在i=1时要停下来进入左子树了,刚好sub[i]是5的左子树。

2.插入

找到位置后我们就要进行插入操作了。

首先我们先判断根节点是不是空的,如果是空的我们要先插在根上。

如果我们定义的flag是true的话,说明这个点已经有了无法插入,直接返回即可。

插入操作比较简单,就是一个直接插入排序,插入完不要忘记更新usedSize值(重要)。

插入完我们要判断usedSize是不是已经满了,如果满了我们要进行分裂操作。

java 复制代码
public boolean insert(int val){
        //如果根节点是空的
        if(root==null){
            root=new TreeNode();
            root.key[0]=val;
            root.usedSize++;
            return true;
        }

        TreeNode parent=find(val);
        if(flag==true){
            //已经有这个点了
            return false;
        }

        //开始插入
        int i = parent.usedSize-1;
        for (; i >= 0;i--) {
            if(parent.key[i] >= val) {
                parent.key[i+1] = parent.key[i];
            }else {
                break;
            }
        }
        parent.key[i+1] = val;
        parent.usedSize++;

        if(parent.usedSize<m){
            //没满
            return true;
        }else{
            split(parent);
            return true;
        }
    }

3.分裂

先说一下怎么分裂:

我们先找到中间位置,中间位置的坐标是mid=M/2。这个中间位置的节点就是要上提的,这个节点的左面数组部分就变成了它的左子树,这个节点右面数组部分就变成了它的右子树。

例子:一颗4阶B树,下图

我们可以将分裂分为两步,先单独建立一个节点,将中间值右面部分复制到新节点里;再将中间值上提到原来数组里的父节点里。

第一步,复制右半部分,不仅要复制值,还要复制孩子的地址。这里要记住,复制了孩子的地址仅仅只是父节点连接上了孩子,孩子的parent也要进行更改。还要注意一点是,孩子地址的数组比值的数组多一个,最后还要加上。复制完不要忘记更新数据。

第二步,上提。这里分为两种情况:是根,不是根。

先说是根,如果当前节点是根节点,那么上提的话要新建一个根,如果再进行复制更新。

如果不是根,将中间值插入到数组后,然后进行直接插入排序。

最后都处理完判断上提到的数组是不是满了,如果满了要继续进行分裂。

java 复制代码
public void split(TreeNode cur){
    //处理右半部分
    TreeNode node=new TreeNode();
    int mid= cur.usedSize/2;
    int i=mid+1;
    int j=0;
    for(;i<cur.usedSize;i++){
        node.key[j]=cur.key[i];
        node.sub[j]=cur.sub[i];

        if(node.sub[j]!=null){
            node.sub[j].parent=node;
        }

        j++;
    }
    node.sub[j]=cur.sub[i];
    if(node.sub[j]!=null){
        node.sub[j].parent=node;
    }
    //更新数据
    node.usedSize=j;
    cur.usedSize= cur.usedSize-j-1;
    node.parent=cur.parent;

    //特判:cur是根节点
    if(cur==root){
        root=new TreeNode();
        root.key[0]=cur.key[mid];
        root.sub[0]=cur;
        root.sub[1]=node;
        node.parent=root;
        cur.parent=root;
        root.usedSize++;

        return;
    }

    //上提
    TreeNode parent=cur.parent;
    int end=parent.usedSize-1;
    int val=cur.key[mid];
    for (; end >= 0;end--) {
        if(parent.key[end] >= val) {
            parent.key[end+1] = parent.key[end];
            parent.sub[end+2] = parent.sub[end+1];
        }else {
            break;
        }
    }
    parent.key[end+1] = val;
    parent.sub[end+2] = node;
    parent.usedSize++;

    if(parent.usedSize>=m){
        split(parent);
    }
}

四.B+树和B*树

1.B+树

B+树的B树的变种,再B树的基础上加入新的规则:

1.关键字的数量和孩子的数量相同

2.非叶子节点的子树指针p[i],指向关键字值属于[ k[i],k[i+1] )的子树(都是右子树);

3.为所有叶子节点增加一个链指针

4.所有关键字都在叶子节点出现。

2.B*树

B*树的B+树的变种,在B+树的非根和非叶子节点再增加指向兄弟节点的指针。

相关推荐
m0_571957582 小时前
Java | Leetcode Java题解之第543题二叉树的直径
java·leetcode·题解
一点媛艺3 小时前
Kotlin函数由易到难
开发语言·python·kotlin
姑苏风3 小时前
《Kotlin实战》-附录
android·开发语言·kotlin
奋斗的小花生4 小时前
c++ 多态性
开发语言·c++
魔道不误砍柴功4 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2344 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
pianmian14 小时前
python数据结构基础(7)
数据结构·算法
闲晨4 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
老猿讲编程4 小时前
一个例子来说明Ada语言的实时性支持
开发语言·ada
Chrikk5 小时前
Go-性能调优实战案例
开发语言·后端·golang