平衡搜索树——B-树小记

news2024/7/4 23:44:40

文章目录

  • B- 树系列
    • 定义
    • 插入规则
    • 代码
      • B-树结点定义
      • 查找key在结点哪个子树
      • 插入+分裂

B- 树系列

定义

B-树是一棵多叉 平衡 搜索树(不是二叉树,B-树中每个结点中可以有多个key,也有多个孩子)

B-树中每个结点在实现时人为规定一个key的上限(KEY_LIMIT = 4)
B-树结点中的key是按照key的顺序进行保存
我们把key的个数记为size,则child的个数—定是size + 1

在这里插入图片描述

插入规则

1.所有插入过程,只能发生在叶子上,不能发生在非叶子结点上
2.插入时,每个结点的kev的数量是有上限的
(1) 未达上限,插入结束
(2) 达到上限,则进行“结点分裂”过程
3.叶子结点的分裂
如下情况中,插入25后,结点中key的数量超过上限4,引发分裂,分裂过程如下:
在这里插入图片描述

步骤:

1. 找到key的中间值(25)把下标记为index

1)不一定就是新插入的结点
2)偶数的话,前一个或者后一个都可以(一般都实现成奇数的形式)

2. 把当前结点分成两个结点(分家)

[0 , index) key留在当前结点中
[index]提到父结点中
(index, size)搬到新结点中

在这里插入图片描述

代码

B-树结点定义

/**
 * 暂时只考虑:
 * 1. key 模型而不是 key-value 模型
 * 2. key 类型定义为 long 类型
 * 3. public
 */
public class BTreeNode {
    /**
     * 先规定,一个 B-树结点中,能存的 key 的数量的上限,要求 size(key) <= KEY_LIMIT
     * 在实际的应用中,一般 KEY_LIMIT 都是 1000 数量级以上的
     */
    public static final int KEY_LIMIT = 4;


    /**
     * 去保存结点中所有的 key
     * 使用数组(线性结构)进行维护,需要维护 key 与 key 之间的有序性
     * 从空间利用率角度考虑,new long[KEY_LIMIT] 就足够了
     * 但我们多申请一个空间,方便进行结点分裂代码的编写(用于保存即将分裂状态下的所有 key)
     */
    public long[] keyList = new long[KEY_LIMIT + 1];

    /**
     * 记录目前拥有的 key 的个数
     */
    public int size = 0;

    /**
     * 去维护结点中所有的 child
     * 同理,多申请一个空间
     * 本来,child 就比 key 多一个
     * new BTreeNode[KEY_LIMIT + 2]
     */
    public BTreeNode[] childList = new BTreeNode[KEY_LIMIT + 2];
    /**
     * 我们不维护 child 的个数,因为 其个数一定是 size + 1
     */

    /**
     * 保存该结点的父结点,null 代表该结点是根结点
     */
    public BTreeNode parent = null;

    @Override
    public String toString() {
        return String.format("%d: %s", size, Arrays.toString(Arrays.copyOf(keyList, size)));
    }
}

在这里插入图片描述

查找key在结点哪个子树

public BTreeNode findKey(long key) {
        if (key > keyList[size - 1]) {
            // child 比 key 多一个
            // keyList 的最后一个元素位于 [size - 1]
            // childList 的最后一个元素位于 [size]
            return childList[size];
        }

        for (int i = 0; i < size; i++) {
            if (key == keyList[i]) {
                // key 就在当前结点中
                return this;
            } else if (key < keyList[i]) {
                // 由于我们是从左向右遍历的
                // 所以 keyList[i] 中的元素是 "第一个" 大于 key 的关键字元素
                return childList[i];
            } else {
                // key > keyList[i] 的情况下,我们什么都不需要做,继续遍历查找
                // 第一个 大于 key 的元素
            }
        }

        // 我们的代码组织方式使得,理论上,不应该走到这个位置
        // 但 java 编译器分析不出来,所以,为了让编译器不报错,我们随便返回一个值
        // 这个 return 没有任何执行的意义
        return null;
  }

测试查找Key的代码
包含测试用例分析

public static void testFindKey() {
        BTreeNode node = new BTreeNode();
        node.keyList[0] = 10;
        node.keyList[1] = 20;
        node.keyList[2] = 30;
        node.keyList[3] = 40;
        node.size = 4;

        BTreeNode= new BTreeNode();
        BTreeNode= new BTreeNode();
        BTreeNode= new BTreeNode();
        BTreeNode= new BTreeNode();
        BTreeNode= new BTreeNode();
        node.childList[0] =;
        node.childList[1] =;
        node.childList[2] =;
        node.childList[3] =;
        node.childList[4] =;

        /**
         * 测试用例:
         * 执行动作           期望结果
         * < 10              期望   甲
         * == 10             期望   node
         * == 20             期望   node
         * (10, 20)          期望   乙
         * (20, 30)          期望   丙
         * (30, 40)          期望   丁
         * > 40              期望   戊
         * == 40             期望   node
         */
        // 引用1 == 引用2: 判断两个引用是否指向同一个对象
        System.out.println(node.findKey(3) ==);
        System.out.println(node.findKey(10) == node);
        System.out.println(node.findKey(20) == node);
        System.out.println(node.findKey(13) ==);
        System.out.println(node.findKey(29) ==);
        System.out.println(node.findKey(31) ==);
        System.out.println(node.findKey(300) ==);
        System.out.println(node.findKey(40) == node);
    }

插入+分裂

插入步骤:

将key插入结点的keyList中,并且维持有序性 ——类似插入排序的做法
在这里插入图片描述

/**
     * 用来作为插入的返回值类型的类
     */
    public static class InsertResult {
        public long key;        // 分裂出的 key
        public BTreeNode node;  // 分裂出的新结点
    }

    /**
     * 将指定关键字 key,插入叶子结点中
     * @param key 关键字
     * @return
     * null: 插入之后不需要分裂
     * !null: 发生了分裂
     */
    public InsertResult insertKey(long key) {
        // 1. 如果 key 按序插入到 keyList 中
        insertIntoKeyList(key);
        size++;
        // 2. 判断是否需要进行分裂
        // 3. 如果有必要,进行结点分裂
        if (shouldSplit()) {
            return splitLeaf();
        }

        return null;
    }

    /**
     * 将指定关键字 key 和 chid,插入非叶子结点中
     * @param key 关键字
     * @param child > key 的孩子
     * @return
     */
    public InsertResult insertKeyWithChild(long key, BTreeNode child) {
        // 1. 如果 key 按序插入到 keyList 中
        int index = insertIntoKeyList(key);
        // 2. 根据 index 进行 child 的插入,暂时没有进行 size 的变动
        // child[0, size]
        // [0, index] 不动
        // [index + 1] 要插入 child 的下标
        // [index + 1, size] 搬到 [index + 2, size + 1]  元素个数 size - (index + 1) + 1 == size - index
        System.arraycopy(childList, index + 1, childList, index + 2, size - index);
        childList[index + 1] = child;
        // 这个时候再让 size++
        size++;

        // 2. 判断是否需要进行分裂
        // 3. 如果有必要,进行结点分裂
        if (shouldSplit()) {
            return splitNotLeaf();
        }

        return null;
    }

    public InsertResult splitNotLeaf() {
        // 因为一会儿 this.size 在中途会变化,先提前记录
        int size = this.size;

        InsertResult result = new InsertResult();
        BTreeNode node = new BTreeNode();

        // 1. 找到 keyList 的中间位置
        int index = size / 2;

        // 2. 保存需要分裂出的 key
        result.key = keyList[index];

        /**
         * 3. 处理 key 的分裂
         * 哪些 key 应该留在当前结点中      [0, index)    一共有 index 个
         * 哪个 key 是分裂出的 key         [index]
         * 哪些 key 应该在新结点中         (index, size)  一共有 size - index - 1
         */
        // 先把 (index, size) 位置的 key 搬到新结点中
        System.arraycopy(keyList, index + 1, node.keyList, 0, size - index - 1);
        node.size = size - index - 1;

        // 把 [index, size) 所有 key 重置为默认值 (0),重置 size
        Arrays.fill(keyList, index, size, 0);
        this.size = index;

        /**
         * 4. 我们对非叶子结点进行分裂,非叶子结点有孩子,所以也进行分裂
         */
        System.arraycopy(this.childList, index + 1, node.childList, 0, size - index);
        Arrays.fill(this.childList, index + 1, size + 1, null);
        for (int i = 0; i < size - index; i++) {
            node.childList[i].parent = node;
        }

        /**
         * 5. 处理分裂的 parent 的问题
         * 1) this.parent 不变,分裂不影响父子关系
         * 2) node.parent 和 this 是同一个父亲
         */
        node.parent = this.parent;

        result.node = node;

        return result;
    }

    public InsertResult splitLeaf() {
        // 因为一会儿 this.size 在中途会变化,先提前记录
        int size = this.size;

        InsertResult result = new InsertResult();
        BTreeNode node = new BTreeNode();

        // 1. 找到 keyList 的中间位置
        int index = size / 2;

        // 2. 保存需要分裂出的 key
        result.key = keyList[index];

        /**
         * 3. 处理 key 的分裂
         * 哪些 key 应该留在当前结点中      [0, index)    一共有 index 个
         * 哪个 key 是分裂出的 key         [index]
         * 哪些 key 应该在新结点中         (index, size)  一共有 size - index - 1
         */
        // 先把 (index, size) 位置的 key 搬到新结点中
        System.arraycopy(keyList, index + 1, node.keyList, 0, size - index - 1);
        node.size = size - index - 1;

        // 把 [index, size) 所有 key 重置为默认值 (0),重置 size
        Arrays.fill(keyList, index, size, 0);
        this.size = index;

        /**
         * 4. 我们对叶子结点进行分裂,叶子结点没有孩子 childList[...] = null,不需要分裂
         */

        /**
         * 5. 处理分裂的 parent 的问题
         * 1) this.parent 不变,分裂不影响父子关系
         * 2) node.parent 和 this 是同一个父亲
         */
        node.parent = this.parent;

        result.node = node;

        return result;
    }

    public boolean shouldSplit() {
        return size > KEY_LIMIT;
    }

    public int insertIntoKeyList(long key) {
        // keyList 中有效的下标范围是 [0, size)
        // 从后向前遍历 [size - 1, 0]
        int i;
        for (i = size - 1; i >= 0; i--) {
            if (keyList[i] < key) {
                // 第一次碰到了 keyList[i] < key
                break;
            }

            // 没有 keyList[i] == key
            // 继续遍历查找,同时将数据向后搬一格
            keyList[i + 1] = keyList[i];
        }

        // 1. i == -1
        // 2. keyList[i] < key
        // 不管怎么样,key 都应该放在 [i + 1]
        keyList[i + 1] = key;

        return i + 1;
    }
