JAVA 实现 - B树

B树历史

B-树是一种自平衡的树形数据结构,它可以存储大量的数据并且支持高效的查找、插入和删除操作。B树-最初是由RudoIf Bayer 和 Edward McCreight 在1972年提出的,用于解决磁盘存储器上的数据管理问题。B-树的设计目标是减少磁盘I/O 操作的次数,从而提高数据访问的效率。

B-树的命名中"B"代表 "Balance",即平衡的意思,B-树的平衡性是指树的所有叶子节点都在同一层级上,这样可以保证每个节点的访问时间相同。B-树的平衡性是通过在节点中存储多个关键词来实现的,这些关键词可以用来分割节点中的数据。B树的历史可以追溯到1962年,当时RudoIf Bayer 在IBM工作,他的工作是研究如何在磁带上存储数据。他发现,磁带的读写非常慢,而且每次读写都需要花费很长时间。为了解决这个问题,他设计了一种树形数据结构,可以将数据存储在磁带上,并且可以高效地访问数据。后来,B树被Edward McCreight 改进,他将B-树的设计应用到了磁盘存储器上。他发现,B-树可以有效地减少磁盘I/O操作的次数,从而提高数据访问的效率。B-树的设计思想被广泛应用于数据库系统中,成为了数据库系统中最重要的数据

100万数据用AVL树来存储,树高是多少? - 20

import math

def avl_height(data: int):
    return math.ceil(math.log2(data))

print(avl_height(1000000)) # 输出20

100数据用B-树来存储,树高是多少? - 3

树高代表着查找次数,如果磁盘数据用AVL树来存储,读写次数多而且磁盘的读写速度慢,而使用B-树只用读写3次

因此:

  • AVL树:存储内存数据
  • B-树:存储磁盘数据

B-树的特性

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

B-树的特性:

  1. 每个节点最多有m个孩子,其中m称为B-树的阶
  2. 除根节点外,其他每个节点至少有ceil(m/2) 个孩子
  3. 若根节点不是叶子节点,则至少有两个孩子
  4. 所有叶子节点都在同一层
  5. 每个非叶子节点的有n个关键字(key)和n+1 个指针组成,关键字数:ceil(m/2) -1 < m-1
  6. 关键字按非降序排列,即节点的第i个关键字大于第i-1个关键字
  7. 指针P[i]指向3关键字值位于第i个关键字和第i+1个关键字之间的子树

节点类

package com.datastructure.binarytree.btree;

import java.util.Arrays;

public class BTree {

    ...

    static class Node {
        int[] keys; //健
        Node[] children; //孩子
        int keyNumber; //有效的key数量
        boolean leaf; //是否叶子节点
        int t; //最小度数(最小孩子数)

        public Node(int t) {  //t >= 2
            this.t = t;
            this.children = new Node[2 * t];  //B-树约定:最小度数 * 2 = 最大度数
            this.keys = new int[2 * t - 1];
        }

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

        //查找元素
        Node get(int key) {
            int i = 0; //当前索引
            while (i < keyNumber) {
                if (keys[i] == key) {
                    return this; //找到key
                }
                if (keys[i] > key) {
                    break;
                }
                i++;

            }
            // 未找到key:i > keyNumber:节点找完不存在要查找的key
            //未找到key:keys[i] > key:找到比当前key大的第一个元素

            if (leaf) {
                return null;
            }
            //非叶子节点
            return children[i].get(key);  //递归查找
        }

        //向keys 指定索引 index 处插入key
        void insertKeys(int key, int index) {

            System.arraycopy(keys, index, keys, index + 1, keyNumber - index);
            keys[index] = key;
            keyNumber++;
        }

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

put 方法

  1. 首先查找本节点中的插入位置i,如果key 被找到,应该走更新逻辑,目前什么没做
  2. 如果没找到key,并且找到了插入位置,则有两种情况
    2.1 如果节点是叶子节点,可以直接插入
    2.2 如果节点是非叶子节点,需要继续在children[i] 处继续递归插入
  3. 无论哪种情况,插入完成后都可能超过节点keys数据限制,此时应当执行节点分裂
package com.datastructure.binarytree.btree;

import java.util.Arrays;

public class BTree {

    ...
   
    Node root;
    int t; //树的最小度数,最小孩子数
    //最大key数
    final int MAX_KEY_NUMBER;//最大孩子数 -1
    final int MIN_KEY_NUMBER;//最小孩子数-1

    //最小key数
    public BTree() {
        this(2);
    }

    public BTree(int t) {
        this.t = t;
        MAX_KEY_NUMBER = 2 * t - 1;
        MIN_KEY_NUMBER = t - 1;
    }

    public boolean contains(int key) {
        return root.get(key) != null;
    }

    //插入key,省略了值的操作
      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++;
        }
        
        if (node.leaf) {
            node.insertChild(node, key);
        } else {
            doPut(node.children[i], key, node, index);
        }

        if (node.keyNumber == MAX_KEY_NUMBER) {
            //执行分裂
            split(node, node, index);
        }

    }
}

裂变

裂变分析

分裂后为叶子节点

存在如下一颗B数,t=3,最小度数(孩子数)为3,最大key数为 2*t-1 = 5,因此当插入key=8时会发生裂变:

过程:

  1. 创建right 节点(分裂后大于当前left节点的)

  1. 从 left[t] 开始 ,copy t-1 长度的key 到 right 节点的key ,即:key=7 和 key=8

3.t -1 的key 插入到 parent 的index 处, index 指当前节点作为孩子时的索引,下图中index 就是1 ,移动的key为 left[2] = 6

  1. right 节点作为parnet 的孩子插入到 index + 1处

分裂后为非叶子节点

处理过程:将left 节点 t-1 个key copy 到right的节点后,也要将 移动节点的孩子处理过去,其他步骤都一样

裂变过程:

t = 2

  1. copy 孩子,从 t 处开始, copy 长度: t

分裂代码的实现

 /**
 *
 * @param left 待分裂节点
 * @param parent 待分裂节点的父节点
 * @param index 待分裂节点作为孩子时的索引
 */
public 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节点,将  t之后的key copy到right节点
    Node right = new Node(t);
    right.leaf = left.leaf;
    System.arraycopy(left.keys, t, right.keys, 0,t-1);

    //待分裂节点和 right 节点 是否为 叶子节点的属性一致,因此可以判断left 节点是否为叶子节点
    if(!left.leaf){ //如果是非叶子节点:处理完key之后,要把孩子也处理过去
        System.arraycopy(left.children, t, right.children, 0 , t); //孩子数比key多一
    }
    //处理节点的有效key的大小
    right.keyNumber = t -1;
    left.keyNumber = t -1;
    //2. 将t-1 的key 插入到父节点的index处
    parent.insertKeys(left.keys[t-1] , index);

    // 将right 节点作为parent的 index + 1 处的孩子插入
    parent.insertChild(right, index + 1);
}

https://blog.51cto.com/universsky/6086378

posted @   chuangzhou  阅读(59)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
历史上的今天:
2023-01-13 Pytest - 自定义测试ID
2022-01-13 HTTP
2022-01-13 xhr 是什么
点击右上角即可分享
微信分享提示