引言:

  天不生仲尼,万古如长夜。在计算机科学中,也有一个划时代的发明,B树(多路平衡查找树)及其变体(B树,b*树,b+树);

由德国科学家(鲁道夫·拜尔 Rudolf Bayer),美国科学家(爱德华·M·麦克特 Edward Meyers McCreight)于1970年共同发明;

B树这种数据结构特别适合用于数据库与文件系统设计中,是人类精神财富的精华部分,B树不诞生,计算机在处理大数据量计算时会变得非常困难。

用途:

  基本上都是软件产品最底层的,最核心的功能。

如:各种操作系统(windows,Linux,Mac)的文件系统索引,各种数据库(sqlserver、oracle、mysql、MongoDB、等等),

基本上大部分与大数据量读取有关的事务,多少都与B树家族有关,因为B树的优点太明显,特别是读取磁盘数据效率非常的高效,

查找效率O(log n),甚至在B+树中查询速度恒定,无论多少存储多少数据,查询任何一个速度都一样。简直就是天才的发明。

诞生的原因:

  在上世纪时期,计算机内存储器都非常的小,以KB为单位,比起现在动不动以G计算,简直小的可怜。

计算机运算数据时,数据是在内存中进行操作的,比如一些加减乘除、正删改查等。

举个简单的栗子:从一个数组 int a[1,2,3,4,5,6,7,8,9]中找出3,那非常简单;大概步骤如下:

  1、在内存中初始化这个数组

  2、获取数组指针遍历这个数组,查到3就完成

  但是这个数组很大,比如包含1亿个数字怎么办?如果数组容量大大超过内存大小,那这种比较就不现实了。现在的做法都是把文件

数据存放在外存储器,比如磁盘,U盘,光盘;然后把文件分多次的拷贝数据至内存进行操作。但是读取外存储器效率对比读取内存,

差距是非常大的,一般是百万级别的差距,差6个数量级,所以这个问题不解决一切都是空谈。

  好在操作系统在设计之初,就对读取外存储器进行了一定的优化,引入了“逻辑块”概念,当做操作文件的最小单元,而B树合理地利用这个“逻辑块”

功能开发的高效存储数据结构;在介绍B树特性之前,先来了解一下磁盘的基本工作原理。

磁盘简单介绍:

1)磁盘结构介绍

  网上引用的两张图,将就看看,基本结构是:磁盘 > 盘面 > 磁道 > 扇区

  左边是物理图,这个大家应该都是经常见到了,一般圆形的那部分有很多层,每一层叫盘片;右边的是示意图,代表左图的一个盘面。

每个盘面有跟多环形的磁道,每个磁道有若干段扇区组成,扇区是磁盘的最小组成单元,若干段扇区组成簇(也叫磁盘块、逻辑块等)

先看看我电脑的磁盘簇与扇区大小

  可以看到我的E盘每个扇区512个字节,每个簇4096字节,这个先记下来,后边有用到

扇区是磁盘组成的最小单元,簇是虚拟出来的,主要是为了操作系统方便读写磁盘;由于扇区比较小,数量非常多,

在寻址比较麻烦,操作系统就将相邻的几个扇区组合在一起,形成簇,再以簇为每次操作文件的最小单元。比如加载一个磁盘文件内容,

操作系统是分批次读取,每次只拷贝一个簇的单位数据,我的电脑就是一次拷贝4096字节,知道文件全部拷贝完成。

2)读写速度

  磁盘读取时间是毫秒级别的一般几毫秒到十几毫秒之间,这个跟磁盘转速有点关系,还有就是数据所在磁道远近有关系;

CPU处理时间是纳秒级别,毫秒:纳秒 = 1:1000000,所以在程序设计中,读取文件是时间成本非常高的,应该尽量合理设计;

B树简介(维基百科):

  B树(英语:B-tree)是一种自平衡的树,能够保持数据有序。这种数据结构能够让查找数据、顺序访问、插入数据及删除的动作,

都在对数时间内完成。B树,概括来说是一个一般化的二叉查找树(binary search tree)一个节点可以拥有最少2个子节点。

与自平衡二叉查找树不同,B树适用于读写相对大的数据块的存储系统,例如磁盘。B树减少定位记录时所经历的中间过程,从而加快存取速度。

B树这种数据结构可以用来描述外部存储。这种数据结构常被应用在数据库和文件系统的实现上。

  一个 m 阶的B树是一个有以下特性:

  • 每一个节点最多有 m 个子节点
  • 每一个非叶子节点(除根节点)最少有 ⌈m/2⌉ 个子节点
  • 如果根节点不是叶子节点,那么它至少有两个子节点
  • 有 k 个子节点的非叶子节点拥有 k − 1 个键
  • 所有的叶子节点都在同一层

  好吧,上边这一段看了等于没看的定义可以不看,这里有个重要的B树特性需要了解,就是B树的阶,对于阶的定义国内外是有分歧的,有的定义为度

