JavaDS —— B树

news2025/2/23 6:56:10

前言

本章节将带领大家进入B树的学习,主要介绍B树的概念和B树的插入代码的实现,删除代码不做讲解,最后简单介绍B+树和B*树。

B树的概念

1970年,R.Bayer和E.mccreight提出了一种适合外查找的树,它是一种平衡的多叉树,称为B树(有些地方写的是B-树,注意不要误读成"B减树")。

如果B树是一颗三叉平衡树的话,上面一层是关键字区域,下面一层存放的是孩子结点:
在这里插入图片描述

我们来直观感受一下插入的过程:

B树的插入过程

一棵M阶(M>2)的B树,是一棵平衡的M路平衡搜索树,可以是空树或者满足一下性质:

  1. 根节点至少有两个孩子
  2. 每个非根节点至少有 【M/2(向上取整) - 1】 个关键字,至多有M-1个关键字,并且以升序排列
  3. 每个非根节点至少有【M/2(向上取整)】个孩子,至多有M个孩子
  4. key[i]和key[i+1]之间的孩子节点的值介于key[i]、key[i+1]之间
  5. 所有的叶子节点都在同一层

B树的实现

这里实现B树的插入代码。

结点定义

这里以三叉树为演示例子,定义 M 为 3,在结点初始化的时候,我们分别在keys 和 subs 域都增加一个空间,这样会方便我们后续的结点分裂。

    public static final int M = 3;
    public Node root;

    static class Node {
        public int[] keys;//关键字
        public Node[] subs;//孩子结点
        public Node parent;//双亲结点
        public int usedSize;//使用的空间个数

        public Node() {
            //多分配一个空间是为了后面便于我们分裂结点
            this.keys = new int[M];
            this.subs = new Node[M+1];
        }
    }

插入分析

首先如果根节点为空的话,直接插入即可:

//根节点为空,直接插入
        if(root == null) {
            root = new Node();
            root.keys[0] = key;
            root.usedSize = 1;
            return;
        }

然后这里我们实现的B树是不能插入相同的数据的,所以我们需要先查找是否已经存在过 key 值,先写一个查找代码:

当遇到和key 值是一样的情况下,我们直接返回即可,如果没有遇到,我们需要继续查找下去。

结点的 keys 是连续的数组,我们需要遍历这个数组:
如果发现 key 大于数组元素,需要继续向后遍历,如果发现 key 小于数组元素,我们则需要进入到对应的孩子结点继续寻找 key 。

最后我们要考虑返回值,我们应该返回什么样的数据?
如果至少单纯判断是否存在,也就是返回布尔值,如果存在某个数据就是返回true,这时候是不需要进行插入操作的,但是如果不存在,你返回的是 false ,那我们应该从哪个结点进行插入操作,所以我们需要获得具体的结点,这时候就需要在查找的循环过程中保存上一个 cur 结点,当cur 走到空的时候,cur 的上一个结点就是我们需要插入数据的结点了。

但是如果返回结点,那就意味着最后的返回值不可能为空,那就无法判断是否存在了 key,所以我们需要接收两个返回值,这时候我们可以定义一个泛型类,用来创建对象保存两个数据,一个是结点,一个是下标,当不存在的时候直接返回 -1。

public class Pair<K, V> {
    private K key;
    private V val;

    public Pair(K key, V val) {
        this.key = key;
        this.val = val;
    }

    public K getKey() {
        return key;
    }

    public void setKey(K key) {
        this.key = key;
    }

    public V getVal() {
        return val;
    }

    public void setVal(V val) {
        this.val = val;
    }
}

    //查找
    public Pair<Node,Integer> find(int key) {
        Node cur = root;
        Node prev = null;
        while(cur != null) {
            int i = 0;
            while(i < cur.usedSize) {
                if(cur.keys[i] == key) {
                    //存在该节点
                    return new Pair<>(cur,i);
                } else if(cur.keys[i] > key) {
                    //需要进入孩子结点继续查找
                    break;
                } else {
                    //继续查找
                    i++;
                }
            }
            prev = cur;
            cur = cur.subs[i];
        }

        //找不到,返回双亲结点
        return new Pair<>(prev,-1);
    }

如果不存在,我们就需要插入key ,在获取到的 prev 上进行直接插入,最后我们就要考虑是否超过了M,如果超过了M,就需要进行结点的分裂:

