- 浏览: 24620 次
- 来自: 北京
文章分类
最新评论
写在前面:搞了SQL Server时间也不短了,对B树的概念也算是比较了解。去网上搜也搜不到用C#或java实现的B树,干脆自己写一个。实现B树的过程中也对很多细节有了更深的了解。
简介
B树是一种为辅助存储设计的一种数据结构,在1970年由R.Bayer和E.mccreight提出。在文件系统和数据库中为了减少IO操作大量被应用。遗憾的是,他们并没有说明为什么取名为B树,但按照B树的性质来说B通常被解释为Balance。在国内通常有说是B-树,其实并不存在B-树,只是由英文B-Tree直译成了B-树。
一个典型的 B树如图1所示。
图1.一个典型的B树
符合如下特征的树才可以称为B树:
- 根节点如果不是叶节点,则至少需要两颗子树
- 每个节点中有N个元素,和N+1个指针。每个节点中的元素不得小于最大节点容量的1/2
- 所有的叶子位于同一层级(这也是为什么叫平衡树)
- 父节点元素向左的指针必须小于节点元素,向右的指针必须大于节点元素,比如图1中Q的左指针必须小于Q,右指针必须大于Q
为什么要使用B树
在计算机系统中,存储设备一般分为两种,一种为主存(比如说CPU二级缓存,内存等),主存一般由硅制成,速度非常快,但每一个字节的成本往往高于辅助存储设备很多。还有一类是辅助存储(比如硬盘,磁盘等),这种设备通常容量会很大,成本也会低很多,但是存取速度非常的慢,下面我们来看一下最常见的辅存--硬盘。
硬盘作为主机中除了唯一的一个机械存储设备,速度远远落后于CPU和内存。图2是一个典型的磁盘驱动器。
图2.典型的磁盘驱动器工作原理
一个驱动器包含若干盘片,以一定的速度绕着主轴旋转(比如PC常见的转速是7200RPM,服务器级别的有10000RPM和15000RPM的),每个盘片表面覆盖一个可磁化的物质.每个盘片利用摇臂末端的磁头进行读写。摇臂是物理连接在一起的,通过移动远离或贴近主轴。
因为有机械移动的部分,所以磁盘的速度相比内存而言是非常的慢。这个机械移动包括两个部分:盘旋转和磁臂移动。仅仅对于盘旋转来说,比如常见的7200RPM的硬盘,转一圈需要60/7200≈8.33ms,换句话说,让磁盘完整的旋转一圈找到所需要的数据需要8.33ms,这比内存常见的100ns慢100000倍左右,这还不包括移动摇臂的时间。
因为机械移动如此的花时间,磁盘会每次读取多个数据项。一般来说最小单位为簇。而对于SQL Server来说,则为一页(8K)。
但由于要查找的数据往往很大,不能全部装入主存。需要磁盘来辅助存储。而读取磁盘则是占处理时间最重要的一部分,所以如果我们尽可能的减少对磁盘的IO操作,则会大大加快速度。这也是B树设计的初衷。
B树通过将根节点放入主存,其它所有节点放入辅存来大大减少对于辅存IO的操作。比如图1中,我如果想查找元素Y,仅仅需要从主存中取得根节点,再根据根节点的右指针做一次IO读,再根据这个节点最右的指针做一次IO读,就可以找到元素Y。相比其他数据结构,仅仅做两次辅存IO读大大减少了查找的时间。
B树的高度
根据上面的例子我们可以看出,对于辅存做IO读的次数取决于B树的高度。而B树的高度由什么决定的呢?
其中T为度数(每个节点包含的元素个数),N为总元素个数.
我们可以看出T对于树的高度有决定性的影响。因此如果每个节点包含更多的元素个数,在元素个数相同的情况下,则更有可能减少B树的高度。这也是为什么SQL Server中需要尽量以窄键建立聚集索引。因为SQL Server中每个节点的大小为8092字节,如果减少键的大小,则可以容纳更多的元素,从而减少了B树的高度,提升了查询的性能。
上面B树高度的公式也可以进行推导得出,将每一层级的的元素个数加起来,比如度为T的节点,根为1个节点,第二层至少为2个节点,第三层至少为2t个节点,第四层至少为2t*t个节点。将所有最小节点相加,从而得到节点个数N的公式:
两边取对数,则可以得到树的高度公式。
这也是为什么开篇所说每个节点必须至少有两个子元素,因为根据高度公式,如果每个节点只有一个元素,也就是T=1的话,那么高度将会趋于正无穷。
B树的实现
讲了这么多概念,该到实现B树的时候了。
首先需要定义B树的节点,如代码1所示。
public class TreeNode<T>where T:IComparable<T> { public int elementNum = 0;//元素个数 public IList<T> Elements = new List<T>();//元素集合,存在elementNum个 public IList<TreeNode<T>> Pointer = new List<TreeNode<T>>();//元素指针,存在elementNum+1 public bool IsLeaf = true;//是否为叶子节点 }
代码1.声明节点
我给每个节点四个属性,分别为节点包含的元素个数,节点的元素数组,节点的指针数组和节点是否为叶子节点。我这里对节点存储的元素类型使用了泛型T,并且必须实现ICompable接口使得节点所存储的元素可以互相比较。
有了节点的定义后,就可以创建B树了,如代码2所示。
//创建一个b树,也是类的构造函数 public BTree() { RootNode = new TreeNode<T>(); RootNode.elementNum = 0; RootNode.IsLeaf = true; //将节点写入磁盘,做一次IO写 }
代码2.初始化B树
这是BTree类的构造函数,初始化一个根节点。全部代码我稍后给出。
下面则要考虑B树的插入,其实B树的构建过程也是向B树插入元素的过程.B树的插入相对来说比较复杂,需要考虑很多因素。
首先,每一个节点可容纳的元素个数是一样并且有限的,这里我声明了一个常量最为每个节点,如代码3所示。
const int NumPerNode = 4;
代码3.设置每个节点最多容纳的元素个数
对于B树来说,节点增加的唯一方式就是节点分裂,这个概念和SQL SERVER中的页分裂是一样的。
页分裂的过程首先需要生成新页,然后将大概一半的元素移动到新页中,然后将中间元素提升到父节点。比如我想在现有的元素中插入8,造成已满的页进行分裂,如图3所示:
图3.向已经满的叶子节点插入元素会造成页分裂
通过叶子分裂的概念不难看出,叶子节点分裂才会造成非叶子节点元素的增加。最终传递到根元素。而根元素的分裂是树长高的唯一途径。
在C#中的实现代码如代码4所示。
//B树中的节点分裂 public void BTreeSplitNode(TreeNode<T> FatherNode, int position, TreeNode<T> NodeToBeSplit) { TreeNode<T> newNode = new TreeNode<T>();//创建新节点,容纳分裂后被移动的元素 newNode.IsLeaf = NodeToBeSplit.IsLeaf;//新节点的层级和原节点位于同一层 newNode.elementNum = NumPerNode - (NumPerNode / 2 + 1);//新节点元素的个数大约为分裂节点的一半 for (int i = 1; i < NumPerNode - (NumPerNode / 2 + 1); i++) { //将原页中后半部分复制到新页中 newNode.Elements[i - 1] = NodeToBeSplit.Elements[i + NumPerNode / 2]; } if (!NodeToBeSplit.IsLeaf)//如果不是叶子节点,将指针也复制过去 { for (int j = 1; j < NumPerNode / 2 + 1; j++) { newNode.Pointer[j - 1] = NodeToBeSplit.Pointer[NumPerNode / 2]; } } NodeToBeSplit.elementNum = NumPerNode / 2;//原节点剩余元素个数 //将父节点指向子节点的指针向后推一位 for (int k = FatherNode.elementNum + 1; k > position + 1; k--) { FatherNode.Pointer[k] = FatherNode.Pointer[k - 1]; } //将父节点的元素向后推一位 for (int k = FatherNode.elementNum; k > position + 1; k--) { FatherNode.Elements[k] = FatherNode.Elements[k - 1]; } //将被分裂的页的中间节点插入父节点 FatherNode.Elements[position - 1] = NodeToBeSplit.Elements[NumPerNode / 2]; //父节点元素大小+1 FatherNode.elementNum += 1; //将FatherNode,NodeToBeSplit,newNode写回磁盘,三次IO写操作 }
代码4.分裂节点
通过概念和代码不难看出,节点的分裂相对比较消耗IO,这也是为什么SQL Server中需要一些最佳实现比如不用GUID做聚集索引,或是设置填充因子等来减少页分裂。
而如果需要插入元素的节点不满,则不需要页分裂,则需要从根开始查找,找到需要被插入的节点,如代码5所示。
//在节点非满时寻找插入节点 public void BTreeInsertNotFull(TreeNode<T> Node, T KeyWord) { int i=Node.elementNum; //如果是叶子节点,则寻找合适的位置直接插入 if (Node.IsLeaf) { while (i >= 1 && KeyWord.CompareTo(Node.Elements[i - 1]) < 0) { Node.Elements[i] = Node.Elements[i - 1];//所有的元素后推一位 i -= 1; } Node.Elements[i - 1] = KeyWord;//将关键字插入节点 Node.elementNum += 1; //将节点写入磁盘,IO写+1 } //如果是非叶子节点 else { while (i >= 1 && KeyWord.CompareTo(Node.Elements[i - 1]) < 0) { i -= 1; } //这步将指针所指向的节点读入内存,IO读+1 if (Node.Pointer[i].elementNum == NumPerNode) { //如果子节点已满,进行节点分裂 BTreeSplitNode(Node, i, Node.Pointer[i]); } if (KeyWord.CompareTo(Node.Elements[i - 1]) > 0) { //根据关键字的值决定插入分裂后的左孩子还是右孩子 i += 1; } //迭代找叶子,找到叶子节点后插入 BTreeInsertNotFull(Node.Pointer[i], KeyWord); } }
代码5.插入
通过代码5可以看出,我们没有进行任何迭代。而是从根节点开始遇到满的节点直接进行分裂。从而减少了性能损失。
再将根节点分裂的特殊情况考虑进去,我们从而将插入操作合为一个函数,如代码6所示。
public void BtreeInsert(T KeyWord) { if (RootNode.elementNum == NumPerNode) { //如果根节点满了,则对跟节点进行分裂 TreeNode<T> newRoot = new TreeNode<T>(); newRoot.elementNum = 0; newRoot.IsLeaf = false; //将newRoot节点变为根节点 BTreeSplitNode(newRoot, 1, RootNode); //分裂后插入新根的树 BTreeInsertNotFull(newRoot, KeyWord); //将树的根进行变换 RootNode = newRoot; } else { //如果根节点没有满,直接插入 BTreeInsertNotFull(RootNode, KeyWord); } }
代码6.插入操作
现在,我们就可以通过插入操作,来实现一个B树了。
B树的查找
既然B树生成好了,我们就可以对B树进行查找了。B树的查找实现相对简单,仅仅是从跟节点进行迭代,如果找到元素则返回节点和位置,如果找不到则返回NULL.
//从B树中搜索节点,存在则返回节点和元素在节点的值,否则返回NULL public returnValue<T> BTreeSearch(TreeNode<T> rootNode, T keyword) { int i = 1; while (i <= rootNode.elementNum && keyword.CompareTo(rootNode.Elements[i - 1])>0) { i = i + 1; } if (i <= rootNode.elementNum && keyword.CompareTo(rootNode.Elements[i - 1]) == 0) { returnValue<T> r = new returnValue<T>(); r.node = rootNode.Pointer[i]; r.position = i; return r; } if (rootNode.IsLeaf) { return null; } else { //从磁盘将内容读出来,做一次IO读 return BTreeSearch(rootNode.Pointer[i], keyword); } }
代码7.对B树进行查找
顺带说一下,returnValue类仅仅是对返回值的一个封装,代码如代码8所示。
public class returnValue<T> where T : IComparable<T> { public TreeNode<T> node; public int position; }
代码8.returnValue的代码
总结
本文从B树的概念原理,以及为什么需要B树到B树的实现来阐述B树的概念。B树是一种非常优雅的数据结构。是关系数据库和文件系统的核心算法。对于B树的了解会使得你对于数据库的学习更加系统和容易。
相关推荐
B树,全称为平衡多路查找树(Balanced Multiway Search Tree),是一种自平衡的树数据结构,广泛应用于数据库和文件系统中,特别是在构建索引时。B树的设计旨在减少磁盘I/O操作,因为磁盘读写速度远慢于内存。在B树...
B+树是一种多路搜索树,它的每个节点可以有多个子节点。与普通二叉树不同,B+树的所有关键字都存储在叶子节点,而非叶子节点仅作为索引使用,这使得所有数据查询的路径长度相同,提高了数据访问效率。此外,B+树的...
B树是一种多路搜索树,每个节点可以有多个子节点,通常用m表示最大分支数。每个非叶节点最多含有m个子节点,并且至少有⌈m/2⌉个子节点。叶子节点是满的,即至少包含⌈m/2⌉个键,而根节点至少有两个子节点,除非它...
2. **多路树**:每个节点可以有超过两个子节点,如B树和B+树,常见于数据库索引。 3. **堆**:特殊的树形数据结构,满足堆属性(如最大堆或最小堆),常用于优先队列和排序算法。 4. **图**:虽然不是严格意义上的...
B树,全称平衡多路查找树(Balanced Multiway Search Tree),是一种自平衡的查找树,可以保持数据的有序性,并能支持快速的插入和删除操作。这里,我们将深入探讨B树的原理及其在图书管理系统中的应用。 一、B树的...
B+树是一种多路搜索树,每个节点可以有多个子节点,通常比二叉搜索树拥有更高的分支因子。它的主要特点包括: 1. 分层结构:B+树具有分层的节点,分为叶子节点和非叶子节点。所有的叶子节点都在同一层,非叶子节点...
B树(B-tree),则是一种自平衡的多路搜索树,最初由R. Bayer和E. McCreight在数据库领域提出。B树的特点是每个节点可以有多个子节点,这些子节点按照键值排序,并且每个节点都存储部分键和指向子节点的指针。B树的...
- **B树(B-Tree)**:多路搜索树,广泛应用于数据库和文件系统中。 #### 6. **图(Graph)** - **无向图(Undirected Graph)**:边没有方向的图。 - **有向图(Directed Graph)**:边具有方向性的图。 - **图的遍历...
15.4 接口的实现 .182 15.5 抽象类与接口 .195 15.6 小 结 .196 第十六章 组织应用程序 .198 16.1 基 本 概 念 .198 16.2 使用名字空间 .200 16.3 使用指示符 .203 16.4 程 序 示 例 .206 16.5 小 ...
- _B树(B-Tree)_: 广泛用于数据库和文件系统的多路搜索树。 - **二叉树(Binary Tree)**: 特殊类型的树,每个节点最多有两个子节点。 - _二叉搜索树(Binary Search Tree)_: 对于任意节点,其左子树的所有节点的值...
B树(B-tree)是一种自平衡的多路搜索树,特别适合于大型数据存储,如磁盘或网络环境。它的特点是每个节点可以有多个子节点,这使得B树在插入、删除和查找操作时具有较高的效率,特别是对于大规模数据集。B树的这种...
7. **B树的生长过程.swf**:B树是一种自平衡的多路搜索树,适合大量数据的存储系统,如数据库和文件系统。其生长过程展示了节点的分裂和合并,以保持平衡。 8. **邻接表表示的图的广度优先遍历.swf**:图的遍历是...
B树是一种自平衡的多路搜索树,能够保持数据排序。其特点是每个节点可以有多个子节点,通常2到32个,这使得B树可以在磁盘等慢速存储介质上保持高效的查找性能。B树的关键特性包括: 1. **分层结构**:B树由根节点、...
- **B树**:多路平衡查找树,适用于磁盘等慢速存储设备。 - **B+树**:B树的变体,所有数据都存储在叶子节点上。 **9. 红黑树** 红黑树是一种自平衡的二叉查找树,通过维护五个简单的性质来保证查找的高效性。 ##...
综上所述,生产计划书是企业运营的核心,它整合了市场、质量、生产、资源和安全管理等多个方面,旨在通过科学的规划实现企业的高效运作和持续发展。在实际操作中,企业需要不断根据市场变化和内部情况进行调整,以...