使用Java实现一个简单的B树

news2024/9/29 1:17:17

1.B树简介

B树是一个搜索树,数据结构可以抽象成如二叉树一样的树,不过它有平衡、有序、多路的特点。

  • 平衡:所有叶子节点都在同一层。
  • 有序:任一元素的左子树都小于它,右子树都大于它。
  • 多路:B树的每个节点最多有m个子节点,m即为B数的阶数,同时由阶数可以得到另一个概念:B树的度数,它决定了每个节点最多存在的元素个数。

其结构图如下:

8ad6bb26f2bd4f9193869a91897c794b.jpeg

2.代码实现

2.1 B树节点类设计

首先定义一个节点,代码结构如下,它的成员变量以及函数定义如下。

  • curKeyNums记录当前节点元素个数,用于判断节点是否需要分裂的操作等。
  • keys数组接收每个节点的元素,初始化时定义一个B树度数的数组,表示最多能容纳n个元素。
  • children数组负责存储对应keys数组中每个元素对应的左子树、右子树,初始化时定义一个B树度数长度+1的数组,表示该节点最多有B树度数+1的子节点。
  • isLeaf()方法判断当前节点是否是叶子节点,判断依据就是看它有没有“孩子”。
public class BtreeNode {
   int curKeyNums;
   Integer[] keys;
   BtreeNode[] children;
   public BtreeNode(int degrees){
       curKeyNums = 0;
       keys = new Integer[degrees];
       children = new BtreeNode[degrees+1];
   }
   public boolean isLeaf(){
       return children[0] == null;
   }
}

假设某个key在keys数组中的下标是i,那么在children数组中小于等于i的子树都是该key的左子树,都小于key,同理大于i的子树都是该key的右子树,都大于key。这个结论非常重要,务必要记住,因为将会在后面的合并节点中使用到。

2.2 B树类设计

B树是一颗树,因此它的设计如下:

  • degress表示一颗B树的度数,即任一节点最多容纳的key数量。
  • root根节点,因为本质上是一颗结构树,B树的增删查操作都是通过根节点作为入口完成的。
public class Btree {
    int degrees; // 度数
    BtreeNode root; // B树的根节点

    public Btree(int degrees){
        this.degrees = degrees;
        root = new BtreeNode(degrees);
    }
}

2.3 增加元素到B树

这个小节将用Java手动实现怎么将一个元素添加到B树。

增加元素要考虑的场景其实也比较复杂,从上面定义的B树度数、任一元素的左右子树均小于、大于该元素,因此会涉及一个节点拆分的过程,此过程不是太复杂,复杂的是拆分以后的节点合并工作。

2.3.1 二分查找确定插入key的位置

当添加一个新的key到B树时,假设此时找到了需要添加的节点(keys数组也没满),那么如何确定待插入的key应该放在keys数组的哪里吗?

通过二分查找(时间复杂度O(logN))的方法确定插入下标,二分查找的特点是根据中间元素和待插入key判断,随时调整每一次遍历的起始下标,具体代码如下:

    private int binarySearchInsertIdx(int key, BtreeNode node){
        // 二分查找的特点是随时调整左右下标
        int left = 0;
        int right = node.curKeyNums-1;
        while (left<=right){
            int mid = (right-left)/2 + left;// 再加上left的意思是补充移位
            if (node.keys[mid]>key){
                // 往左边查找
                right = mid-1;
            } else if (node.keys[mid]<key) {
                // 往右边查找
                left = mid+1;
            }else {
                return mid;
            }
        }
        return left;
    }

2.3.2 节点分裂操作

当插入元素,找到一个插入节点时,若发现此时的keys数组已满,则需要进行节点的拆分。

由于keys数组的元素插入都是通过二分查找的方式找到插入下标完成,因此keys数组是有序的。

