文章图片代码来自邓俊辉老师的课件

概述

上图就是 B-Tree 的结构,可以看到这棵树和二叉树有点不同---“又矮又肥”。同时子节点可以有若干个小的子节点构成。那么这样一棵树又有什么作用呢?

动机

我们知道电脑的访问内存比访问外的存I/O操作快了,但是内存的容量大小又只有那么一点点(相对于外存),所以计算机访问的过程常常使用高速缓存。使用高速缓存也是在以下两个事实想出的策略。

而B-Tree这种结构就是根据这种情况被发掘出来的。下图 m 指的是每次的数据块数量

B-Tree 介绍

多路平衡

关键码指的是一个超级节点包含的子节点。

B-Tree定义

m阶指的是m路,一个超级节点最大可以分出多少路。二叉树分出两边,左边和右边,就是两路,二阶。

下面是几个定义为不同阶的B-树。

分支数

B-Tree的分支数有个上下限,例如6阶的B-Tree(m=6),又被称为 “(3,6)-树”,类似的还有 “(3,5)-树”,“(2,4)-树”,而(2,4)树就是我们后面要学的红黑树。

最大树高和最小数高

可以看到对于“含N个关键码的m阶B-树”的最大树高和最小树高之间的波动并不大。

代码实现

代码实现主要的两个方法为插入和删除。其中插入的时候需要注意查看某个节点是否超出了阶数,若超出了,需要分裂,最坏的情况就是分裂到根部,而删除操作需要注意查看是否会产生下溢,处理下溢,我们常用的方法就是旋转和合并。

插入

下图分别是分裂和再分裂的图示。    

删除

删除操作的旋转和合并。

旋转可以理解为左右兄弟有足够的节点,向左右兄弟节点借来补充的操作。

假如向兄弟们借都不成功,那就拿父节点的一个元素一起合并,代码实现中有分左合并和右合并。

java版本代码

B-树节点类

package BTree;

import java.util.Vector;

/**
* B-树的节点Bean
* 包含一个有序向量 value 和 指向子节点的 child 向量
*
*/
public class BTreeNode {
BTreeNode parent;
Vector<BTreeNode> child;
Vector<Integer> value; public BTreeNode(int value, BTreeNode left, BTreeNode right) {
if (this.value == null) {
this.value = new Vector<>();
this.value.sort(new VectorComparable());
}
if (child == null) {
child = new Vector<>();
}
this.value.add(value);
this.child.add(0, left);
this.child.add(1, right);
if (left != null) {
left.parent = this;
}
if (right != null) {
right.parent = this;
}
} public BTreeNode() {
parent = null;
if (this.value == null) {
this.value = new Vector<>();
this.value.sort(new VectorComparable());
}
if (child == null) {
child = new Vector<>();
}
} /**
* 一个关键块内的查找 查找到与否都返回一个index
* 返回最靠近的值的原因是为了下面的节点继续查找
*
* @param value 查找的值
* @return 不存在的情况返回最靠近的index 值 , -1
*/
public int search(int value) {
int hot = -1;
for (int i = 0; i < this.value.size(); i++) {
if (this.value.get(i) > value) {
return hot;
} else if (this.value.get(i) < value) {
hot = i;
} else { // 相等
return i;
}
}
return this.value.size() - 1;
} public int getIndexInValue(int compare) {
for (int i = 0; i < this.value.size(); i++) {
if (compare == value.get(i)) {
return i;
}
}
return -1;
} /**
* 查找当前node在父节点中的index
*
* @return -1 为父类不存在或是父类为null ,其他为当前节点在父节点为位置
*/
public int getIndexFromParent() {
if (parent == null) {
return -1;
}
for (int i = 0; i < parent.child.size(); i++) {
if (parent.child.get(i) == this) {
return i;
}
}
return -1;
} public void addValue(int index, int val) {
value.add(index, val);
value.sort(new VectorComparable());
} public void addValue(int val) {
value.add(val);
value.sort(new VectorComparable());
} }

B-树数据结构方法。

package BTree;

