数据结构与算法——20.B-树

news2024/10/6 16:21:44

这篇文章我们来讲解一下数据结构中非常重要的B-树。

目录

1.B树的相关介绍

1.1、B树的介绍

1.2、B树的特点

2.B树的节点类

3.小结


1.B树的相关介绍

1.1、B树的介绍

在介绍B树之前,我们回顾一下我们学的树。

首先是二叉树,这个不用多说,然后为了查找的效率,我们提出了搜索二叉树(或者称为二叉搜索树),就是节点类加个key值,然后左边小右边大的那个。然后为了避免极端情况的出现,就是二叉搜索树节点集中在一侧的情况,我们提出了平衡二叉树,就是带自旋的,可以左旋或者右旋的,高度差小于1的那种,平衡二叉树里面有AVL树和红黑树两种实现方式,注意,平衡二叉树是在二叉搜索树的基础上提出的,所以平衡二叉树也叫平衡二叉搜索树

下面介绍一下B树。

B-树是一种自平衡的多路查找树,注意: B树就是B-树,"-"是个连字符号,不是减号 。

在大多数的平衡查找树(Self-balancing search trees),比如 AVL树 和红黑树,都假设所有的数据放在主存当中。那为什么要使用 B-树呢(或者说为啥要有 B-树呢)?要解释清楚这一点,我们假设我们的数据量达到了亿级别,主存当中根本存储不下,我们只能以块的形式从磁盘读取数据,与主存的访问时间相比,磁盘的 I/O 操作相当耗时,而提出 B-树的主要目的就是减少磁盘的 I/O 操作

大多数平衡树的操作(查找、插入、删除,最大值、最小值等等)需要 O(ℎ)次磁盘访问操作,其中 ℎ 是树的高度。但是对于 B-树 而言,树的高度将不再是log(n)(n为数中节点的个数),而是一个我们可控的高度 ℎ (通过调整 B-树中结点所包含的键【你也可以叫做数据库中的索引,本质上就是在磁盘上的一个位置信息】的数目,使得 B-树的高度保持一个较小的值)一般而言,B-树的结点所包含的键的数目和磁盘块大小一样,从数个到数千个不等。由于B-树的高度 h 可控(一般远小于log(n)),所以与 AVL 树和红黑树相比,B-树的磁盘访问时间将极大地降低。

我们之前谈过红黑树与AVL树相比较,红黑树更好一些,这里我们将红黑树与B-树进行比较,并以一个例子对上面一段的内容进行解释。

假设我们现在有 838,8608 条记录,对于红黑树而言,树的高度 ℎ=log⁡(838,8608)=23 ,也就是说树的高度为23,也就是说如果要查找到叶子结点需要 23 次磁盘 I/O 操作;但是 B-树,情况就不同了,假设每一个结点可以包含 8 个键(当然真实情况下没有这么平均,有的结点包含的键可能比8多一些,有些比 8 少一些),那么整颗树的高度将最多 8 ( log8⁡(838,8608)=7.8 ) 层,也就意味着磁盘查找一个叶子结点上的键的磁盘访问时间只有 8 次,这就是 B-树提出来的原因所在。

1.2、B树的特点

下面讲一下B树的特点

在讲B树的特点之前,我们先来了解几个概念

度:degree 指树中节点的孩子数

阶:order 指所有节点中孩子数最大值

B树的特点:

  1. 每个节点最多有m个孩子,其中m称为B-树的阶;(孩子数目的上限)
  2. 除根节点和叶子节点外,其他节点至少有 ceil(m/2) (阶数除以2向上取整)个孩子,就是说B树中节点最大有m个孩子即阶个孩子,至少有 m/2(向上取整) 个孩子;(孩子数目的下限)
  3. 若根节点不是叶子节点,则至少有两个孩子;(根节点孩子数的下限)
  4. 所有叶子节点都在同一层;(B树是否平衡的前提条件)
  5. 每个非叶子节点由 n 个关键字(就是n个关键值,参考二叉搜索树中的关键值)和 n+1 个指针(就是n+1个孩子)组成,其中 ceil(m/2)-1 <= n <= m-1;
  6. 关键字按非降序排列(就是升序排列,和二叉树搜索相同),即节点中的第 i 个关键字大于等于第 i-1 个关键字;
  7. 指针P[ i ] 指向关键字值位于第 i 个关键字和第 i+1 个关键字之间的子树;

这些特性都要理解。看一下一个B树的实例:

2.B树的节点类

下面,我们来看一下B树的具体实现吧

package Tree;

import java.util.Arrays;

public class L5_BTree {