因此这里我们用 (keys.length/2)计数出中间元素的下标即为midIdx,即此时下标小于midIdx的元素都小于中间元素,提取为左子树,大于midIdx的元素都大于中间元素,提取为右子树。此外中间元素将升级为一个新的父节点,将中间元素值直接存入keys[0]中,该新父节点的curKeyNums为1,这是后面插入方法中判断是否分裂成了一个新节点的判断依据(因为B树的度数一般都大于1)。

具体代码如下:

    private BtreeNode splitNode(BtreeNode node){
        // 拆分节点,进到这里的node的keys数量肯定是刚好等于B树的度数了
        int midIdx = (node.curKeyNums)/2;
        int extractVal = node.keys[midIdx];
        BtreeNode newFatherNode = new BtreeNode(degrees);
        newFatherNode.keys[0] = extractVal;
        newFatherNode.curKeyNums++;
        // 小于midIdx的作为新父节点的左子树
        BtreeNode leftNode = new BtreeNode(degrees);
        for (int i=0;i<midIdx;i++){
            leftNode.curKeyNums++;
            leftNode.keys[i] = node.keys[i];
        }
        // 大于midIdx的作为新父节点的右子树,右子树的填充使用倒序更加明确
        BtreeNode rightNode = new BtreeNode(degrees);
        for (int i= node.curKeyNums-1;i>midIdx;i--){
            rightNode.curKeyNums++;
            rightNode.keys[i-midIdx-1] = node.keys[i];
        }
        newFatherNode.children[0]=leftNode;
        newFatherNode.children[1]=rightNode;
        return newFatherNode;
    }

2.3.3 插入元素操作

插入元素的操作,切入点都是B树的root根节点。因为后面的插入操作都是通过递归完成的,需要有较强的递归回溯思维才能更好理解。

首先 insert(int key)方法是外界添加元素到B树的唯一入口,里面调用了insertKeyToNode(key, root)方法,表示每一次的插入元素都是以root根节点为入口。在insertKeyToNode方法中,会存在递归调用自己的情况。

首先,我们分析以下这几种情况:

①keys数组还没有满并且是叶子节点:

(新元素必须添加到叶子节点中)将key插入到二分查找的插入下标位置中。如度数为3的B树,有这么一个节点的keys=【2,5】,要插入的key是1,则最终找到的插入下标是0,那么将变成keys=【1,2,5】。

    private void insert(int key){
        root = insertKeyToNode(key, root);
    }
    // 插入key到叶子节点
    private BtreeNode insertKeyToNode(int key, BtreeNode node){
        int curNodeKeyNums = node.curKeyNums;
        if (curNodeKeyNums == degrees){
        // 如果插入节点的key数量达到了度数,则需要拆分该节点并返回新的父节点
            node = splitNode(node);
        }
        if (node.keys[0]!=null && node.keys[0]==key){
            return node;
        }
        int possibleInsertIdx = binarySearchInsertIdx(key, node);
        // 然后再判断当前的node是不是叶子节点
        if (node.isLeaf()){
        // 如果插入节点的key数量还没有达到度数,直接添加即可
            insertKeyToArray(possibleInsertIdx, key, node);
        }else {
            // 如果不是叶子节点,且节点数是1的话说明分裂成了新节点
            BtreeNode t = insertKeyToNode(key, node.children[possibleInsertIdx]);
            if(t.curKeyNums==1){
            // 需要将当前的t的key以及左右子树都并入到node中
            // 首先需要将node的key移动位置,比如node的keys=[5],此时t的keys[0]为2,那么
            // 插入下标就是0,从0+1开始
                for (int i = node.curKeyNums; i>possibleInsertIdx;i--){
                    node.keys[i] = node.keys[i-1];
                }
                // 合并t的keys[0]
                node.keys[possibleInsertIdx] = t.keys[0];
                // 配合上面的insertKeyToNode(key, node.children[possibleInsertIdx])
                // 更好理解
                for (int i=node.curKeyNums+1;i>possibleInsertIdx+1;i--){
                    node.children[i] = node.children[i-1];
                }
                node.children[possibleInsertIdx] = t.children[0];
                node.children[possibleInsertIdx+1] = t.children[1];
                node.curKeyNums++;
            }
        }
        return node;
    }

 ②keys数组还没有满并且不是叶子节点:

