引言:

  天不生仲尼,万古如长夜。在计算机科学中,也有一个划时代的发明,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. 【数据结构】之散列链表(Java语言描述)

    散列链表,在JDK中的API实现是 HashMap 类. 为什么HashMap被称为“散列链表”?这与HashMap的内部存储结构有关.下面将根据源码进行分析. 首先要说的是,HashMap中维护着的 ...

  2. v-bind和v-model的本质区别和作用域

    每篇一句 一场寂寞凭谁诉.算前言,总轻负. Vue视图数据展示方式和彼此的区别: {{插值表达式}} {{}}插值表达式里面 只能写表达式,不能写语句 文本输出,不会解析标签 不能作用在标签的属性上, ...

  3. Apple 应用内支付心得

    http://tank2308635.iteye.com/blog/1238687Apple 应用内支付 首先简要说一下IAP 流程 简要步骤说明: 用户进入购买虚拟物品页面,App从后台服务器获取产 ...

  4. jquery.countdown 倒计时插件的学习

    1.第一种简单的使用 第一个时间是你的倒计时截止时间,finalDate格式可以是YYYY/MM/DD MM/DD/YYYY YYYY/MM/DD hh:mm:ss MM/DD/YYYY hh:mm: ...

  5. 线性规划VB求解

    线性规划VB求解 Rem 定义动态数组 Dim a() As Single, c() As Single, b() As Single, cb() As Single Dim aa() As Sing ...

  6. XCode项目配置

    此设置优先级在playersetting之上,如果为空或者格式不正确或者文件不存在将不会设置,请注意 一.设置面板 二.对应Xcode中设置 1.TeamID  登录苹果开发者网站,查看个人信息,就有 ...

  7. 史上最全的linuxvi命令的总结

    第8章 linux编辑文件内容命令 8.1 vi命令 8.1.1 快速移动光标技巧 ID 快捷键 快捷键说明 1 G 将光标快速移动到最后一行 2 gg 将光标快速移动到行首 3 nG 将光标快速移动 ...

  8. 谈一谈AOP面向切面编程

    AOP是什么 : AOP面向切面编程他是一种编程思想,是指在程序运行期间,将某段代码动态的切入到指定方法的指定位置,将这种编程方式称为面向切面编程 AOP使用场景 : 日志 事务 使用AOP的好处是: ...

  9. java之单例设计模式

    什么是设计模式? 设计模式是在大量的实践中总结和理论化之后优选的代码结构.编程风格.以及解决问题的思考方式.设计模式就像是经典的棋谱,不同的棋局,我们用不同的棋谱,免去我们自己再思考和探索. 所谓单例 ...

  10. 大数据学习笔记——Java篇之网络编程基础

    Java网络编程学习笔记 1. 网络编程基础知识 1.1 网络分层图 网络分层分为两种模型:OSI模型以及TCP/IP网络模型,前者模型分为7层,是一个理论的,参考的模型:后者为实际应用的模型,具体对 ...