首页 > 编程语言 >Java 实现 B树(通俗易懂)

Java 实现 B树(通俗易懂)

时间:2024-08-14 12:24:17浏览次数:9  
标签:Java val parent 实现 usedSize 通俗易懂 key TreeNode 节点

目录

一.概念

二.节点定义

三.插入操作

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,val,parent,实现,usedSize,通俗易懂,key,TreeNode,节点
From: https://blog.csdn.net/lllsure/article/details/141182492

相关文章

  • 基于flask+vue框架的智慧校园管理系统的设计与实现[开题+论文+程序]-计算机毕设
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容研究背景随着信息技术的飞速发展,智慧校园的概念逐渐深入人心,成为现代教育领域的重要趋势。传统校园管理模式在面对日益复杂的教育环境和多元化需求......
  • 面试官:说说volatile应用和实现原理?
    volatile是并发编程中的重要关键字,它的名气甚至是可以与synchronized、ReentrantLock等齐名,也是属于并发编程五杰之一。需要注意的是volatile并不能保证原子性,因此使用volatile并没有办法保证线程安全。并发编程五杰:PS:“并发编程五杰”是我个人起的名字,大家也不用太......
  • C语言实现排序之归并排序算法
    一、归并排序算法基本思想        归并排序采用分治法策略,将数组递归地分成两半,然后对每一半进行排序,最后将两个已排序的子数组合并成一个完整的有序数组。步骤递归划分:将数组分成两半,直到每个子数组只有一个元素。递归排序:递归地对每个子数组进行排序。合并子数组......
  • C语言实现排序之堆排序算法
    一、堆排序算法基本思想堆排序是一种比较有效的排序方法,其基本思想是:构建最大堆:首先将待排序的数组构建成一个最大堆,即对于每个非叶子节点,它的值都大于或等于其子节点的值。排序:然后将堆顶元素(最大值)与堆的最后一个元素交换位置,将其移出堆,并调整剩余元素以保持最大堆的性质......
  • Java基础-学习笔记11
    11枚举、注解枚举枚举是一组常量的集合。可以这么理解:枚举属于一种特殊的类,里面只包含一组有限的特定的对象。比如,Season类,只包含SPRING、SUMMER、AUTUMN、WINTER四个对象常量。两种实现方式(1)自定义类实现枚举     1)构造器私有化     2)本类内部创建一组对......
  • Java中封装的学习
    封装目录封装概念优点例子概念封装(encapsulation)是指对于某个对象,Java隐藏对象的属性和实现细节,仅对外公开接口,控制在程序中属性的读取和修改的访问级别。封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。要访问该类的代码和数据,必须通过严格......
  • java几种常见漏洞种类及处理方案
    一、SQLInjection(SQL注入漏洞)1.使用参数化查询(PreparedStatements)参数化查询是防止SQL注入最有效的方法之一。它确保用户输入的数据作为参数传递,而不是作为SQL命令的一部分。在Java中,可以使用PreparedStatement来实现这一点。示例代码:Stringsql="SELECT*FROMusers......
  • echrts 折线图实现数据排名展示
    效果:  接口结构:{"data":[{"tdtRank":1,"tranTransferOvertimeRate":0.0211,"outtranOpOvertimeRate":0.0041,"siteDispSignOvertimeRate":0.019......
  • Java--抽象类与接口
    目录抽象类的概念1.什么是抽象(与具体类相对)2.为什么要抽象抽象类的好处抽象类和接口的区别抽象类的概念1.什么是抽象(与具体类相对)Java专门提供了一种机制,名为“抽象方法”。它属于一种不完整的方法,只含有一个声明,没有方法主体。下面是抽象方法声明时采用的语法:abstractvoidX......
  • Java--继承
    目录概念优缺点object类null概念由于封装,使得有共同特征的一类事物的所有描述信息都被归于一类之中,但我们知道,这并不是万能的,有些事物有共性,但还存在区别,比如码农,简单封装起来如下:优缺点优点:1、提高代码的维护性(只需要改动父类)。2、提高代码的复用性(共性的成员抽取到父类中......