由于按照B树的定义,元素必须先插入到叶子节点中。不过此时需要回顾的一点是,初始化的root根节点是叶子节点,也就是说当root节点的keys数组满了并且经过节点分裂操作,此时的root节点升级为非叶子节点,因为root根节点有了“孩子”。

即无法直接添加到如今的root根节点时,根据此时root.keys数组和待插入key通过二分查找方式找到插入下标possibleInsertIdx,通过它可以知道这个key应该插入到root根节点的哪个子树,左子树还是右子树?

其实这个左右子树的判断光从代码上看挺难分析出来的,还是以具体例子去理解:B树度数为3,此时root.keys=【1,4,7】,准备插入元素5,此时需要将root节点拆分为root.keys=【4】,root.children=【【1】,【7】】,那么此时再添加5的时候,发现插入下标是1,则将5添加到的子树就是root的右子树【7】...最终将变成root.keys=【4】,root.children=【【1】,【5,7】】。

通过上面的例子,知道了如果找到的第一层节点不是叶子节点,则还是先通过二分查找找到待插入key的下标,再次递归调用insertKeyToNode方法,不过传入的插入节点此时是第一层节点的对应子树节点。

③新的分裂节点与旧的分裂节点合并:

由于在递归调用insertKeyToNode方法的时候,有可能又分裂了节点,那么就需要对新分裂的节点t和原来的节点node做合并操作。这个合并操作理解起来并不是那么容易。

如果新节点t的curKeyNums是1则肯定是一个新分裂出来的节点,此时待插入的key已经存入到了t节点,并且注意到BtreeNode t = insertKeyToNode(key, node.children[possibleInsertIdx])这行代码,是将node节点的某个插入子树传入调用了,即当调用栈回溯到这里时,可以认为node的这个子树已经死了、消亡了。记住这个概念对后面做子树合并会有非常大的帮助!

  • 新旧分裂节点的key元素合并,将新分裂节点t的key转移到老分裂节点node的keys中,由于新分裂节点的key只有一个,而且是没递归调用insertKeyToNode方法时就确定的插入下标,因此直接将从插入下标后面开始,将元素移动到后一位,最后再将t.keys[0]赋值给node.keys[possibleInsertIdx]中即完成了新旧分裂节点key元素的合并。
  • 新分裂节点的子树合并到老节点的子树中,子树合并的情况似乎更复杂一点,但是我们要记住的一点时,此时的旧节点其实只剩下一个子树了(只剩下一个子树的说法不严谨,但是由于是递归实现的,以第一次的情况描述好像也无问题,因为从数学归纳法角度看,一次递归和n次递归的效果都是一样的)。由于插入key的下标是possibleInsertIdx,并且node.children[possibleInsertIdx]也已经“死了”,那么新分裂节点的左子树t.children[0]应该被安置在哪里呢?安置在node.children[possibleInsertIdx]对应空间中。因为,t.keys[0]被安置在了node.keys[possibleInsertIdx]中,前面也介绍了小于等于元素下标的子树中都是小于该元素的。同理,新分裂节点的右子树t.children[1]应该分配在下标大于possibleInsertIdx、即possibleInsertIdx+1的老节点的子树数组中。由于最终子树合并占用了possibleInsertIdx、possibleInsertIdx+1两个下标空间,因此需要先在老分裂节点的子树数组中先从possibleInsertIdx+1+1下标开始将子树节点往后挪

总结,插入元素非常烧脑,需要对递归很熟悉才能理解整个流程操作。

 

 

 

 

 

 

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2136903.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深入链表的遍历——快慢指针算法(LeetCode——876题)

今天我们一起来学习一下一个快速遍历链表的方法 我们先来看看一道经典的需要遍历链表的题目 &#xff08;题目来自LeetCode&#xff09; 876. 链表的中间结点https://leetcode.cn/problems/middle-of-the-linked-list/ 给你单链表的头结点 head &#xff0c;请你找出并返回链…