阶指的是节点的最大孩子数,度指的是节点的最小孩子数,我查阅了很多资料,基本上可以理解为:

1度 = 2阶,比如说3度B树,可以理解为6阶B树。这点有些疑问,有更好的说法的可以留言讨论一下。

1)内部节点:

  内部节点是除叶子节点和根节点之外的所有节点。每个内部节点拥有最多 U 个,最少 L 个子节点。元素的数量总是比子节点指针的数量少1。

U 必须等于 2L 或者 2L-1。这个L一般是度数。

2)根节点:根节点拥有的子节点数量的上限和内部节点相同,但是没有下限。

3)叶子节点:叶子节点对元素的数量有相同的限制,但是没有子节点,也没有指向子节点的指针。

4)为了分析方便举例3阶3层B树

                        图1 

从上图中可以得出以下几个信息:

  • 红色数字标示整个节点(即3、6在同一个节点内,图中总共9个节点),黑色数字表示每个节点内的键值。
  • 所有数据插入B树后,都是从左到右顺序排列,从根节点开始,节点左边孩子键值都小于节点键值,右边孩子键值都大于节点键值。
  • 树的阶数指的是每个节点的最大孩子节点数,图中最多孩子节点数为3,即阶数=3,键值数量最少为:1,最大为:阶数 -1

数据检索分析:

  依据上图分析,因为整棵树已经在内存中,相当于一个变量,数据检索首先是从根节点开始;

1)如果要查询9,首先从根节点比较,那比较一次就得到结果,

2)如果要查询第二层的3、4,首先判断根节点键值,没有匹配到,但是可以判断要检索的键值比根节点小,

   所以接下来是从左孩子树继续检索,12、15也是类似,总共需要2次比较就得到结果

3)如果查询叶子节点键值,类似2),只需要3次比较就能得到结果。

4)对比普通的数组遍历查询,B树检索的时间成本没有随数据量增加而线性增加,效率大大提高。

B树的应用分析:

  前面已经提到,如果树已经在内存中,那当然好办,直接遍历就好了。如果B树仅仅如此,那也和数组差别不大,同样受限于内存大小;

所以,在内存中创建整棵B树是不现实的,这不是B树的正确打开方式。

  前面也已经提到,操作系统加载磁盘文件的时候,如果文件超过大小(即4096个字节),那会分多次的读取磁盘,直到拷贝数据完成。

这里看似一个加载动作,其实这个动作包含了N次磁盘寻址,而我们已经知道,每次磁盘寻址直至拷贝数据开销是非常大的;是CPU指令耗时百万倍以上;

这种操作应该尽量少地执行,而B树这种数据结构就是为了解决磁盘读取瓶颈这个问题而产生的。

  实际应用中,B树会持久化到磁盘,然后只在内存保留一个根节点的指针。已上图1为例:

  每个节点大小刚好等于大小,这样只需一次磁盘IO就可以获取到一整个节点的所有键值,及其所有子树的指针。

比如,查询键值8:

  1)第一步,读取根节点得到键值9,以及2个子树指针,分别指向左右孩子节点,因为9 > 8,所以下一步加载左孩子节点

  2)第二部,加载节点2,得到键值3、6,以及3个子树指针,因为3、6 < 8,所以下一步要加载节点2的右孩子节点

  3)第三部,加载节点6,得到键值7、8,因为是叶子节点所以没有子树指针,遍历键值匹配到8,返回。

总结:

  在这个3阶3层的B树中,无论查找哪一个键值,最多只需要3次磁盘操作,就算平均每次耗时10毫秒,总共需要耗时30毫秒(CPU运算耗时可以忽略);

以此类推,3阶4层的B树,需要读取4次磁盘,耗时40毫秒,5层50毫秒,6层60毫秒,7层,8层,,,,

  这样一看貌似也没什么,几十毫秒已经不能说快了,但是别忘了我们这颗树只有3阶,即一个节点保存2个键值。一个簇最多能有4096/4=1024个键值;

如果创建一个1024阶的B树,分别控制在3、4、5层的话,根据B树高度公式:,H为层数,T为1024,n为数据总数

