Java 实现 B树(通俗易懂)

目录

一.概念

二.节点定义

三.插入操作

1.查找位置

2.插入

3.分裂

四.B+树和B*树

1.B+树

2.B*树


一.概念

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

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

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

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

3.每个非根节点至少\frac{M}{2}(上取整)个孩子,至多M个孩子;

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

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。这个技巧在很多地方能够用到,如算法题。

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是不是已经满了,如果满了我们要进行分裂操作。

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也要进行更改。还要注意一点是,孩子地址的数组比值的数组多一个,最后还要加上。复制完不要忘记更新数据。

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

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

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

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

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+树的非根和非叶子节点再增加指向兄弟节点的指针。

Java面经是指在面试过程中常被问到的与Java相关的问题和知识点。下面是一些常见的Java面经问题及其解答: 1. Java的特点是什么? Java是一种面向对象的编程语言,具有跨平台性、简单性、可靠性、安全性和高性能等特点。 2. 什么是Java虚拟机(JVM)? JVM是Java程序运行的环境,它负责将Java源代码编译成字节码,并在不同的操作系统上执行。 3. 什么是面向对象编程(OOP)? 面向对象编程是一种编程范式,它将数据和操作数据的方法封装在一起,通过创建对象来实现程序的功能。 4. Java中的四种访问修饰符分别是什么? Java中的四种访问修饰符分别是public、protected、default和private,用于控制类、方法和变量的访问权限。 5. 什么是Java中的异常处理机制? 异常处理机制是Java中用于处理程序运行过程中出现的异常情况的一种机制,通过try-catch-finally语句块来捕获和处理异常。 6. 什么是Java中的多线程? 多线程是指在一个程序中同时执行多个线程,每个线程都可以独立执行不同的任务,提高程序的并发性和效率。 7. 什么是Java中的集合框架? 集合框架是Java中用于存储和操作一组对象的类库,包括List、Set、Map等常用的数据结构算法。 8. 什么是Java中的反射机制? 反射机制是指在运行时动态地获取和操作类的信息,可以通过反射来创建对象、调用方法和访问属性等。 9. 什么是Java中的IO流? IO流是Java中用于输入和输出数据的一种机制,包括字节流和字符流,用于读取和写入文件、网络等数据源。 10. 什么是Java中的设计模式? 设计模式是一种解决常见软件设计问题的经验总结,包括单例模式、工厂模式、观察者模式等常用的设计模式。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值