C++多态 学习

目录 一、多态的概念 二、多态的实现 三、纯虚函数和多态类 四、多态的原理 一、多态的概念 多态&#xff1a;多态分为编译时多态(静态多态)和运行时多态(动态多态)。编译时多态主要是我们之前学过的函数重载和函数模板&#xff0c;他们在传不同类型的参数就可以调用不同的函…

diff 命令:文本比较

一、diff 命令简介 ​diff ​命令是一个用于比较两个文件并输出它们之间差异的工具。它是文件比较的基本工具&#xff0c;广泛用于源代码管理、脚本编写和日常的文件维护工作中。 ‍ 二、diff 命令参数 diff [选项] 文件1 文件2选项&#xff1a; ​-b​ 或 --ignore-space…

光伏选址和设计离不开气象分析!

都说光伏选址和设计离不开气象分析&#xff0c;气象条件对太阳能发电影响较大&#xff0c;具体有哪些影响呢&#xff1f;今天我就来讲解下。 - 太阳辐射&#xff1a;太阳辐射的强度是光伏发电的首要因素&#xff0c;对光伏发电有着重要的影响。太阳辐射的强度决定了光伏发电系…

信息安全数学基础(14)欧拉函数

前言 在信息安全数学基础中&#xff0c;欧拉函数&#xff08;Eulers Totient Function&#xff09;是一个非常重要的概念&#xff0c;它与模运算、剩余类、简化剩余系以及密码学中的许多应用紧密相关。欧拉函数用符号 φ(n) 表示&#xff0c;其中 n 是一个正整数。 一、定义 欧…

LVGL学习

注&#xff1a;本文使用的lvgl-release-v8.3版本&#xff0c;其它版本可能稍有不同。 01 LVGL模拟器配置 day01-02_课程介绍_哔哩哔哩_bilibili LVGL开发教程 (yuque.com) 如果按照上述视频和文档中配置不成功的话&#xff0c;直接重装VsCode&#xff0c;我的就是重装以后就…

[Visual Stuidio 2022使用技巧]2.配置及常用快捷键

使用vs2022开发WPF桌面程序时常用配置及快捷键。 语言&#xff1a;C# IDE&#xff1a;Microsoft Visual Studio Community 2022 框架&#xff1a;WPF&#xff0c;.net 8.0 一、配置 1.1 内联提示 未开启时&#xff1a; 开启后&#xff1a; 开启方法&#xff1a; 工具-选…

torch.linspace() torch.arange() torch.stack() 函数详解

1 torch.linspace函数详解 torch.linspace(start, end, steps100, outNone, dtypeNone, layouttorch.strided, deviceNone, requires_gradFalse) → Tensor 函数的作用是&#xff0c;返回一个一维的tensor&#xff08;张量&#xff09;&#xff0c;这个张量包含了从start到end…

【专题】2024新能源企业“出海”系列之驶向中东、东南亚报告合集PDF分享(附原数据表)

原文链接&#xff1a; https://tecdat.cn/?p37698 在“双碳”目标引领下&#xff0c;中国新能源产业近年迅猛发展&#xff0c;新能源企业凭借技术革新、政策支持与市场驱动实现快速增长&#xff0c;在产业链完备、技术领先、生产效能及成本控制等方面优势显著。面对国内外环境…

单向循环链表

文章目录 &#x1f34a;自我介绍&#x1f34a;单向循环链表 你的点赞评论就是对博主最大的鼓励 当然喜欢的小伙伴可以&#xff1a;点赞关注评论收藏&#xff08;一键四连&#xff09;哦~ &#x1f34a;自我介绍 Hello,大家好&#xff0c;我是小珑也要变强&#xff08;也是小珑&…

Linux(3)--CentOS8下载、安装

文章目录 1. CentOS简介2. 下载3. 使用VmWare安装CentOS4. 第一次使用 1. CentOS简介 这个版本我个人比较推荐大家学习&#xff0c;为何&#xff1f;因为容易学习所以不难入门。 2. 下载 可以从国内的开源镜像站下载&#xff0c;这样比较快&#xff0c;例如阿里巴巴开源镜像…