耗时如下:

  3阶3层:能容纳2147483648(20亿)个键值,检索耗时也将30毫秒内

  3阶4层:能容纳2147483648(20亿) ~ 2199023255552(2兆亿)个键值,检索耗时也将40毫秒内,当然这已经超出键值表达范围了

  3阶5层:不可思议。。。

  当然实际运用当中达不到1024阶,因为树持久化到磁盘时,索引结构体一般都是超过4个字节,比如12个字节,那一个簇最多能有4096/12=341个键值。

如果阶数按341来算:

  3阶3层:能容纳79303642(7千万)个键值,检索耗时也将30毫秒内

  3阶4层:能容纳79303642(7千万) ~ 27042541922(200亿)个键值,检索耗时也将40毫秒内

  也是非常多了。。

B树简单示例:

1)首先,我们把B树基本信息定义出来

 public class Consts
{
public const int M = ; // B树的最小度数
public const int KeyMax = * M - ; // 节点包含关键字的最大个数
public const int KeyMin = M - ; // 非根节点包含关键字的最小个数
public const int ChildMax = KeyMax + ; // 孩子节点的最大个数
public const int ChildMin = KeyMin + ; // 孩子节点的最小个数
}

先写个简单的demo,因为最小度数为3,那就是6阶。先实现几个简单的方法,新增,拆分,其余的合并,删除比较复杂以后有机会再看看

2)定义BTreeNode,B树节点

     public class BTreeNode
{
private bool leaf;
public int[] keys;
public int keyNumber;
public BTreeNode[] children;
public int blockIndex;
public int dataIndex; public BTreeNode(bool leaf)
{
this.leaf = leaf;
keys = new int[Consts.KeyMax];
children = new BTreeNode[Consts.ChildMax];
} /// <summary>在未满的节点中插入键值</summary>
/// <param name="key">键值</param>
public void InsertNonFull(int key)
{
var index = keyNumber - ; if (leaf == true)
{
// 找到合适位置,并且移动节点键值腾出位置
while (index >= && keys[index] > key)
{
keys[index + ] = keys[index];
index--;
} // 在index后边新增键值
keys[index + ] = key;
keyNumber = keyNumber + ;
}
else
{
// 找到合适的子孩子索引
while (index >= && keys[index] > key) index--; // 如果孩子节点已满
if (children[index + ].keyNumber == Consts.KeyMax)
{
// 分裂该孩子节点
SplitChild(index + , children[index + ]); // 分裂后中间节点上跳父节点
// 孩子节点已经分裂成2个节点,找到合适的一个
if (keys[index + ] < key) index++;
} // 插入键值
children[index + ].InsertNonFull(key);
}
} /// <summary>分裂节点</summary>
/// <param name="childIndex">孩子节点索引</param>
/// <param name="waitSplitNode">待分裂节点</param>
public void SplitChild(int childIndex, BTreeNode waitSplitNode)
{
var newNode = new BTreeNode(waitSplitNode.leaf);
newNode.keyNumber = Consts.KeyMin; // 把待分裂的节点中的一般节点搬到新节点
for (var j = ; j < Consts.KeyMin; j++)
{
newNode.keys[j] = waitSplitNode.keys[j + Consts.ChildMin]; // 清0
waitSplitNode.keys[j + Consts.ChildMin] = ;
} // 如果待分裂节点不是也只节点
if (waitSplitNode.leaf == false)
{
for (var j = ; j < Consts.ChildMin; j++)
{
// 把孩子节点也搬过去
newNode.children[j] = waitSplitNode.children[j + Consts.ChildMin]; // 清0
waitSplitNode.children[j + Consts.ChildMin] = null;
}
} waitSplitNode.keyNumber = Consts.KeyMin; // 拷贝一般键值到新节点
for (var j = keyNumber; j >= childIndex + ; j--)
children[j + ] = children[j]; children[childIndex + ] = newNode;
for (var j = keyNumber - ; j >= childIndex; j--)
keys[j + ] = keys[j]; // 把中间键值上跳至父节点
keys[childIndex] = waitSplitNode.keys[Consts.KeyMin]; // 清0
waitSplitNode.keys[Consts.KeyMin] = ; // 根节点键值数自加
keyNumber = keyNumber + ;
} /// <summary>根据节点索引顺序打印节点键值</summary>
public void PrintByIndex()
{
int index;
for (index = ; index < keyNumber; index++)
{
// 如果不是叶子节点, 先打印叶子子节点.
if (leaf == false) children[index].PrintByIndex(); Console.Write("{0} ", keys[index]);
} // 打印孩子节点
if (leaf == false) children[index].PrintByIndex();
} /// <summary>查找某键值是否已经存在树中</summary>
/// <param name="key">键值</param>
/// <returns></returns>
public BTreeNode Find(int key)
{
int index = ;
while (index < keyNumber && key > keys[index]) index++; // 该key已经存在, 返回该索引位置节点
if (keys[index] == key) return this; // key 不存在,并且节点是叶子节点
if (leaf == true) return null; // 递归在孩子节点中查找
return children[index].Find(key);
}
}

3)B树模型

     public class BTree
{
public BTreeNode Root { get; private set; } public BTree() { } /// <summary>根据节点索引顺序打印节点键值</summary>
public void PrintByIndex()
{
if (Root == null)
{
Console.WriteLine("空树");
return;
} Root.PrintByIndex();
} /// <summary>查找某键值是否已经存在树中</summary>
/// <param name="key">键值</param>
/// <returns></returns>
public BTreeNode Find(int key)
{
if (Root == null) return null; return Root.Find(key);
} /// <summary>新增B树节点键值</summary>
/// <param name="key">键值</param>
public void Insert(int key)
{
if (Root == null)
{
Root = new BTreeNode(true);
Root.keys[] = key;
Root.keyNumber = ;
return;
} if (Root.keyNumber == Consts.KeyMax)
{
var newNode = new BTreeNode(false); newNode.children[] = Root;
newNode.SplitChild(, Root); var index = ;
if (newNode.keys[] < key) index++; newNode.children[index].InsertNonFull(key);
Root = newNode;
}
else
{
Root.InsertNonFull(key);
}
}
}

4)新增20个无序键值,测试一下

             var bTree = new BTree();

             bTree.Insert();
bTree.Insert();
bTree.Insert();
bTree.Insert();
bTree.Insert();
bTree.Insert();
bTree.Insert();
bTree.Insert();
bTree.Insert();
bTree.Insert();
bTree.Insert();
bTree.Insert();
bTree.Insert();
bTree.Insert();
bTree.Insert();
bTree.Insert();
bTree.Insert();
bTree.Insert();
bTree.Insert();
bTree.Insert(); Console.WriteLine("输出排序后键值");
bTree.PrintByIndex();

5)运行

B树持久化:

  上文提到,B数不可能只存在内存而无法落地,那样没有意义。所以就需要将整棵树持久化到磁盘文件,并且还要支持快速地从磁盘文件中检索到键值;

要持久化就要考虑很多问题,像上边的简单示例是没有实际意义的,因为节点不可能只有键值与孩子树,还得有数据指针,存储位置等等,大概有以下一些问题:

  • 如何保存每个节点占有字节数刚好等于一个簇大小(4096字节),因为这样就符合一次IO操作的数据交换上限?
  • 如何保存每个节点的所有键值,以及这个节点下属所有子树关系?
  • 如何保存每个键值对应的数据指针地址,以及指针与键值的对应关系如何维持?
  • 如何保证内存与磁盘的数据交换中能够正确地还原树结构,即重建树的某部分层级与键值和子树的关系?
  • 等等。。

  问题比较多,非常麻烦。具体的过程就不列举了,以下展示以下修改后的B树模型。

1、先定义一个结构体

 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = )]
public struct BlockItem
{
public int ChildBlockIndex;
public int Key;
public int DataIndex; public BlockItem(int key, int dataIndex)
{
ChildBlockIndex = -;
Key = key;
DataIndex = dataIndex;
}
}

  结构体总共12字节,为了能够持久化整棵B树到磁盘,加入了ChildBlockIndex子孩子节点块索引,根据这个块索引在下一次重建子孩子树层级关系时就知道从

文件的那个位置开始读取;Key键值,DataIndex数据索引,数据索引也是一个文件位置记录,跟ChildBlockIndex差不多,这样检索到key后就知道从

文件哪个位置获取真正的数据。为了更形象了解B树应用,我画了一个结构体的示意图:

0、总共3个节点,每个节点由N个结构体组成,最末尾只有孩子指针,没有数据与键值

1、黄色为子树块索引,即ChildBlockIndex,指向这个子孩子树所有数据在文件中的位置

2、红色为键值,即Key,键值一般是唯一的,不允许重复

3、蓝色为数据块索引,即DataIndex,指向键值对应的数据在文件中的什么位置开始,然后读取一个结构体的长度即可

4、底下绿色的一块是数据指针指向的具体数据块

2、数据结构体

 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = )]
public struct SDataTest
{
public int Idx;
public int Age;
public byte Sex; [MarshalAs(UnmanagedType.ByValArray, SizeConst = )]
public byte[] Name; public byte Valid;
};

3、B树节点类修改改一下,这个就不解释了,复习一下程序员基本功,啃代码。

     public class BTreeNode
{
private BTree tree;
private bool leaf; public int keyNumber;
public BlockItem[] keys;
public BTreeNode[] children; public int blockIndex;
public int findIndex; public BTreeNode(BTree tree, bool leaf)
{
this.tree = tree;
this.leaf = leaf;
keys = new BlockItem[Consts.KeyMax];
children = new BTreeNode[Consts.ChildMax];
blockIndex = Consts.BlockIndex++;
} /// <summary>在未满的节点中插入键值</summary>
/// <param name="key">键值</param>
public void InsertNonFull(BlockItem item)
{
var index = keyNumber - ; if (leaf == true)
{
// 找到合适位置,并且移动节点键值腾出位置
while (index >= && keys[index].Key > item.Key)
{
keys[index + ] = keys[index];
index--;
} // 在index后边新增键值
keys[index + ] = item;
keyNumber = keyNumber + ;
}
else
{
// 找到合适的子孩子索引
while (index >= && keys[index].Key > item.Key) index--; // 如果孩子节点已满
if (children[index + ].keyNumber == Consts.KeyMax)
{
// 分裂该孩子节点
SplitChild(index + , children[index + ]); // 分裂后中间节点上跳父节点
// 孩子节点已经分裂成2个节点,找到合适的一个
if (keys[index + ].Key < item.Key) index++;
} // 插入键值
children[index + ].InsertNonFull(item);
}
} /// <summary>分裂节点</summary>
/// <param name="childIndex">孩子节点索引</param>
/// <param name="waitSplitNode">待分裂节点</param>
public void SplitChild(int childIndex, BTreeNode waitSplitNode)
{
var newNode = new BTreeNode(tree, waitSplitNode.leaf);
newNode.keyNumber = Consts.KeyMin; // 把待分裂的节点中的一般节点搬到新节点
for (var j = ; j < Consts.KeyMin; j++)
{
newNode.keys[j] = waitSplitNode.keys[j + Consts.ChildMin]; // 清0
waitSplitNode.keys[j + Consts.ChildMin] = default(BlockItem);
} // 如果待分裂节点不是也只节点
if (waitSplitNode.leaf == false)
{
for (var j = ; j < Consts.ChildMin; j++)
{
// 把孩子节点也搬过去
newNode.children[j] = waitSplitNode.children[j + Consts.ChildMin]; // 清0
waitSplitNode.children[j + Consts.ChildMin] = null;
}
} waitSplitNode.keyNumber = Consts.KeyMin; for (var j = keyNumber; j >= childIndex + ; j--)
children[j + ] = children[j]; children[childIndex + ] = newNode; for (var j = keyNumber - ; j >= childIndex; j--)
keys[j + ] = keys[j]; // 把中间键值上跳至父节点
keys[childIndex] = waitSplitNode.keys[Consts.KeyMin]; // 清0
waitSplitNode.keys[Consts.KeyMin] = default(BlockItem); // 根节点键值数自加
keyNumber = keyNumber + ;
} /// <summary>根据节点索引顺序打印节点键值</summary>
public void PrintByIndex()
{
int index;
for (index = ; index < keyNumber; index++)
{
// 如果不是叶子节点, 先打印叶子子节点.
if (leaf == false) children[index].PrintByIndex(); Console.Write("{0} ", keys[index].Key);
} // 打印孩子节点
if (leaf == false) children[index].PrintByIndex();
} /// <summary>查找某键值是否已经存在树中</summary>
/// <param name="item">键值</param>
/// <returns></returns>
public BTreeNode Find(BlockItem item)
{
findIndex = ;
int index = ;
while (index < keyNumber && item.Key > keys[index].Key) index++; // 遍历全部都未找到,索引计数减1
if (index > && index == keyNumber) index--; // 该key已经存在, 返回该索引位置节点
if (keys[index].Key == item.Key)
{
findIndex = index;
return this;
} // key 不存在,并且节点是叶子节点
if (leaf == true) return null; // 重建children[index]数据结构
var childBlockIndex = keys[index].ChildBlockIndex;
tree.LoadNodeByBlock(ref children[index], childBlockIndex); // 递归在孩子节点中查找
if (children[index] == null) return null;
return children[index].Find(item);
}
}

4、B树模型也要修改一下 ,不解释

     public class BTree
{
private FileStream rwFS; public BTreeNode Root; public BTree(string fullName)
{
rwFS = new FileStream(fullName, FileMode.OpenOrCreate, FileAccess.ReadWrite); // 创建10M的空间,用做索引存储
if (rwFS.Length == )
{
rwFS.SetLength(Consts.IndexTotalSize);
} // 从数据文件重建根节点,内存只保存根节点
LoadNodeByBlock(ref Root, );
} public void LoadNodeByBlock(ref BTreeNode node, int blockIndex)
{
var items = Helper.Read(rwFS,blockIndex);
if (items.Count > )
{
var isLeaf = items[].ChildBlockIndex == Consts.NoChild; node = new BTreeNode(this, isLeaf);
node.blockIndex = blockIndex;
node.keys = items.ToArray();
node.keyNumber = items.Count;
}
} /// <summary>根据节点索引顺序打印节点键值</summary>
public void PrintByIndex()
{
if (Root == null)
{
Console.WriteLine("空树");
return;
} Root.PrintByIndex();
} /// <summary>查找某键值是否已经存在树中</summary>
/// <param name="item">键值</param>
/// <returns></returns>
public BTreeNode Find(BlockItem item)
{
if (Root == null) return null; return Root.Find(item);
}
public BTreeNode Find(int key)
{
return Find(new BlockItem() { Key = key });
} /// <summary>新增B树节点键值</summary>
/// <param name="item">键值</param>
private void Insert(BlockItem item)
{
if (Root == null)
{
Root = new BTreeNode(this, true);
Root.keys[] = item;
Root.keyNumber = ;
}
else
{
if (Root.keyNumber == Consts.KeyMax)
{
var newNode = new BTreeNode(this, false); newNode.children[] = Root;
newNode.SplitChild(, Root); var index = ;
if (newNode.keys[].Key < item.Key) index++; newNode.children[index].InsertNonFull(item);
Root = newNode;
}
else
{
Root.InsertNonFull(item);
}
}
} public void Insert(SDataTest data)
{
var item = new BlockItem()
{
Key = data.Idx
}; var node = Find(item);
if (node != null)
{
Console.WriteLine("键值已经存在,info:{0}", item.Key);
return;
} // 保存数据
item.DataIndex = Helper.InsertData(rwFS, data); // 保存索引
if (item.DataIndex >= )
Insert(item);
} /// <summary>持久化整棵树</summary>
public void SaveIndexAll()
{
SaveIndex(Root);
} /// <summary>持久化某节点以下的树枝</summary>
/// <param name="node">某节点</param>
public void SaveIndex(BTreeNode node)
{
var bw = new BinaryWriter(rwFS);
var keyItem = default(BlockItem); // 第一层
var nodeL1 = node;
if (nodeL1 == null) return; for (var i = ; i <= nodeL1.keyNumber; i++)
{
keyItem = default(BlockItem);
if (i < nodeL1.keyNumber) keyItem = nodeL1.keys[i]; SaveIndex(bw, , i, nodeL1.children[i], keyItem); // 第二层
var nodeL2 = nodeL1.children[i];
if (nodeL2 == null) continue; for (var j = ; j <= nodeL2.keyNumber; j++)
{
keyItem = default(BlockItem);
if (j < nodeL2.keyNumber) keyItem = nodeL2.keys[j]; SaveIndex(bw, nodeL2.blockIndex, j, nodeL2.children[j], keyItem); // 第三层
var nodeL3 = nodeL2.children[j];
if (nodeL3 == null) continue; for (var k = ; k <= nodeL3.keyNumber; k++)
{
keyItem = default(BlockItem);
if (k < nodeL3.keyNumber) keyItem = nodeL3.keys[k]; SaveIndex(bw, nodeL3.blockIndex, k, nodeL3.children[k], keyItem); // 第四层
var nodeL4 = nodeL3.children[k];
if (nodeL4 == null) continue; for (var l = ; l <= nodeL4.keyNumber; l++)
{
keyItem = default(BlockItem);
if (l < nodeL4.keyNumber) keyItem = nodeL4.keys[l]; SaveIndex(bw, nodeL4.blockIndex, l, nodeL4.children[l], keyItem); // 第五层
var nodeL5 = nodeL4.children[l];
if (nodeL5 == null) continue; for (var z = ; z <= nodeL5.keyNumber; z++)
{
keyItem = default(BlockItem);
if (z < nodeL5.keyNumber) keyItem = nodeL5.keys[z]; SaveIndex(bw, nodeL5.blockIndex, z, nodeL5.children[z], keyItem);
}
}
}
}
}
}
private void SaveIndex(BinaryWriter bw, int blockIndex, int num, BTreeNode node, BlockItem item)
{
bw.Seek((blockIndex * Consts.BlockSize) + (num * Consts.IndexSize), SeekOrigin.Begin);
bw.Write(node == null ? Consts.NoChild : node.blockIndex);
bw.Write(item.Key);
bw.Write(item.DataIndex);
bw.Flush();
} public SDataTest LoadData(int dataIndex)
{
return Helper.Load(rwFS, dataIndex);
}
}

5、写测试

 private static void InsertTest(ref BTree bTree)
{
// 新增测试数据
for (int i = ; i <= Consts.TotalKeyNumber; i++)
{
bTree.Insert(new SDataTest()
{
Idx = i,
Age = i,
Sex = ,
Name = Helper.Copy("Name(" + i.ToString() + ")", ),
Valid =
});
} Console.WriteLine("测试数据添加完毕,共新增{0}条数据", Consts.TotalKeyNumber);
}

6、读测试

 private static void FindTest(ref BTree bTree)
{
var count = ; // 校验数据查找
for (int i = ; i <= Consts.TotalKeyNumber; i++)
{
var node = bTree.Find(i);
if (node == null)
{
//Console.WriteLine("未找到{0}", i);
continue;
} //Console.WriteLine("findIndex:{0},key:{1},dataIndex:{2}", node.findIndex, node.keys[node.findIndex].Key, node.keys[node.findIndex].DataIndex); count++;
if (count % == )
{
var data = bTree.LoadData(node.keys[node.findIndex].DataIndex);
var name = Encoding.Default.GetString(data.Name).TrimEnd('\0');
Console.WriteLine("Idx:{0},Age:{1},Sex:{2},Name:{3},Valid:{4}", data.Idx, data.Age, data.Sex, name, data.Valid);
}
} Console.WriteLine("有效数据个数:{0}", count);
}

7、最后测试一下

8、测试查询时间

 private static void CheckLoadTime(ref BTree bTree, int key)
{
var start = DateTime.Now;
var node = bTree.Find(key);
if (node == null) return; Console.WriteLine("查找{0},耗时:{1}", key.ToString(), (DateTime.Now - start).TotalMilliseconds.ToString()); var data = bTree.LoadData(node.keys[node.findIndex].DataIndex);
var name = Encoding.Default.GetString(data.Name).TrimEnd('\0');
Console.WriteLine("Idx:{0},Age:{1},Sex:{2},Name:{3},Valid:{4}", data.Idx, data.Age, data.Sex, name, data.Valid);
Console.WriteLine();
}
      CheckLoadTime(ref bTree, );
CheckLoadTime(ref bTree, );
CheckLoadTime(ref bTree, );
CheckLoadTime(ref bTree, );

9、重新生成10000000条数据,测试查询效率

      CheckLoadTime(ref bTree, );
CheckLoadTime(ref bTree, );
CheckLoadTime(ref bTree, );
CheckLoadTime(ref bTree, );
CheckLoadTime(ref bTree, );
CheckLoadTime(ref bTree, );

全是1毫秒内返回,数据检索效率非常高,

学习历程:

  实际上最初在学校潦草学了一遍【数据结构】之后,工作那么多年都用不着这方面的知识点,早就忘得一干二净了。

重新引起我兴趣的是2017年下半年,当时一个项目需要用到共享内存作为快速读写数据的底层核心功能。在设计共享内存存储关系时,

就遇到了索引的快速检索要求,第一次是顺序检索,当数据量达到5万以上时系统就崩了,检索速度太慢;后来改为二分查找法,轻松达到20万数据;

达到20万后就差不多到了单机处理性能瓶颈了,因为CPU不够用,除了检索还需要做其他的业务计算;

  那时候就一直在搜索快速查找的各种算法,什么快速排序算法、堆排序算法、归并排序、二分查找算法、DFS(深度优先搜索)、BFS(广度优先搜索),

基本上都了解了一遍,但是看得头疼,没去实践。最后看到树结构,引起我很大兴趣,就是园友nullzx的这篇:B+树在磁盘存储中的应用

这让我了解到原来数据库是这样读写的,这很有意思,得造个轮子自己试一次

  粗陋仓促写成,恐怕有很多地方有漏洞,所以如果文中有错误的地方,欢迎留言讨论,但是拒绝一波流的吐槽,我可是会删低级评论的。

B树概述与简单应用示例(C#)的更多相关文章

  1. Linux概述及简单命令

    Linux概述及简单命令 转自https://www.cnblogs.com/ayu305/p/Linux_basic.html 一.准备工作 1.环境选择:VMware\阿里云服务器 2.Linux ...

  2. 背水一战 Windows 10 (9) - 资源: 资源限定符概述, 资源限定符示例

    [源码下载] 背水一战 Windows 10 (9) - 资源: 资源限定符概述, 资源限定符示例 作者:webabcd 介绍背水一战 Windows 10 之 资源 资源限定符概述 资源限定符示例 ...

  3. 【java开发系列】—— spring简单入门示例

    1 JDK安装 2 Struts2简单入门示例 前言 作为入门级的记录帖,没有过多的技术含量,简单的搭建配置框架而已.这次讲到spring,这个应该是SSH中的重量级框架,它主要包含两个内容:控制反转 ...

  4. Springmvc整合tiles框架简单入门示例(maven)

    Springmvc整合tiles框架简单入门示例(maven) 本教程基于Springmvc,spring mvc和maven怎么弄就不具体说了,这边就只简单说tiles框架的整合. 先贴上源码(免积 ...

  5. hadoop环境安装及简单Map-Reduce示例

    说明:这篇博客来自我的csdn博客,http://blog.csdn.net/lxxgreat/article/details/7753511 一.参考书:<hadoop权威指南--第二版(中文 ...

  6. EasyHook远注简单监控示例 z

    http://www.csdn 123.com/html/itweb/20130827/83559_83558_83544.htm 免费开源库EasyHook(inline hook),下面是下载地址 ...

  7. Web Service简单入门示例

    Web Service简单入门示例     我们一般实现Web Service的方法有非常多种.当中我主要使用了CXF Apache插件和Axis 2两种. Web Service是应用服务商为了解决 ...

  8. Ext简单demo示例

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/stri ...

  9. GDAL中MEM格式的简单使用示例

    GDAL库中提供了一种内存文件格式--MEM.如何使用MEM文件格式,主要有两种,一种是通过别的文件使用CreateCopy方法来创建一个MEM:另外一种是图像数据都已经存储在内存中了,然后使用内存数 ...

随机推荐

  1. ios webp转换jpg

    在项目开发的过程中,遇到了一个问题,就是webp的图片,先解释一下webp是啥,webp是谷歌开发的一种旨在加快图片加载速度的图片格式.图片压缩体积大约只有JPEG的2/3,说白了就是省空间,特别对于 ...

  2. 【Java】Java中的final关键字和static

    0.概述 final关键字表示是不可变的: 下面分别从属性(字段).方法.类中进行说明: 1.属性(or字段),表示常量 final声明在属性(or字段)中,表示常量,有两种初始化方法,1是在声明时直 ...

  3. 图解leetcode —— 395. 至少有K个重复字符的最长子串

    前言: 每道题附带动态示意图,提供java.python两种语言答案,力求提供leetcode最优解. 描述: 找到给定字符串(由小写字符组成)中的最长子串 T , 要求 T 中的每一字符出现次数都不 ...

  4. Magicodes.Sms短信库的封装和集成

    简介 Magicodes.Sms是心莱团队封装的短信服务库,已提供Abp模块的封装. Nuget 新的包 名称 说明 Nuget Magicodes.Sms.Aliyun 阿里云短信库 Magicod ...

  5. 如何运用DDD - 领域服务

    目录 如何运用DDD - 领域服务 概述 什么是领域服务 从实际场景下手 更贴近现实 领域服务VS应用服务 扩展上面的需求 最常见的认证授权是领域服务吗 使用领域服务 不要过多的使用领域服务 不要将过 ...

  6. 在一个数组中,除了两个数外,其余数都是两两成对出现,找出这两个数,要求时间复杂度O(n),空间复杂度O(1)

    题目:在一个数组中,除了两个数外,其余数都是两两成对出现,找出这两个数,要求时间复杂度O(n),空间复杂度O(1) 分析:这道题考察位操作:异或(^),按位与(&),移位操作(>> ...

  7. 2019 AI Bootcamp Guangzhou 参会日记

    2019年的全球AI训练营在北京.上海.广州.杭州.宁波五个地方同时举办! 12月14日,微软全球AI Bootcamp活动再次驾临广州,本次会议结合 ML.NET 和基于 SciSharp 社区介绍 ...

  8. 笔记||Python3之对象与变量

    什么是对象?什么是变量? 在python中,一切都是对象,一切都是对象的引用. 变量相当于数学中的等式,比如xy = 20 .在编程中变量还可以是任意数据类型. 对象是分配的一块内存,有足够的空间去表 ...

  9. JavaScript 逻辑与(&&) 与 逻辑或(||) 运算规则

    逻辑与(&&) 逻辑与(&&)操作可以应用于任何的操作类型,不仅仅是布尔值, 在有一个操作数不是布尔值的情况下,&&操作符就不一定返回布尔值:遵循下面规 ...

  10. java开发中常用的Liunx操作命令

    查看所有端口的占用情况 netstat -nultp 其中State值为LISTEN则表示已经被占用 查看某个端口的占用情况: netstat -anp |grep 端口号 在liunx中启动tomc ...