public class BTree {
private BTreeNode root;
private int degree; // m阶B-树 ,阶树至少为3 /*
* 私有方法
*/ /**
* 查找在哪个BTreeNode ,假如到了外部节点,返回该外部节点 返回的结果只有两种 :
* - 存在,返回该节点
* - 不存在,返回值应该插入的节点
*
* @param val 查找的值
* @return 返回搜索结果,假如该关键块不存在(到达了外部节点)就返回该关键快
*/
private BTreeNode searchSurroundNode(int val) {
BTreeNode node, hot = null;
int rank;
node = root;
while (node != null) {
rank = node.search(val);
if (rank != -1 && node.value.get(rank) == val) { // 找到对应的值
return node;
} else {
hot = node;
if (node.child.get(rank + 1) == null) {
return hot;
}
node = node.child.get(rank + 1);
}
}
// 到了外部节点
return hot;
} private void addNodeForBtNode(BTreeNode node, int rank, int val) {
node.addValue(val);
if (rank != -1) {
node.child.add(rank + 2, null);
} else {
node.child.add(0, null);
}
} /*
* 下面为可调用的方法
*/ public BTree(int degree) {
this.degree = degree;
} /**
* 返回值所在的节点
*
* @param val 插入的值
* @return 找到的话返回节点,找不到返回 null
*/
public BTreeNode search(int val) {
BTreeNode node = searchSurroundNode(val);
if (node.value.get(node.search(val)) == val) { // 该节点存在该值
return node;
}
return null;
} /**
*
* 插入的值都会进入到底部节点
* @param val 插入的值
* @return 是否插入成功
*/
public boolean insert(int val) {
if (root == null) {
root = new BTreeNode(val, null, null);
return true;
}
//root 已经创建,插入的值最终会到达底部,然后插进去
BTreeNode node = searchSurroundNode(val);
int rank = node.search(val);
if (rank != -1 && node.value.get(rank) == val) { // 该节点存在该值,返回插入失败
return false;
} else { // 值将会插入该关键码
addNodeForBtNode(node, rank, val);
split(node);
return true;
}
} private void split(BTreeNode node) {
while (node.value.size() >= degree) {
// 1.取中数
int midIndex = node.value.size() / 2;
BTreeNode rightNode = new BTreeNode();
for (int i = midIndex + 1; i < node.value.size(); i++) {
rightNode.addValue(node.value.remove(i));
if (i == midIndex + 1) {
rightNode.child.add(node.child.remove(i));
}
rightNode.child.add(node.child.remove(i));
}
for (BTreeNode rn : rightNode.child) {
if (rn != null) {
rn.parent = rightNode;
}
} // 移除原节点记得移除对应它的子节点
int insertValue = node.value.remove(midIndex);
if (node.parent != null) { // 存在父节点,把分裂点添加在父节点上
node.parent.addValue(insertValue);
/*
* 对插入的节点的子节点进行处理
* 1.得出插入点的index
* 2.左边子节点连接原node,右节点连接 rightNode
*/
int indexInValue = node.parent.getIndexInValue(insertValue);
node.parent.child.add(indexInValue + 1, rightNode);
rightNode.parent = node.parent;
node = node.parent;
} else { // 不存在父节点,并且当前节点溢出
root = new BTreeNode(insertValue, node, rightNode);
break;
}
}
} public boolean delete(int val) {
//node 为要删除的val所在的节点
BTreeNode node = search(val);
if (node != null) {
int rank = node.getIndexInValue(val);
// 找到继承结点并代替
if (node.child.get(0) != null) { //非底部节点
BTreeNode bottom = node.child.get(rank + 1);
while (bottom.child.get(0) != null) {
bottom = bottom.child.get(0);
}
node.value.set(rank, bottom.value.get(0));
bottom.value.set(0, val);
node = bottom;
rank = 0;
}
// 此时 node 一定是外部节点了(最底层)
node.value.remove(rank);
node.child.remove(rank + 1);
// 由于删除了某个值,所以需要从兄弟中借一个来拼凑(旋转)
// 当兄弟自己已到达下限,与父类合并成更大的节点,原来父节点所在的节点有可能-1后
// 导致又达到了下限,然后循环
solveUnderflow(node);
return true;
}
return false;
} /**
* 下溢的节点 :
* - 外部节点
* - 非外部节点
*
* @param node 下溢的节点
*/
public void solveUnderflow(BTreeNode node) {
//没有达到下溢的条件
int condition = (degree + 1) / 2;
if (node.child.size() >= condition) {
return;
}
BTreeNode parent = node.parent;
if (parent == null) { //到了根节点
if (node.value.size() == 0 && node.child.get(0) != null) {
root = node.child.get(0);
root.parent = null;
node.child.set(0, null);
}
return;
}
int rank = node.getIndexFromParent(); //旋转
if (rank > 0 && parent.child.get(rank - 1).child.size() > condition) { //左旋转,从左兄弟拿一个
BTreeNode ls = parent.child.get(rank - 1);
node.addValue(0, parent.value.remove(rank - 1));
parent.addValue(rank - 1, ls.value.remove(ls.value.size() - 1));
/*
* 被取走的节点可能存在子节点,需要放在新的位置
* 有可能上一次进行合并操作中,父节点的关键码为空了,
* 但是父节点还存在子节点(不为null)
*/
node.child.add(0, ls.child.remove(ls.child.size() - 1));
if (node.child.get(0) != null) {
node.child.get(0).parent = node;
}
return;
} else if (rank < parent.child.size() - 1 && parent.child.get(rank + 1).child.size() > condition) { //右旋转,从右兄弟拿一个
BTreeNode rs = parent.child.get(rank + 1);
node.addValue(parent.value.remove(rank));
parent.addValue(rs.value.remove(0)); node.child.add(node.child.size(), rs.child.remove(0));
if (node.child.lastElement() != null) {
node.child.lastElement().parent = node;
}
return;
} // 合并
if (rank > 0) { // 左合并
BTreeNode ls = parent.child.get(rank - 1);
//父类节点转入到左节点
ls.addValue(ls.value.size(), parent.value.remove(rank - 1));
parent.child.remove(rank);
//当前节点转入到左节点
ls.child.add(ls.child.size(), node.child.remove(0));
if (ls.child.get(ls.child.size() - 1) != null) {
ls.child.get(ls.child.size() - 1).parent = ls;
}
// 当前节点有可能value为空,但是child不为空。
// value 为空不移动,不为空移动
while (node.value.size() != 0) {
ls.addValue(node.value.remove(0));
ls.child.add(ls.child.size(), node.child.remove(0));
if (ls.child.get(ls.child.size() - 1) != null) {
ls.child.get(ls.child.size() - 1).parent = ls;
}
} } else { //右合并,有可能 rank = 0
BTreeNode rs = parent.child.get(rank + 1);
//父类节点转入到右节点
rs.addValue(0, parent.value.remove(rank));
//父类节点断开与当前节点的连接
parent.child.remove(rank);
//当前节点转入到右节点
rs.child.add(0, node.child.remove(0));
if (rs.child.get(0) != null) {
rs.child.get(0).parent = rs;
}
while (node.value.size() != 0) {
rs.addValue(0, node.value.remove(0));
rs.child.add(0, node.child.remove(0));
if (rs.child.get(0) != null) {
rs.child.get(0).parent = rs;
}
} }
solveUnderflow(parent);
} public int height() {
int h = 1;
BTreeNode node = root;
while (node != null) {
if (node.child.get(0) != null) {
h++;
node = node.child.get(0);
} else {
break;
}
}
return h;
} }