    //B数的节点类
    static class Node{

        int[] keys; //关键字,即关键值,排序用的
        Node[] children; //孩子,存孩子用的节点类数组
        int keyNumber; //有效关键字数目(就是真正存了几个关键字)
        boolean leaf = true; //是否是叶子节点
        int t; //最小度数(最小孩子数)

        //构造函数
        public Node(int t) { // t >= 2
            this.t = t;//手动设置最小孩子数
            this.children = new Node[2 * t];//最大孩子数是最小孩子数的二倍
            this.keys = new int[2 * t -1];//关键字的最大数量 是 最大孩子数-1
        }

        @Override
        public String toString() {
            return Arrays.toString(Arrays.copyOfRange(keys,0,keyNumber));
        }

        //多路查找,就是我给你一个关键值,你返回这个关键值对应的节点
        Node get(int key){
            int i = 0; //设置个变量i,方便用来循环遍历
            while (i < keyNumber){ //节点中有关键字
                if (keys[i] == key){ //如果节点中的关键字 等于 我给出的关键字,那就返回这个关键字对应的节点
                    return this;
                }
                if (keys[i] > key){ //如果关键字中的最小值都比给出的大,那就直接退出这个节点的循环了
                    break;
                }
                i++; //变量i自增
            }
            //执行到这里,就是说当前节点的关键字一定比给出的大,或者说,超出索引了,即keys[i]>key 或 i == keyNumber
            if (leaf){ //如果是叶子节点,那就肯定没有孩子了
                return null;
            }
            //这种情况就是 i == keyNumber 了,就找这个节点所对应的孩子了(孩子数比节点关键值数多1)
            return children[i].get(key);
        }

        //写一个方法,向 keys 指定索引 index 处插入 key
        void insertKey(int key, int index){
            for (int i = keyNumber-1; i >= index ; i--) {
                keys[i+1] = keys[i];
            }
            keys[index] = key;
            keyNumber++;
        }

        //写一个方法,向 children 指定索引 index 处插入 child
        void insertChild(Node child, int index){
            System.arraycopy(children,index,children,index+1,keyNumber);
            children[index] = child;
        }

        //移除指定index处的key
        int removeKey(int index){
            int t = keys[index];
            System.arraycopy(keys,index+1,keys,index,--keyNumber-index);
            return t;
        }
        //移除最左边的key
        int removeLeftmostKey(){
            return removeKey(0);
        }
        //移除最右边的key
        int removeRightmostKey(){
            return removeKey(keyNumber-1);
        }


        //移除指定index处的child
        Node removeChild(int index){
            Node node = children[index];
            children[index] = null;
            return children[index];
        }
        //移除最左边的child
        Node removeLeftmostChild(){return removeChild(0);}
        //移除最右边的child
        Node removeRightmostChild(){return removeChild(keyNumber);}

        //返回index孩子处左边的兄弟
        Node childLeftSibling(int index){
            return index > 0 ? children[index-1]:null;
        }
        //返回index孩子处右边的兄弟
        Node childRightSibling(int index){
            return index == keyNumber ? null : children[index+1];
        }

        //复制当前节点的所有key和child到target
        void moveToTarget(Node target){
            int start = target.keyNumber;
            if (!leaf){
                for (int i = 0; i <= keyNumber; i++) {
                    target.children[start+i] = children[i];
                }
            }
            for (int i = 0; i < keyNumber; i++) {
                target.keys[target.keyNumber++] = keys[i];
            }
        }

    }


    Node root; //定义一个根节点
    int t; //树中节点的最小度数(就是一个节点的最小孩子数,根节点叶子节点除外)
    final int MIN_KEY_NUMBER;//最小关键字的数量
    final int MAX_KEY_NUMBER;//最大关键字的数量

    //无参构造,最小度数默认值为2
    public L5_BTree() {
        this(2);
    }
    //有参构造
    public L5_BTree(int t) {
        this.t = t;
        root = new Node(t);//new出根节点,并给出根节点最小度数
        MIN_KEY_NUMBER = t-1;
        MAX_KEY_NUMBER = 2*t-1;
    }

    //判断关键字中是否存在指定关键字对应的节点
    public boolean contains(int key){
        return root.get(key) != null;
    }

    //新增一个关键字
    /**描述一下流程吧
     * 你构造一颗B树,给定了最小度数,那么最小关键字数、最大关键字数、阶数也就都定了
     * 你开始往节点中插入关键值,一开始没满,你继续插入
     * 当插入的关键字数等于最大关键字数时,这个节点就要分裂了,即将自身的关键字分出去,变为孩子节点
     * 然后你再插入,它就会按照关键字的顺序去选位置,
     * 如果找到位置了,是叶子节点,那么就直接插入(当然超过MAX_KEY_NUMBER就分裂一下)
     * 如果恰好发现一个非叶子节点里面也有位置,那么应该先搜索一下这个节点的孩子,然后再进行判断插在哪里
     * 当某个节点的关键字数再满,那这个树就再分裂一次
     * */
    public void put(int key){
        doPut(root,key,null,0);
    }
    //递归的函数
    private void doPut(Node node,int key,Node parent,int index){
        int i = 0;
        while (i < node.keyNumber){
            if (node.keys[i] == key){
                return; //更新逻辑
            }
            if (node.keys[i] > key){
                break; //找到插入位置,记为i
            }
            i++;
        }
        if (node.leaf){
            node.insertKey(key,i);
            //可能到达上限
        }else {
            doPut(node.children[i],key,node,i);
            //可能到达上限
        }
        if (node.keyNumber == MAX_KEY_NUMBER){
            split(node,parent,index);
        }
    }

    //分裂函数
    /**
     * left:要分裂的节点
     * parent:分裂节点的父节点
     * index:分裂节点是第几个孩子
     * */
    private void split(Node left, Node parent, int index){
        if (parent == null){//分裂的是根节点
            Node newRoot = new Node(t);
            newRoot.leaf = false;
            newRoot.insertChild(left,0);
            this.root = newRoot;
            parent = newRoot;
        }

        //1.创建right节点,把left中t之后的key和child移动过去
        Node right = new Node(t);
        right.leaf = left.leaf;
        System.arraycopy(left.keys,t,right.keys,0,t-1);

        //分裂节点是非叶子节点的情况
        if (!left.leaf){
            System.arraycopy(left.children,t,right.children,0,t);
        }
        right.keyNumber = t-1;
        left.keyNumber = t-1;

        //2.中间的key(t-1处)插入到父节点中
        int mid = left.keys[t-1];
        parent.insertKey(mid,index);

        //3.right节点作为父节点的孩子
        parent.insertChild(right,index+1);
    }


    //删除一个关键字
    public void remove(int key){doRemove(null,root,0,key);}

    private void doRemove(Node parent,Node node,int index,int key){
        int i = 0;
        while (i < node.keyNumber){
            if (node.keys[i] >= key){
                break;
            }
            i++;
        }
        //找到了,代表待删除key的索引
        //没找到,表示到第 i 个孩子里面继续查找
        if (node.leaf){
            if(!found(node, key, i)){//case1
                return;
            }else {//case2
                node.removeKey(i);
            }
        }else {
            if(!found(node, key, i)){//case3
                doRemove(node,node.children[i],i,key);
            }else {//case4
                Node s = node.children[i+1];
                while (!s.leaf){
                    s = s.children[0];
                }
                int skey = s.keys[0];
                node.keys[i] = skey;
                doRemove(node,node.children[i+1],i+1,skey);
            }
        }
        if (node.keyNumber < MIN_KEY_NUMBER){
            //调整平衡 case5 and case6
            balance(parent,node,index);
        }

    }
    private void balance(Node parent, Node x, int i){
        //case6 根节点
        if (x == root){
            if (root.keyNumber == 0 && root.children[0] != null){
                root = root.children[0];
            }
            return;
        }
        Node left = parent.childLeftSibling(i);
        Node right = parent.childRightSibling(i);
        if (left != null && left.keyNumber > MAX_KEY_NUMBER){
            //case5-1 左边富裕 右旋
            //把父节点中前驱key旋转下来
            x.insertKey(parent.keys[i-1],0);
            if (!left.leaf){
                //left中最大的孩子换爹
                x.insertChild(left.removeRightmostChild(),0);
            }
            //left中最大的key旋转上去
            parent.keys[i-1] = left.removeRightmostKey();
            return;
        }
        if (right != null && right.keyNumber > MAX_KEY_NUMBER){
            //case5-2 右边富裕 左旋
            //把父节点中后继key旋转下来
            x.insertKey(parent.keys[i],x.keyNumber);
            //right中最小的孩子换爹
            if (!right.leaf){
                x.insertChild(right.removeLeftmostChild(),x.keyNumber+1);
            }
            //right中最小的key旋转上去
            parent.keys[i] = right.removeLeftmostKey();
            return;
        }
        //case5-3 两边都不富裕 向左合并
        if(left != null){
            //向左兄弟合并
            parent.removeChild(i);
            left.insertKey( parent.removeKey(i-1), left.keyNumber);
            x.moveToTarget(left);
        }else {
            //自己合并
            parent.removeChild(i+1);
            x.insertKey(parent.removeKey(i),x.keyNumber );
            right.moveToTarget(x);
        }

    }
    private boolean found(Node node, int key, int i) {
        return i < node.keyNumber && node.keys[i] == key;
    }


}

为了对应代码中插入和删除的逻辑思路,下面给出两张图来看一下。

节点中插入key值后的节点分裂展示图:

在节点中删除key的6种情况展示图(删除的是某个节点的key):

3.小结

说实话,我感觉这东西挺难的,写完之后脑瓜子都嗡嗡的。没有在纸上画图,单靠脑子想,我是肯定写不出来的,所以我的建议是:一定一定一定要画图,一定一定一定要看着图对着代码来一步一步的走,一定一定一定要看图!

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

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

相关文章

车载电子电器架构 —— 电子电气架构开发总结和展望

车载电子电器架构 —— 电子电气架构开发总结和展望 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 屏蔽力是信息过载时代一个人的特殊竞争力,任何消耗你的人和事,多看一眼都是你的不对。非必要…

HTML基本语法

前言&#xff1a; html中不区分大小写&#xff0c;但建议用小写&#xff0c;因为使用组件时一般使用大写&#xff0c;便于区分两者 注释&#xff1a; <!-- 注释的内容 --> ~注释的内容只会显示在源码当中&#xff0c;不会显示在网页中 ~用于解释说明代码&#xff0c;或隐…

论文笔记:The Expressive Power of Transformers with Chain of Thought

ICLR 2024 reviewer 评分 6888【但是chair 很不喜欢】 1 intro 之前的研究表明&#xff0c;即使是具有理想参数的标准Transformer&#xff0c;也无法完美解决许多大规模的顺序推理问题&#xff0c;如模拟有限状态机、判断图中的节点是否相连&#xff0c;或解决矩阵等式问题 这…

架构师系列-搜索引擎ElasticSearch(七)- 集群管理之分片

集群健康检查 Elasticsearch 的集群监控信息中包含了许多的统计数据&#xff0c;其中最为重要的一项就是集群健康&#xff0c;它在 status字段中展示为 green&#xff08;所有主分片和副本分片都正常&#xff09;、yellow&#xff08;所有数据可用&#xff0c;有些副本分片尚未…

探索设计模式的魅力:深度挖掘响应式模式的潜力,从而精准优化AI与机器学习项目的运行效能,引领技术革新潮流

​&#x1f308; 个人主页&#xff1a;danci_ &#x1f525; 系列专栏&#xff1a;《设计模式》 &#x1f4aa;&#x1f3fb; 制定明确可量化的目标&#xff0c;坚持默默的做事。 挖掘响应式模式&#xff0c;优化AI与机器学习项目性能&#xff0c;引领技术新潮流 ✨机器学习界的…

phpMyadmin 设置显示完整内容

额外选项这里&#xff0c;默认部分内容改成完整内容 方案&#xff1a; 版本>4.5.4.1&#xff0c;修改文件&#xff1a;config.inc.php&#xff0c;添加一行代码&#xff1a; if ( !isset($_REQUEST[pftext])) $_REQUEST[pftext] F;

【core analyzer】core analyzer的介绍和安装详情

目录 &#x1f31e;1. core和core analyzer的基本概念 &#x1f33c;1.1 coredump文件 &#x1f33c;1.2 core analyzer &#x1f31e;2. core analyzer的安装详细过程 &#x1f33c;2.1 方式一 简单但不推荐 &#x1f33c;2.2 方式二 推荐 &#x1f33b;2.2.1 安装遇到…

IOS 短信拦截插件

在使⽤iOS设备的时候, 我们经常会收到1069、1065开头的垃圾短信, 如果开了iMessage会更严重, 各种乱七⼋糟的垃圾信息会时不时地收到。 从iOS11开始, ⼿机可以⽀持恶短信拦截插件了. 我们可以通过该插件添加⼀些规则通过滤这些不需要的信息. ⼀. 使⽤xcode新建⼀个项⽬ 【1】…

python爬虫-----Selenium (第二十二天)

&#x1f388;&#x1f388;作者主页&#xff1a; 喔的嘛呀&#x1f388;&#x1f388; &#x1f388;&#x1f388;所属专栏&#xff1a;python爬虫学习&#x1f388;&#x1f388; ✨✨谢谢大家捧场&#xff0c;祝屏幕前的小伙伴们每天都有好运相伴左右&#xff0c;一定要天天…

设计模式系列:责任链模式

简介 责任链模式是一种行为型设计模式&#xff0c;它允许你将请求沿着处理者链进行发送。每个处理者都可以对请求进行处理&#xff0c;或者将其传递给链上的下一个处理者。责任链模式主要应用于面向对象编程中&#xff0c;特别是当系统中的对象需要根据其属性来决定如何处理请…

Python程序设计 二维列表

教学案例九 二维列表 1. 成绩文件的读取 score.csv文件中记录了多门同学的编号、姓名和三门功课的成绩(逗号键分隔) 格式如下 编写程序&#xff0c;将文件score.csv文件中的数据放入二维列表cjlb中(注意&#xff1a;语文、数学、英语成绩要转换为数值类型) f1open("lbk…

ASP.NET基于BS的计算机等级考试系统的设计与实现

摘 要 随着计算机技术的发展及计算机的日益普及&#xff0c;基于B/S结构的考试系统与无纸化办公一样已成为大势所趋。论文详细论述了一个基于B/S结构的计算机等级考试系统的设计过程。软件采用ASP.NET 2005作开发平台&#xff0c;C#作编程语言&#xff0c;SQL Server 2005作…

sheng的学习笔记-AI-决策树(Decision Tree)

AI目录&#xff1a;sheng的学习笔记-AI目录-CSDN博客 目录 什么是决策树 划分选择 信息增益 增益率 基尼指数 剪枝处理 预剪枝 后剪枝 连续值处理 另一个例子 基本步骤 排序 计算候选划分点集合 评估分割点 每个分割点都进行评估&#xff0c;找到最大信息增益的…

绿联HDMI延长器40265使用AG7120芯片放大器方案

HDMI延长器和放大器 延长器&#xff1a;主要用于HDMI线的延长&#xff0c;有HDMI对接头方式延长&#xff0c;或HDMI公头加HDMI母头的HDMI线进行延长&#xff0c;或通过网线方式延长&#xff0c;早期为双网线&#xff0c;目前已发展为单网线&#xff0c;需要注意的是&#xff0…

ChatGPT-4 Turbo 今天开放啦!附如何查询GPT-4 是否为 Turbo

2024年4月12日&#xff0c;OpenAI在X上宣布GPT-4 Turbo开放了&#xff01;提高了写作、数学、逻辑推理和编码方面的能力。另外最重要的是&#xff0c;响应速度更快了&#xff01;&#xff01; ChatGPT4 Turbo 如何升级&#xff1f;解决国内无法升级GPT4 Turbo的问题&#xff0…

【数据结构】泛型(分享重点)

什么是泛型&#xff1f; 泛型就是适用于许多许多类型&#xff0c;对类型参数化。 怎么创建一个泛型呢 class 泛型类名称<类型形参列表> { // 这里可以使用类型参数 } class ClassName<T1, T2, ..., Tn> { } class 泛型类名称<类型形参列表> extends 继承类…

消息中间件Kafka分布式数据处理平台

目录 一.Kafka基本介绍 1.定义 2.特点 &#xff08;1&#xff09;高吞吐量、低延迟 &#xff08;2&#xff09;可扩展性 &#xff08;3&#xff09;持久性、可靠性 &#xff08;4&#xff09;容错性 &#xff08;5&#xff09;高并发 3.系统架构 &#xff08;1&#…

oracle 19c 主备 补丁升级19.22

补丁升级流程 备库升级 备库备份$ORALCE_HOME du -sh $ORACLE_HOME ​​​​​​​ 备份目录将dbhome_1压缩 cd $ORACLE_HOME cd .. Ls tar -cvzf db_home.tar.gz db_home_1 /opt/oracle/product/19c ​​​​​​​​​​​​​​ 关闭监听关闭数据库查看sq…

【VS2019】x64 Native Tools Command Prompt for Vs 2019使用conda命令进入环境

【VS2019】x64 Native Tools Command Prompt for Vs 2019使用conda命令进入环境 安装完VS2019后&#xff0c;打开终端x64 Native Tools Command Prompt for Vs 2019&#xff0c;直接运行conda会出现‘conda’ 不是内部或外部命令&#xff0c;也不是可运行的程序 原因分析&am…

【Java虚拟机】三色标记、增量更新、原始快照、记忆集与卡表

三色标记、增量更新、原始快照、记忆集与卡表 三色标记基本原来错标、漏标错标漏标 增量更新基本原理写屏障 原始快照基本原理为什么G1使用原始快照而不用增量更新。 记忆集与卡表 三色标记 基本原来 三色标记是JVM的垃圾收集器用于标记对象是否存活的一种方法。 三色是指黑…