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-树的特性:
- 每个节点最多有m个孩子,其中m称为B-树的阶
- 除根节点外,其他每个节点至少有ceil(m/2) 个孩子
- 若根节点不是叶子节点,则至少有两个孩子
- 所有叶子节点都在同一层
- 每个非叶子节点的有n个关键字(key)和n+1 个指针组成,关键字数:ceil(m/2) -1 < m-1
- 关键字按非降序排列,即节点的第i个关键字大于第i-1个关键字
- 指针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 方法
- 首先查找本节点中的插入位置i,如果key 被找到,应该走更新逻辑,目前什么没做
- 如果没找到key,并且找到了插入位置,则有两种情况
2.1 如果节点是叶子节点,可以直接插入
2.2 如果节点是非叶子节点,需要继续在children[i] 处继续递归插入 - 无论哪种情况,插入完成后都可能超过节点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时会发生裂变:
过程:
- 创建right 节点(分裂后大于当前left节点的)
- 从 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
- right 节点作为parnet 的孩子插入到 index + 1处
分裂后为非叶子节点
处理过程:将left 节点 t-1 个key copy 到right的节点后,也要将 移动节点的孩子处理过去,其他步骤都一样
裂变过程:
t = 2
-
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);
}
本文来自博客园,作者:chuangzhou,转载请注明原文链接:https://www.cnblogs.com/czzz/p/17961980
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源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 是什么