最后是一个测试的方法。

package BTree;

public class BTreeTest {
public static void main(String[] args) {
BTree tree = new BTree(3);
tree.insert(53);
tree.insert(97);
tree.insert(36);
tree.insert(89);
tree.insert(41);
tree.insert(75);
tree.insert(19);
tree.insert(84);
tree.insert(77);
tree.insert(79);
tree.insert(51);
// System.out.println(tree.height());
// tree.insert(23);
// tree.insert(29);
// tree.insert(45);
// tree.insert(87);
// System.out.println("-------------");
System.out.println("插入节点以后的树的高度 : "+tree.height());
System.out.println("-------------");
// tree.delete(41);
// tree.delete(75);
// tree.delete(84);
// tree.delete(51); tree.delete(36);
tree.delete(41);
System.out.println("删除节点以后的树的高度 : "+tree.height()); }
}

参考资料

  • 邓俊辉老师的数据结构课程

数据结构(三)--- B树(B-Tree)的更多相关文章

  1. Linux 内核中的数据结构:基数树(radix tree)

    转自:https://www.cnblogs.com/wuchanming/p/3824990.html   基数(radix)树 Linux基数树(radix tree)是将指针与long整数键值相 ...

  2. 数据结构之树(Tree)(一) :树

    ps:好久没用动手写blog了,要在这条路上不断发展,就需要不停的学习,不停的思考与总结,当把写blog作为一种习惯,就是自我成长的证明,Fighting!. 一.简介 树是一种重要的非线性数据结构, ...

  3. SDUT 3375 数据结构实验之查找三:树的种类统计

    数据结构实验之查找三:树的种类统计 Time Limit: 400MS Memory Limit: 65536KB Submit Statistic Problem Description 随着卫星成 ...

  4. Python入门篇-数据结构树(tree)的遍历

    Python入门篇-数据结构树(tree)的遍历 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.遍历 迭代所有元素一遍. 二.树的遍历 对树中所有元素不重复地访问一遍,也称作扫 ...

  5. Python入门篇-数据结构树(tree)篇

    Python入门篇-数据结构树(tree)篇 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.树概述 1>.树的概念 非线性结构,每个元素可以有多个前躯和后继 树是n(n& ...

  6. 数据结构(三) 树和二叉树,以及Huffman树

    三.树和二叉树 1.树 2.二叉树 3.遍历二叉树和线索二叉树 4.赫夫曼树及应用 树和二叉树 树状结构是一种常用的非线性结构,元素之间有分支和层次关系,除了树根元素无前驱外,其它元素都有唯一前驱. ...

  7. SDUT-3375_数据结构实验之查找三:树的种类统计

    数据结构实验之查找三:树的种类统计 Time Limit: 400 ms Memory Limit: 65536 KiB Problem Description 随着卫星成像技术的应用,自然资源研究机 ...

  8. 线段树 Interval Tree

    一.线段树 线段树既是线段也是树,并且是一棵二叉树,每个结点是一条线段,每条线段的左右儿子线段分别是该线段的左半和右半区间,递归定义之后就是一棵线段树. 例题:给定N条线段,{[2, 5], [4, ...

  9. 数据结构与算法->树->2-3-4树的查找,添加,删除(Java)

    代码: 兵马未动,粮草先行 作者: 传说中的汽水枪 如有错误,请留言指正,欢迎一起探讨. 转载请注明出处. 目录 一. 2-3-4树的定义 二. 2-3-4树数据结构定义 三. 2-3-4树的可以得到 ...

  10. 『线段树 Segment Tree』

    更新了基础部分 更新了\(lazytag\)标记的讲解 线段树 Segment Tree 今天来讲一下经典的线段树. 线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间 ...

随机推荐

  1. HDP Spark2 HIVE3.1 的问题

    HDP 上安装了 Hive3.1 和 Spark2, 提交 Spark 作业时,报找不到 Hive 中表的问题 但是查一了下 hive 表,明明是存在这个表的.查看日志,注意到如下的一段日志. 没修改 ...

  2. php 递归数据,三维数组转换二维

    public function sortarea($area, $parent_id = 0, $lev = 1){ static $list; foreach($area as $v){ if($v ...

  3. Memcached安装教程及使用

    Memcached Memcached 是一个高性能的分布式内存对象缓存系统,用于动态Web应用以减轻数据库负载 Table of contents 安装 使用 在spring中使用 安装 下载下来m ...

  4. Asp.net的生命周期应用之IHttpModule和IHttpHandler

    摘自:http://www.cnblogs.com/JimmyZhang/archive/2007/11/25/971878.html 从 Http 请求处理流程 一文的最后的一幅图中可以看到,在Ht ...

  5. 链式二叉树的实现(Java)

    定义树节点: package 链式二叉树; public class TreeNode { private Object data; private TreeNode left; private Tr ...

  6. Linux修改profile文件改错了,恢复的方法

    Linux修改profile文件改错了,恢复的方法在改profile的时候,改出问题了,除了cd以外的命令基本都不能用了,连vi都不能用了,上网查了下,用export PATH=/usr/bin:/u ...

  7. javaweb Servlet接收Android请求,并返回json数据

    1.实现功能 (1)接收http请求 (2)获取Android客户端发送的参数对应的内容 (3)hibernate查询数据库 (4)返回json数据 2.java代码 import EntityCla ...

  8. 【Alpha】Phylab 展示博客

    目录 Phylab Alpha 展示博客 一.团队简介 二.项目目标 2.1 典型用户 2.2 功能描述 2.3 用户量 三.项目发布与展示 3.1 新功能 3.2 修复缺陷 3.3 问题与限制 3. ...

  9. 升级vue-cli为 cli3 并创建项目

    一.升级npm install -g @vue/cli 二.创建项目 1.vue  create vue3-project 下面会提示让你配置下自己想要用到的功能,然后它会自动帮你安装,这个看自己需求 ...

  10. Smarty <= 3.1.32 Remote Code execution(CVE-2017-1000480)

    Smarty介绍   smarty是一个php模板引擎,其项目地址:https://github.com/smarty-php/smarty 测试环境搭建   下载:https://github.co ...