这里要注意的是,我们插入过程都是在叶子结点上进行的,所以不需要进行孩子域 subs 的调整。

        //不存在,需要进行插入
        Node cur = find.getKey();
        //插入是在叶子结点进行的,不需要调整孩子结点
        int i = cur.usedSize - 1;
        for (; i >= 0; i--) {
            if(cur.keys[i] > key) {
                cur.keys[i+1] = cur.keys[i];
            } else {
                break;
            }
        }
        cur.keys[i+1] = key;
        cur.usedSize++;

        //是否需要进行分裂
        if(cur.usedSize == M) {
            split(cur);
        }

分裂分析

我们来看一下非根结点的分裂过程:
在这里插入图片描述
我们需要获取中间的关键字,然后从中间的关键字的下一个开始拷贝到新结点上,然后中间的关键字需要提取到上面去,也就是需要调整 双亲结点将 中间值插入进去,最后调整三个结点即可。

由于你往双亲结点上插入了一个数据,所以可能导致双亲结点超过容量,所以最后还需要查看双亲结点是否需要进行分裂

		Node newNode = new Node();
        Node parent = cur.parent;

        //进行keys和孩子结点的拷贝
        int mid = M / 2;
        int i = 0;
        int j = mid + 1;
        for(; j < cur.usedSize; j++) {
            newNode.keys[i] = cur.keys[j];
            newNode.subs[i] = cur.subs[j];
            //如果拷贝过来的孩子结点不为空,需要修改孩子结点的双亲结点
            if(newNode.subs[i] != null) {
                newNode.subs[i].parent = newNode;
            }
            //usedSize 随之修改
            newNode.usedSize++;
            i++;
        }

        //还差一个孩子结点没有拷贝,再次拷贝孩子结点
        newNode.subs[i] = cur.subs[j];
        if(newNode.subs[i] != null) {
            newNode.subs[i].parent = newNode;
        }

        //新结点的双亲结点设置为 parent
        newNode.parent = parent;

        //设置 cur 的 usedSize 数值
        cur.usedSize = mid;

        //需要提取的中间关键字
        int midVal = cur.keys[mid];

        //特殊情况:当分裂的结点正好是根结点
        if(cur == root) {
            root = new Node();
            root.keys[0] = midVal;
            root.subs[0] = cur;
            root.subs[1] = newNode;
            cur.parent = newNode.parent = root;
            root.usedSize = 1;
            return;
        }

        //处理 parent 结点
        //将 cur 的中间关键值提到 parent;
        int end = parent.usedSize - 1;
        for(; end >= 0; end--) {
            if(parent.keys[end] > midVal) {
                parent.keys[end+1] = parent.keys[end];
                parent.subs[end+2] = parent.subs[end+1];
            } else {
                break;
            }
        }
        parent.keys[end+1] = midVal;
        parent.subs[end+2] = newNode;
        parent.usedSize++;

        //是否需要继续分裂
        if(parent.usedSize == M) {
            split(parent);
        }

如果分裂的是根节点的话,就有一点不一样了:我们需要为中间值创建一个新结点作为新的 根节点
在这里插入图片描述
在这里插入图片描述

		//特殊情况:当分裂的结点正好是根结点
        if(cur == root) {
            root = new Node();
            root.keys[0] = midVal;
            root.subs[0] = cur;
            root.subs[1] = newNode;
            cur.parent = newNode.parent = root;
            root.usedSize = 1;
            return;
        }

根节点的插入和非根结点的插入区别就在于中间值的处理,所以在前面拷贝的过程的代码可以保留,最后进行特殊情况的判断处理即可。

    private void split(Node cur) {
        Node newNode = new Node();
        Node parent = cur.parent;

        //进行keys和孩子结点的拷贝
        int mid = M / 2;
        int i = 0;
        int j = mid + 1;
        for(; j < cur.usedSize; j++) {
            newNode.keys[i] = cur.keys[j];
            newNode.subs[i] = cur.subs[j];
            //如果拷贝过来的孩子结点不为空,需要修改孩子结点的双亲结点
            if(newNode.subs[i] != null) {
                newNode.subs[i].parent = newNode;
            }
            //usedSize 随之修改
            newNode.usedSize++;
            i++;
        }

        //还差一个孩子结点没有拷贝,再次拷贝孩子结点
        newNode.subs[i] = cur.subs[j];
        if(newNode.subs[i] != null) {
            newNode.subs[i].parent = newNode;
        }

        //新结点的双亲结点设置为 parent
        newNode.parent = parent;

        //设置 cur 的 usedSize 数值
        cur.usedSize = mid;

        //需要提取的中间关键字
        int midVal = cur.keys[mid];

        //特殊情况:当分裂的结点正好是根结点
        if(cur == root) {
            root = new Node();
            root.keys[0] = midVal;
            root.subs[0] = cur;
            root.subs[1] = newNode;
            cur.parent = newNode.parent = root;
            root.usedSize = 1;
            return;
        }

        //处理 parent 结点
        //将 cur 的中间关键值提到 parent;
        int end = parent.usedSize - 1;
        for(; end >= 0; end--) {
            if(parent.keys[end] > midVal) {
                parent.keys[end+1] = parent.keys[end];
                parent.subs[end+2] = parent.subs[end+1];
            } else {
                break;
            }
        }
        parent.keys[end+1] = midVal;
        parent.subs[end+2] = newNode;
        parent.usedSize++;

        //是否需要继续分裂
        if(parent.usedSize == M) {
            split(parent);
        }
    }

最终代码

package mybtree;

public class Btree {

    public static final int M = 3;
    public Node root;

    static class Node {
        public int[] keys;//关键字
        public Node[] subs;//孩子结点
        public Node parent;//双亲结点
        public int usedSize;//使用的空间个数

        public Node() {
            //多分配一个空间是为了后面便于我们分裂结点
            this.keys = new int[M];
            this.subs = new Node[M+1];
        }
    }

    //插入
    public void insert(int key) {
        //根节点为空,直接插入
        if(root == null) {
            root = new Node();
            root.keys[0] = key;
            root.usedSize = 1;
            return;
        }

        //先查找是否存在key
        Pair<Node,Integer> find = find(key);

        //如果已经存在,直接返回
        if(find.getVal() != -1) {
            return;
        }

        //不存在,需要进行插入
        Node cur = find.getKey();
        //插入是在叶子结点进行的,不需要调整孩子结点
        int i = cur.usedSize - 1;
        for (; i >= 0; i--) {
            if(cur.keys[i] > key) {
                cur.keys[i+1] = cur.keys[i];
            } else {
                break;
            }
        }
        cur.keys[i+1] = key;
        cur.usedSize++;

        //是否需要进行分裂
        if(cur.usedSize == M) {
            split(cur);
        }
    }

    private void split(Node cur) {
        Node newNode = new Node();
        Node parent = cur.parent;

        //进行keys和孩子结点的拷贝
        int mid = M / 2;
        int i = 0;
        int j = mid + 1;
        for(; j < cur.usedSize; j++) {
            newNode.keys[i] = cur.keys[j];
            newNode.subs[i] = cur.subs[j];
            //如果拷贝过来的孩子结点不为空,需要修改孩子结点的双亲结点
            if(newNode.subs[i] != null) {
                newNode.subs[i].parent = newNode;
            }
            //usedSize 随之修改
            newNode.usedSize++;
            i++;
        }

        //还差一个孩子结点没有拷贝,再次拷贝孩子结点
        newNode.subs[i] = cur.subs[j];
        if(newNode.subs[i] != null) {
            newNode.subs[i].parent = newNode;
        }

        //新结点的双亲结点设置为 parent
        newNode.parent = parent;

        //设置 cur 的 usedSize 数值
        cur.usedSize = mid;

        //需要提取的中间关键字
        int midVal = cur.keys[mid];

        //特殊情况:当分裂的结点正好是根结点
        if(cur == root) {
            root = new Node();
            root.keys[0] = midVal;
            root.subs[0] = cur;
            root.subs[1] = newNode;
            cur.parent = newNode.parent = root;
            root.usedSize = 1;
            return;
        }

        //处理 parent 结点
        //将 cur 的中间关键值提到 parent;
        int end = parent.usedSize - 1;
        for(; end >= 0; end--) {
            if(parent.keys[end] > midVal) {
                parent.keys[end+1] = parent.keys[end];
                parent.subs[end+2] = parent.subs[end+1];
            } else {
                break;
            }
        }
        parent.keys[end+1] = midVal;
        parent.subs[end+2] = newNode;
        parent.usedSize++;

        //是否需要继续分裂
        if(parent.usedSize == M) {
            split(parent);
        }
    }

    //查找
    public Pair<Node,Integer> find(int key) {
        Node cur = root;
        Node prev = null;
        while(cur != null) {
            int i = 0;
            while(i < cur.usedSize) {
                if(cur.keys[i] == key) {
                    //存在该节点
                    return new Pair<>(cur,i);
                } else if(cur.keys[i] > key) {
                    //需要进入孩子结点继续查找
                    break;
                } else {
                    //继续查找
                    i++;
                }
            }
            prev = cur;
            cur = cur.subs[i];
        }

        //找不到,返回双亲结点
        return new Pair<>(prev,-1);
    }

    public void inorder(Node root){
        if(root == null)
            return;
        for(int i = 0; i < root.usedSize; ++i){
            inorder(root.subs[i]);
            System.out.println(root.keys[i]);
        }
        inorder(root.subs[root.usedSize]);
    }
}

B+树介绍

B+树是B-树的变形,也是一种多路搜索树:
其定义基本与B-树相同,除了:

  1. 非叶子节点的子树指针与关键字个数相同
  2. 非叶子节点的子树指针p[i],指向关键字值属于【k[i],k[i+1]】的子树【这句话的意思是B+树在B树的基础上只存在右子树,也就是说keys 数组第一个区域是不存在左孩子的,然后每一个孩子结点的范围是 k[i] 到 k[i+1] 之间的】
  3. 所有叶子节点通过双向链表进行连接
  4. 所有关键字都在叶子节点出现在这里插入图片描述

B+树的应用:
在MySQL中使用B+树来对数据进行管理,在下一篇MySQL的索引中我会进行详细的讲解。

B* 树介绍

B*树是B+树的变形,在B+树的非根和非叶子节点再增加指向兄弟节点的指针。
在这里插入图片描述
这样做的好处是可以节约存储空间,结点在进行分裂的时候,会优先先看看兄弟结点是否已满,如果没有满,会将数值插入到兄弟结点上。

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

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

相关文章

裸土覆盖检测算法、裸土识别检测算法、裸土检测算法

裸土检测算法是一种基于计算机视觉和图像处理技术&#xff0c;用于自动识别和评估裸露土壤区域的智能工具。以下是对裸土检测算法的详细介绍&#xff1a; 1. 技术原理 - 数据采集与预处理&#xff1a;利用卫星、无人机或传感器收集地面图像数据&#xff0c;并进行必要的预处理…

练完这些项目,你就是大模型大师!

去年侧重大模型和GPT的原理&#xff0c;今年就侧重项目实战了。找到的这个合集不仅收集了大模型训练实战&#xff0c;还有微调实战&#xff0c;分布式训练&#xff0c;真的很全。 github项目名llm-action 本项目旨在分享大模型相关技术原理以及实战经验。 这份《大模型项目集》…

【Python爬虫】学习Python必学爬虫,这篇文章带您了解Python爬虫,Python爬虫详解!!!

Python爬虫详解 Python爬虫是一种用于从网站获取数据的自动化脚本。它使用Python编程语言编写&#xff0c;并利用各种库和模块来实现其功能。以下是Python爬虫的详细讲解&#xff0c;包括基本概念、常用库、基本流程和示例代码。 基本概念 HTTP请求&#xff1a;爬虫通过向目…

Docker部署及基本操作

Docker是一个开源的平台 &#xff0c;用于开发、交付和运行应用程序。它能够在Windows&#xff0c;macOS&#xff0c;Linux计算机上运行&#xff0c;并将某一应用程序及其依赖项打包至一个容器中&#xff0c;这些容器可以在任何支持Docker的环境中运行。容器彼此隔离&#xff0…

rpm方式安装Mysql报错依赖冲突解决

使用rpm安装mysql时在安装到client包时报错依赖库冲突以及GPG密钥问题&#xff0c; 解决 1&#xff0c;下载 MySQL 的 YUM 存储库文件。 wget https://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm 2&#xff0c;安装下载的 YUM 存储库文件。 sudo rpm -…

Promise.all解决同时请求同一接口,返回值渲染时间问题

语法&#xff1a;Promise.all(iterable); 参数&#xff1a;iterable 一个可迭代对象&#xff0c;如 Array 或 String。 返回值&#xff1a;如果传入的参数是一个空的可迭代对象&#xff0c;则返回一个已完成&#xff08;already resolved&#xff09;状态的 Promise。 如果传入…

万能小程序运营管理系统 _requestPost 任意文件读取漏洞复现

0x01 产品简介 万能小程序运营管理系统是一种功能全面的系统,旨在帮助开发者和运营人员更好地管理和推广小程序。该系统集成了多种功能模块,覆盖了从小程序开发、部署到运营管理的全链条服务。系统通过提供丰富的功能和工具,帮助用户轻松搭建、管理和优化小程序。该系统支持…

提高数据集成稳定性:EMQX Platform 端到端规则调试指南

自 5.7.0 版本起&#xff0c;EMQX 支持了 SQL 调试&#xff0c;并支持在数据集成全流程中进行规则调试&#xff0c;使用户能够在开发阶段就全面验证和优化规则&#xff0c;确保它们在生产环境中的稳定高效运行。 点击此处下载 EMQX 最新版本&#xff1a;https://www.emqx.com/z…

JavaSE - 面向对象编程01

01 什么是面向对象编程(oop) 答&#xff1a;就是只关心对象之间的交互&#xff0c;而并不关心任务是怎样具体完成的。例如把一个大象放进冰箱需要几步&#xff1f;如果是面向对象编程只会思考冰箱和大象之间的交互&#xff0c;那么给出的答案就是&#xff1a;把冰箱门打开&…

PyCharm用法

一、汉化 要将‌PyCharm设置为中文&#xff0c;可以通过以下两种方法&#xff1a;‌ 通过内置插件市场安装中文语言包 1.打开PyCharm&#xff0c;点击File -> Settings&#xff08;在Mac上是PyCharm -> Preferences&#xff09;。 2.在设置界面中选择Plugins&#x…

[Linux]进程控制详解

1.创建进程 进程调用fork,当控制转移到内核中的fork代码后&#xff0c;内核做&#xff1a; ● 分配新的内存块和内核数据结构给子进程 ● 将父进程部分数据结构内容拷贝至子进程 ● 添加子进程到系统进程列表当中 ● fork返回&#xff0c;开始调度器调度 这个前面提到过&#…

智能玩具用离线语音识别芯片有什么优势

随着科技的发展&#xff0c;很多智能电子产品和儿童玩具实现了与人类的交互&#xff0c;语音芯片在这些人机交互中起到了不可替代的作用&#xff0c;语音识别芯片在智能玩具中的应用就为其带来了更多的优势。‌ 离线语音识别&#xff0c;即小词汇量、低成本的语音识别系统&…

esp32 wifi 联网后,用http 发送hello 用pc 浏览器查看网页

参考chatgpt Esp32可以配置为http服务器&#xff0c;可以socket编程。为了免除编写针对各种操作系统的app。完全可以用浏览器仿问esp32服务器&#xff0c;获取esp32的各种数据&#xff0c;甚至esp的音频&#xff0c;视频。也可以利用浏览器对esp进行各种操作。但esp不能主动仿…

【深度学习】(2)--PyTorch框架认识

文章目录 PyTorch框架认识1. Tensor张量定义与特性创建方式 2. 下载数据集下载测试展现下载内容 3. 创建DataLoader&#xff08;数据加载器&#xff09;4. 选择处理器5. 神经网络模型构建模型 6. 训练数据训练集数据测试集数据 7. 提高模型学习率 总结 PyTorch框架认识 PyTorc…

【STL】vector 基础,应用与操作

vector 是 C 标准库中最常用的顺序容器之一&#xff0c;提供了动态数组的功能。与普通数组相比&#xff0c;vector 能够根据需求自动扩展或收缩&#xff0c;为程序员提供了更灵活的数据存储方案。本文将详细介绍 vector 的相关操作&#xff0c;并结合实例代码帮助读者深入理解。…

MindShare PCIE 3.0 笔记-第一二章

MindShare 官网&#xff0c;地址如下: MindShare Chapter 1&#xff1a;PCIE 背景介绍 - PCI 总线模型 1. 以 PCI 总线作为外设总线的 SOC 芯片架构 下图展示了一个以 PCI 总线作为外设总线的 SOC 芯片架构(PCI 总线类似 AXI 下的 AHB&#xff1f;)&#xff1a; 由上图可知…

Django Auth组件

文章目录 前言一、使用场景二、使用步骤1.验证用户( authenticate() 方法)2.注册用户3.退出登陆4.装饰器 前言 Django 的用户认证组件基于以下几个核心概念&#xff1a; 1.用户认证&#xff1a;处理用户的登录、注销和密码管理&#xff0c;提供了一个User模型和相关的视图、表…

技术美术一百问(02)

问题 前向渲染和延迟渲染的流程 前向渲染和延迟渲染的区别 G-Buffer是什么 前向渲染和延迟渲染各自擅长的方向总结 GPU pipeline是怎么样的 Tessellation的三个阶段 什么是图形渲染API? 常见的图形渲染API有哪些&#xff1f; 答案 1.前向渲染和延迟渲染的流程 【例图…

图神经网络池化方法

图神经网络池化方法 提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 图神经网络池化方法前言一、扁平图池化二、分层图池化1.节点聚类池化2.节点丢弃池化 参考文献 前言 图池化操作根据其池化策略的差异&#xff…

软考(中级-软件设计师)(0919)

软考 一、软件设计师-历年考试考点分布情况-上午-计算机与软件工程知识 知识点分数说明比例软件工程基础知识11开发模型、设计原则、测试方法、质量特性、CMM、Pert图、风险管理14.67%面向对象12面向对象基本概念、面向对象分析与设计、UML、常见算法16.00%数据结构与算法10…