C语言-整数和浮点数在内存中的存储-详解-上

C语言-整数和浮点数在内存中的存储-详解-上 1.前言2.整数2.1无符号整数2.2原码、反码、补码符号位最大值转换过程补码的意义简化算术运算易于转换方便溢出处理 1.前言 在C语言的使用中&#xff0c;需要时刻关注数据的类型&#xff0c;不同类型交替使用可能会发生错误&#xff…

GPT-4-Turbo 和 Claude-3.5-Sonnet 图片识别出答题的是否正确 进行比较

1、比较的图片&#xff1a; 使用GPT-4-Turbo 输入的 提问&#xff1a; 识别图片中的印刷字和手写字&#xff0c;如果写错的给一个正确答案 图片 回复&#xff1a; 在图片中&#xff0c;印刷字显示的是一系列的英语填空练习题&#xff0c;而手写字则是填入空白处的答案。以…

[XILINX] 正点原子ZYNQ7015开发板!ZYNQ 7000系列、双核ARM、PCIe2.0、SFPX2,性能强悍,资料丰富!

正点原子ZYNQ7015开发板&#xff01;ZYNQ 7000系列、双核ARM、PCIe2.0、SFPX2&#xff0c;性能强悍&#xff0c;资料丰富&#xff01; 正点原子Z15 ZYNQ开发板&#xff0c;搭载Xilinx Zynq7000系列芯片&#xff0c;核心板主控芯片的型号是XC7Z015CLG485-2。开发板由核心板&…

【刷题】Day3--错误的集合

hello&#xff01;又见面啦~~~ 一道习题&#xff0c;要长脑子了...... 【. - 力扣&#xff08;LeetCode&#xff09;】 【思路】 /*** Note: The returned array must be malloced, assume caller calls free().*/void Bubble_sort(int arr[], int size) {int temp;for (int i…

Unity 粒子系统参数说明

一、Particle System 1. Duration&#xff08;持续时间&#xff09; 粒子系统运行一次所需的时间。它决定粒子系统持续播放的时间长度。 2. Looping&#xff08;循环播放&#xff09; 如果启用&#xff0c;粒子系统将在播放完一次后自动重新开始播放&#xff0c;直到你停止它…

北斗赋能万物互联:新质生产力的强劲驱动力

在数字化转型的大潮中&#xff0c;中国自主研制的北斗卫星导航系统&#xff0c;作为国家重大空间基础设施&#xff0c;正以前所未有的力量推动着万物互联时代的到来&#xff0c;成为新质生产力发展的重要基石。本文将深入剖析北斗系统如何以其独特的技术优势和广泛应用场景&…

【vue】vue3+ts对接科大讯飞大模型3.5智能AI

如今ai步及生活的方方面面,你是否也想在自己的网站接入ai呢&#xff1f;今天分享科大讯飞大模型3.5智能AI对接。 获取APPID、APISecret、APIKey 讯飞开放平台注册登录控制台创建自己的应用复制备用 准备工作做好,直接开始上代码了。 源码参考 <script setup lang"t…

一步到位:通过 Docker Compose 部署 EFK 进行 Docker 日志采集

一、EFK简介 Elasticsearch&#xff1a;一个开源的分布式搜索和分析引擎&#xff0c;用于存储和查询日志数据。它是 EFK 的核心组件&#xff0c;负责高效地存储和检索日志信息。 Filebeat&#xff1a;一个轻量级的日志采集器&#xff0c;主要用于将日志文件数据发送到 Logsta…

Ubuntu20+Noetic+cartographer_ros编译部署

1 准备工作 &#xff08;1&#xff09;准备Ubuntu20系统。 &#xff08;2&#xff09;安装ROS系统,参考 https://blog.csdn.net/weixin_46123033/article/details/139527141&#xff08;3&#xff09;Cartographer相关软件包和源码下载&#xff1a; https://gitee.com/mrwan…