import btree.BTreeNode.InsertResult;

/**
 * B-树定义
 */
public class BTree {
    /**
     * B-树的根结点
     */
    public BTreeNode root = null;

    /**
     * B-树中的 key 的个数
     * 备注:和二叉搜索树不同,B-树的 key 的个数 不等于 结点的个数
     */
    public int size = 0;

    public boolean insert(long key) {
        if (insertWithoutSize(key)) {
            size++;
            return true;
        }

        return false;
    }

    public boolean insertWithoutSize(long key) {
        // 1. 是否是空树
        if (root == null) {
            // root = 构建一个包含 key 的结点;
            root = new BTreeNode();
            root.keyList[0] = key;
            root.size = 1;
            return true;
        }

        // 2. 不是空树的情况
        BTreeNode current = root;
        while (true) {
            BTreeNode node = current.findKey(key);

            if (node == current) {
                // key 就在 current 结点中
                // 不运行重复 key 的插入,插入失败
                return false;
            } else if (node == null) {
                // current 就是叶子结点
                // current 就是接下来要插入 key 的结点
                break;
            } else {
                // 找到了一个孩子 并且 current 不是叶子
                // 规则:插入必须发生在叶子结点上
                current = node;
            }
        }

        // 进行 key 的插入
        // current 就是我们要进行 key 插入的结点
        // current 此时一定是叶子
        InsertResult r = current.insertKey(key);

        while (true) {
            if (r == null) {
                // 插入过程中,没有发生结点分裂,直接退出
                return true;
            }

            // 说明发生分裂了
            // 需要把新分裂出的 key 和 分裂出的结点,插入到 current.parent 中
            BTreeNode parent = current.parent;
            if (parent == null) {
                // 说明 current 是根结点
                // 根结点发生分裂了
                // 需要一个新的根结点,来保存分裂出的 key
                root = new BTreeNode();
                root.keyList[0] = r.key;
                root.size = 1;
                root.childList[0] = current;
                root.childList[1] = r.node;

                // 由于原来 current 是根结点,所以其 parent == null
                // 自然分裂出的结点,跟着也是 null
                // 所以为他们设置新的父结点
                current.parent = r.node.parent = root;
                return true;
            }

            // 不是根结点分裂了
            r = parent.insertKeyWithChild(r.key, r.node);
            current = parent;
        }
    }
}

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

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

相关文章

Mysql语法五:idea连接数据库和jdbc

目录 1.连接数据库 1.1.下载驱动包 1.2&#xff1a;连接数据库 2.jdbc编程 2.1&#xff1a;何为jdbc 2.2&#xff1a;jdbc的使用。 2.2.1&#xff1a;直接插入操作 2.2.2&#xff1a;利用&#xff1f;进行插入 2.2.3&#xff1a;修改操作 2.2.4&#xff1a;删除操作 …

算法基础:动态规划

目录 动态规划之禅 多种纬度解决Fibonacci 数列 什么是Fibonacci数列 朴素递归方案 朴素递归的问题 Fib自上而下、备忘录方案 Fib自下而上法 动态规划之禅 动态规划是算法基础部分中最有趣的一个了&#xff0c;我想了很多天&#xff0c;怎么用很短的一些话把动态规划像之…

onnx删除无用属性

这里写自定义目录标题在推理onnx模型时&#xff0c;报了一个错&#xff0c;如下&#xff1a;InvalidGraph: [ONNXRuntimeError] : 10 : INVALID_GRAPH : This is an invalid model. In Node, ("Conv_0", Conv, "", -1) : ("x": tensor(float),&q…

DPVS时间轮移植

DPDK自带的定时器采用跳表实现&#xff0c;时间复杂度是O(logn)&#xff0c;当有大量事件要定时触发时&#xff0c;比如会话session老化&#xff0c;效率并不高。因此DPVS采用了O(1)复杂度的时间轮。0. 概述 a. 添加定时器事件的核心是 static int __dpvs_timer_sched(struct…

为何 SPARK 在应用 GPU 后表现更出色

什么是 APACHE SPARK&#xff1f; 伴随数据的巨量增长&#xff0c;Apache Spark 已成为分布式横向扩展数据处理的热门框架之一&#xff0c;可以在本地和云端数以百万计的服务器上运行。 Apache Spark 是应用于大型数据处理的快速通用分析引擎&#xff0c;可在 YARN、Apache Mes…

程序员还在为变量取名苦恼,那是因为你不知道,这个变量命名神器

作为程序员&#xff0c;变量命名应该是我们编程的开端&#xff0c;也是我们每天都必须需要做的事情。变量命名规范的重要性&#xff0c;相信大家都知道非常重要&#xff0c;良好的代码风格&#xff0c;带来好处有&#xff1a; 1、具有良好的可读性&#xff1b; 2、维护代码时…

【独立篇】React UI组件库

文章目录1、React UI组件库1.1、material-ui&#xff08;国外&#xff09;1.2、ant-design&#xff08;国内蚂蚁金服-antd&#xff09;2、AntD的简单使用2.1、CODE2.2、Result1、React UI组件库 1.1、material-ui&#xff08;国外&#xff09; 官网: http://www.material-ui.…

硬件定义软件?还是,软件定义硬件?

文章目录**1 软件和硬件****1.1 软件和硬件的定义****1.2 “硬件定义软件”和“软件定义硬件”的定义****1.3 CPU&#xff0c;软件和硬件解耦****1.4 CPU的软硬件定义****2 硬件定义软件****2.1 系统从软件逐步到硬件****2.2 硬件架构决定了软件设计****2.2.1 ASIC的硬件定义**…

【车辆配送】基于模拟退火 (SA)求解车辆配送 (VPR) (Matlab代码实现)

目录 1 车辆配送问题 2 模拟退火法 3 实现结果 4 参考文献 5 Matlab代码实现 1 车辆配送问题 式(9)~( 12)中, 为配送车辆到达需求点i的时间;为需求点i到需求点j的运输成本;、分别为配送车辆提前到达需求点i的或者滞后到达需求点i的单位时间内的等待成本以及惩罚成本。该数…

基于vue项目的代码优化

前言 项目上线后其整体性能的优良是用户也是研发人员所关注的。项目优化非常重要&#xff0c;一丝一毫的提升都是对用户的负责。因此我们在开发中就应该注重细节&#xff0c;优化工作从日常开发做起。本篇文章就分享一些在日常开发中代码层面的优化手段。 开发常用优化手段 …

04-Docker-容器数据卷

目录 一、坑&#xff01;&#xff01;&#xff01;&#xff01; 二、什么是容器卷 三、容器卷的作用 四、容器卷案例 1、宿主vs容器之间映射添加容器卷 五、容器卷ro和rw规则 一、坑&#xff01;&#xff01;&#xff01;&#xff01; 容器卷记得加入 --privilegedtrue …

2022-11-30 Github Forking 工作流模式

Forking 工作流 fork 操作是在个人远程仓库新建一份目标远程仓库的副本&#xff0c;流程如下&#xff1a; 比如在 GitHub 上操作时&#xff0c;在项目的主页点击 fork 按钮&#xff08;页面右上角&#xff09;&#xff0c;即可拷贝该目标远程仓库。 假设开发者 A 拥有一个远程仓…

HTML网页设计制作大作业(游戏主题)---电竞

⛵ 源码获取 文末联系 ✈ Web前端开发技术 描述 网页设计题材&#xff0c;DIVCSS 布局制作,HTMLCSS网页设计期末课程大作业 | 游戏官网 | 游戏网站 | 电竞游戏 | 游戏介绍 | 等网站的设计与制作 | HTML期末大学生网页设计作业&#xff0c;Web大学生网页 HTML&#xff1a;结构 …

《MongoDB》Mongo Shell中的基本操作-文档查询

前端博主&#xff0c;热衷各种前端向的骚操作&#xff0c;经常想到哪就写到哪&#xff0c;如果有感兴趣的技术和前端效果可以留言&#xff5e;博主看到后会去代替大家踩坑的&#xff5e; 主页: oliver尹的主页 格言: 跌倒了爬起来就好&#xff5e; 来个关注吧&#xff0c;点个赞…

在Word、WPS中插入AxMath公式导致行间距异常的解决办法

引言 我最近需要写一些文章&#xff0c;在排版时发现AxMath插入的公式竟然会导致行间距异常&#xff0c;如下图所示&#xff1a; 查遍互联网&#xff0c;最有效的办法竟然要取消文档网格对齐&#xff0c;这对于一些严格要求的场合是非常不利的&#xff0c;经过我的尝试&#…

xss-labs/level15

因为前一关打不开了 所以直接跳到15关来 查看源代码 他的输出点位于属性值处 所以要想通过<script></script>实现弹窗效果的话 那么就要逃离属性值 那么势必要闭合引号 根据以上的分析 我们做出如下构造 "><script>alert(xss)</script>// …

使用 Mason 创建自己的 Flutter brick

使用 Mason 创建自己的 Flutter brick 原文 https://medium.com/gytworkz/create-your-own-flutter-brick-using-mason-7abc70d0324e 前言 谁不喜欢用最少的努力完成大部分事情呢&#xff1f;我当然知道! &#xff01;Mason 帮我完成了几个简单的步骤。 在本文中&#xff0c;我…

观察者(订阅)模式

文章目录思考观察者模式1.观察者模式的本质2.何时选用观察者模式3.优缺点4.实现手写观察者模式JDK观察者模式思考观察者模式 观察者模式是典型的发布订阅模式&#xff0c;当一个东西有变化了&#xff0c;就通知所有订阅他的人 1.观察者模式的本质 观察者模式的本质:触发联动。 …

什么是等保

等保的全称是信息安全等级保护&#xff0c;是《网络安全法》规定的必须强制执行的&#xff0c;保障公民、社会、国家利益的重要工作。以下是一些有关等保的基本知识&#xff0c;希望通过这些知识能让大家更深刻地认识到等级保护的重要性。 等级保护定义 信息安全等级保护是指…

2.Conv2d实现

[C 基于Eigen库实现CRN前向推理] 第二部分&#xff1a;Conv2d实现 前言&#xff1a;(Eigen库使用记录)第一部分&#xff1a;WavFile.class (实现读取wav/pcm,实现STFT)第二部分&#xff1a;Conv2d实现第三部分&#xff1a;TransposedConv2d实现 (mimo,padding,stride,dilatio…