当前位置: 首页> 文旅> 文化 > Java 实现 B树(通俗易懂)

Java 实现 B树(通俗易懂)

时间:2025/7/11 19:05:29来源:https://blog.csdn.net/lllsure/article/details/141182492 浏览次数:0次

目录

一.概念

二.节点定义

三.插入操作

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 实现 B树(通俗易懂)

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

责任编辑: