数据结构之B-树

系列文章目录

数据结构之ArrayList_arraylist o(1) o(n)-CSDN博客

数据结构之LinkedList-CSDN博客

数据结构之栈_栈有什么方法-CSDN博客

数据结构之队列-CSDN博客

数据结构之二叉树-CSDN博客

数据结构之优先级队列-CSDN博客

常见的排序方法-CSDN博客

数据结构之Map和Set-CSDN博客

数据结构之二叉平衡树-CSDN博客

数据结构之位图和布隆过滤器-CSDN博客

数据结构之并查集和LRUCache-CSDN博客


目录

系列文章目录

前言

一、B-树

[1. B-树的性质](#1. B-树的性质)

[2. B-树的插入过程](#2. B-树的插入过程)

[​编辑3. B-树插入过程的实现](#编辑3. B-树插入过程的实现)

[1. B-树节点的定义](#1. B-树节点的定义)

[2. B-树节点的插入](#2. B-树节点的插入)

[3. B-树的性能分析](#3. B-树的性能分析)

二、B+树

[1. B+树的性质](#1. B+树的性质)

[2. B+树的分裂](#2. B+树的分裂)

[3. B-树和 B+树的对比](#3. B-树和 B+树的对比)

三、B-树的应用

[1. 索引](#1. 索引)

[2. MyISAM 和 InnoDB](#2. MyISAM 和 InnoDB)


前言

本文介绍了B-树和B+树的基本性质、插入过程及实现方法。B-树是一种平衡多叉树,具有特定的节点关键字和子节点数量要求。文中详细演示了B-树的插入和分裂过程,并给出了Java实现代码。B+树与B-树类似,但所有关键字都出现在叶子节点,更适合文件索引系统。最后分析了两种树在MySQL索引中的应用差异,指出MyISAM使用B+树作为非聚集索引,而InnoDB采用聚集索引,数据文件本身就是索引文件。文章通过示例和代码展示,帮助理解这两种重要数据结构的特点和应用场景。


一、B-树

1. B-树的性质

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

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

2. B-树的插入过程

下面以三叉树为例,展示 B 树的插入过程;

注意:三叉树只有两个关键字,下面使用四叉树的节点演示三叉树的插入过程,多开一个空间的目的是目的是方便关键字的插入和提取中间节点;

用序列 {53, 139, 75, 49, 145, 36, 101} 构建B树的过程如下:

step1:按顺序依次插入53,139,75

插入时采用插入排序的方式,将值从小到达进行排序,如下图:

step2:节点分裂

分裂一个新的节点,将中间节点右边的值,拷贝到新的节点;

注意:将节点拷贝走的时候,同时也要拷贝它的子节点;

如果进行分裂的节点是根节点,还要将中间值再分裂成一个新的根节点;

step3:插入 49 和 145

step4:插入 36

按照插入排序的方式,插入 36;

左下角节点元素个数已满,分裂一个新的节点,将中间节点后面的元素拷贝到新节点,并将中间节点提到根节点;

step5:插入数字 101

插入排序的方式插入 101;

右边节点已满,分裂一个新节点,中间节点后面的元素都拷贝到新节点,并将中间元素提到根节点;

根节点已满,分裂一个新的节点将中间节点右边的元素都拷贝到新节点,并创建新的根节点,将中间元素提到新的节点;

3. B-树插入过程的实现

1. B-树节点的定义

keys 存放关键字;

subs 存放子节点;

parent 父亲节点;

usedSize 维护关键字的数量;

BTRNode() 构造方法,初始化关键字数组,子节点数组,初始化时,多开一个空间,方便关键字的插入和提取;

java 复制代码
    static class BTRNode{
        public int[] keys;
        public BTRNode[] subs;
        public BTRNode parent;
        public int usedSize;

        public BTRNode(){
            // 为了方便分裂,需多开辟一个空间
            this.keys = new int[M];
            this.subs = new BTRNode[M + 1];
            this.usedSize = 0;
            this.parent = null;
        }
    }

2. B-树节点的插入

M 表示三叉树;

root 表示根节点;

insert(int key): boolean 在 B-树中插入节点;

实现思路:

    1. 如果根节点为空,表示 B-树为空,新插入的节点成为根节点;
    1. 如果要插入的节点,在 B-树中存在,不需要再插入,返回 false 即可;
    1. 采用搜索树的方式,找到关键字 key 要插入的节点,将关键字以插入排序的方式进行插入;
    1. 插入完成后,检查当前节点的关键字数量,如果超过 M,需要将节点进行分裂;

findKey(int key): Pair<BTRNode, Integer> 找到关键字要插入的节点;

实现思路:

    1. 从根节点开始,遍历节点的关键字数组;
    1. 如果要插入的关键字的值等于当前关键字的值,表示关键字已存在,不需要进行插入,直接返回当前节点即可;
    1. 如果要插入的关键字的值大于当前关键字的值,继续往后遍历;
    1. 如果要插入的关键字的值小于当前关键字的值,跳出查找关键字循环,继续遍历当前节点左边的子节点,重复 2 ~ 4 过程;
    1. 如果当前节点为空,当前节点的父亲节点就是关键字要插入的节点,但是因为位置不确定,因此返回父亲节点;

注意:

  • 因为在 insert() 方法中,我们无法区别返回的是当前节点还是父亲节点,也不知道当前的关键字是否在 B-树中存在,因此这里不能简单返回一个节点,而是返回一个数对 Pair<BTRNode, Integer>;
  • 数对中的整数如果为 -1 表示当前返回的节点是父亲节点,要插入的下标未知,用 -1 表示;
  • 数对中的整数如果为大于等于 0 的数字表示返回的节点为 cur 节点,关键字的下标用该数字表示;

split(BTRNode cur): void 分裂当前节点;

实现思路:

    1. 确定当前节点的关键字数组右中点的下标 mid,建立一个新的节点;
    1. 将右中点右边的关键字都拷贝到新节点当中,同时将关键字的子节点也拷贝到新节点当中;同时也要将关键字的子节点的父亲节点设置为新节点;
    1. 维护新节点关键字的数量,以及当前节点关键字的数量;
    1. 判断当前节点是否为根节点,如果是根节点,需要再新建一个节点作为新的根节点,将当前节点 mid 下标指向的关键字拷贝到新的根节点当中;新根节点的子节点数组,0 下标指向当前节点,1 下标指向新节点;同时将当前节点和新节点的父亲节点都设置为新节点;
    1. 如果当前的节点不是根节点,将 mid 下标的关键字拷贝到当前节点的父亲节点的关键字数组中,同时将关键字的右子节点设置为新节点;新节点的父亲节点指向当前节点的父亲节点,并维护父亲节点的关键字的数量;
    1. 判断父亲节点的关键字数量是否超过 M,如果超过,对父亲节点再进行分裂;
java 复制代码
public class MyBTree {    
    public static final int M = 3;
    public BTRNode root;

    public boolean insert(int key){
        // 1. 插入之前要先判断 B 树是否为空
        if(this.root == null){
            BTRNode node = new BTRNode();
            node.keys[0] = key;
            node.usedSize++;
            this.root = node;
            return true;
        }
        // 2. 插入之前要先在 B 树中找,看 key 是否存在
        Pair<BTRNode, Integer> ret = findKey(key);
        // 3. 如果找到了 key,就不用再插入了
        if(ret.getValue() != -1){
            return false;
        }
        // 4. 如果没找到 key,需要进行插入
        BTRNode cur = ret.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++;
        // 5. 如果插入后的 cur.usedSize >= M - 1,需要进行分裂
        if(cur.usedSize >= M){
            split(cur);
            return true;
        }else{
            return true;
        }
    }

    private void split(BTRNode cur) {
        // 创建一个新的节点 - 放在右边
        BTRNode newNode = new BTRNode();
        // 将原来节点的一半值移到新节点中
        int mid = cur.usedSize / 2;
        int i = mid + 1;
        int j = 0;

        for( ; i < cur.usedSize; i++){
            // 不仅要移动 key 值,还要移动它的孩子节点
            newNode.keys[j] = cur.keys[i];
            newNode.subs[j] = cur.subs[i];
            // 处理刚拷贝过来的孩子节点的父亲节点
            if(cur.subs[i] != null){
                cur.subs[i].parent = newNode;
            }
            j++;
        }
        newNode.subs[j] = cur.subs[i];
        if(cur.subs[i] != null){
            cur.subs[i].parent = newNode;
        }
        // 更新 newNode, cur 的 usedSize
        newNode.usedSize = j;
        cur.usedSize = mid;

        // 处理分裂根节点的情况
        if(cur == root){
            BTRNode newRoot = new BTRNode();
            newRoot.keys[0] = cur.keys[mid];
            newRoot.subs[0] = cur;
            cur.parent = newRoot;
            newRoot.subs[1] = newNode;
            newNode.parent = newRoot;
            newRoot.usedSize = 1;
            root = newRoot;
            return;
        }

        // 如果分裂的 cur 节点不是根节点
        BTRNode parent = cur.parent;
        // 将节点的 mid 位置的值,移动向父节点
        int pIndex = parent.usedSize - 1;
        for( ; pIndex >= 0; pIndex--){
            if(parent.keys[pIndex] >= cur.keys[mid]){
                // 不仅要移动 key,还要移动子节点
                parent.keys[pIndex + 1] = parent.keys[pIndex];
                parent.subs[pIndex + 2] = parent.subs[pIndex + 1];
            }else{
                break;
            }
        }
        parent.keys[pIndex + 1] = cur.keys[mid];

        newNode.parent = parent;
        parent.subs[pIndex + 2] = newNode;
        // 更新 parent 的 usedSize
        parent.usedSize++;

        if(parent.usedSize >= M){
            split(parent);
        }
    }

    private Pair<BTRNode, Integer> findKey(int key) {
        BTRNode cur = root;
        BTRNode parent = null;
        while(cur != null){
            int index = 0;
            while(index < cur.usedSize){
                if(cur.keys[index] < key){
                    index++;
                }else if(cur.keys[index] > key){
                    break;
                }else{
                    return new Pair<>(cur, index);
                }
            }
            parent = cur;
            cur = cur.subs[index];
        }
        return new Pair<>(parent, -1);
    }
}

3. B-树的性能分析

对于一棵度为 M,总节点个数为 N 的 B-树,高度在 log(M-1)N ~ log(M/2)N 之间,定位到节点后,利用二分查找,能很快定位到要查找的元素,性能非常高;

例如:N = 62 * 1000000000,如果 M = 1024,则 log(M/2)N = 3.59,即表示 620亿数据,通过 4 次查找就可定位元素的节点,再通过二分查找,最多 10 次就可以找到元素,大大减少了 IO 的次数;

二、B+树

1. B+树的性质

B+树与 B-树的搜索基本相同,区别是 B+树只有达到叶子节点才能命中,性能等价于在关键字全集中做一次二分查找;

  • 非叶子节点的子树个数和关键字个数相同;
  • 非叶子节点的子树指针 p[i],指向关键字值属于 key[i] ~ key[i + 1] 之间的子树;
  • 为所有的叶子节点增加了一个链指针;
  • 所有的关键字都在叶子节点出现;

B+树的特性:

  • 所有关键字都出现在叶子节点的链表中(稠密索引),且链表中的节点都是有序的;
  • 不可能在非叶子节点命中;
  • 非叶子节点相当于是叶子节点的索引(稀疏索引),叶子节点相当于是存储数据的数据层;
  • 更适合文件索引系统;

2. B+树的分裂

当一个节点满时,分配一个新的节点,并将原节点中 1/2 的数据复制到新节点,最后在父亲节点中增加新节点的指针;

B+树的分裂只影响原节点和父亲节点,不影响兄弟节点;

3. B-树和 B+树的对比

B-树是多路搜索树,每个节点存 M/2 - 1 ~ M - 1 个关键字,非叶子节点存储指向关键字范围的子节点;

所有的关键字在整棵树中出现,且只出现依次,非叶子节点可以命中;

B+树在 B-树的基础上为叶子节点增加链表指针,所有的关键字都在叶子节点出现,非叶子节点作为叶子节点的索引,B+树总是到叶子节点才能命中;

三、B-树的应用

1. 索引

B-树最常见的应用就是用来做索引;

索引是一种高效获取数据的数据结构;

在 MySQL 中,索引属于存储引擎级别的概念,不同存储引擎实现索引的方式不同;

索引是基于表的,不是基于数据库的;

2. MyISAM 和 InnoDB

MyISAM 存储引擎不支持事务;

MyISAM 使用 B+树作为索引结构,叶节点的 Data 域存放的是数据的地址,这种索引方式是非聚集索引;

MyISAM 中索引文件和数据文件是分离的(只保存数据的地址);

MyISAM 中主索引和辅助索引的结构是相同的,区别是主索引不可重复,辅助索引可以重复;

InnoDB 是存储引擎支持事务;

InnoDB 存储引擎是 MySQL 默认的存储引擎;

InnoDB 中数据文件本身就是索引文件,其中叶节点的 Data 域保存了完整的数据结构,这种索引方式是聚集索引;

InnoDB 数据文件本身是按主键聚集,因此 InnoDB 要求必须表必须有主键;

如果不显式指定主键,MySQL 会自动选择一个可以唯一表示数据的列作为主键,如果不存在这种列,MySQL 会自动为 InnoDB 表生成一个隐含字段作为主键,字段为 6 个字节,类型为长整型;

InnoDB 的辅助索引的 Data 域存储的是主键的值,并非地址,查询过程为检索辅助索引获得主键,再使用主键去主索引中检索,获得记录,因此需要检索两遍索引;

使用主键索引检索效率非常高,如果使用辅助索引,效率会降低;

相关推荐
晨启AI3 分钟前
Trae IDE:打造完美Java开发环境的实战指南
java·环境搭建·trae
C雨后彩虹16 分钟前
行为模式-策略模式
java·设计模式·策略模式
Ashlee_code21 分钟前
美联储降息趋缓叠加能源需求下调,泰国证券交易所新一代交易系统架构方案——高合规、强韧性、本地化的跨境金融基础设施解决方案
java·算法·金融·架构·系统架构·区块链·需求分析
西奥_24 分钟前
【JVM】运行时数据区域
java·jvm
lgx04060511241 分钟前
Maven详细解
java·maven
玩代码1 小时前
模板方法设计模式
java·开发语言·设计模式·模板方法设计模式
都叫我大帅哥1 小时前
Spring Cloud LoadBalancer:微服务世界的“吃货选餐厅”指南 🍜
java·spring cloud
摸鱼仙人~1 小时前
Spring Boot 参数校验:@Valid 与 @Validated
java·spring boot·后端
GeminiGlory1 小时前
从0到1开发网页版五子棋:我的Java实战之旅
java·开发语言
都叫我大帅哥2 小时前
🌈 深入浅出Java Ribbon:微服务负载均衡的艺术与避坑大全
java·spring cloud