从B 树、B+ 树、B* 树谈到R 树


*.*  *.* *.* *.**.* *.*写的很到位,希望能帮助大家理解*.*  *.**.* *.**.* *.*

 

作者:July、weedge、Frankie。编程艺术室出品。

说明:本文从B树开始谈起,然后论述B+树、B*树,最后谈到R 树。其中B树、B+树及B*树部分由weedge完成,R 树部分由Frankie完成,全文最终由July统稿修订完成。

出处:http://blog.csdn.net/v_JULY_v 

第一节、B树、B+树、B*树

1.前言:

动态查找树主要有:二叉查找树(Binary Search Tree),平衡二叉查找树(Balanced Binary Search Tree),红黑树(Red-Black Tree ),B-tree/B+-tree/ B*-tree (B~Tree)。前三者是典型的二叉查找树结构,其查找的时间复杂度O(log2N)与树的深度相关,那么降低树的深度自然会提高查找效率。

但是咱们有面对这样一个实际问题:就是大规模数据存储中,实现索引查询这样一个实际背景下,树节点存储的元素数量是有限的(如果元素数量非常多的话,查找就退化成节点内部的线性查找了),这样导致二叉查找树结构由于树的深度过大而造成磁盘I/O读写过于频繁,进而导致查询效率低下(为什么会出现这种情况,待会在外部存储器-磁盘中有所解释),那么如何减少树的深度(当然是不能减少查询的数据量),一个基本的想法就是:采用多叉树结构(由于树节点元素数量是有限的,自然该节点的子树数量也就是有限的)。

也就是说,因为磁盘的操作费时费资源,如果过于频繁的多次查找势必效率低下。那么如何提高效率,即如何避免磁盘过于频繁的多次查找呢?根据磁盘查找存取的次数往往由树的高度所决定,所以,只要我们通过某种较好的树结构减少树的结构尽量减少树的高度,那么是不是便能有效减少磁盘查找存取的次数呢?那这种有效的树结构是一种怎样的树呢?

这样我们就提出了一个新的查找树结构——多路查找树。根据平衡二叉树的启发,自然就想到平衡多路查找树结构,也就是这篇文章所要阐述的第一个主题B~tree,即B树结构(后面,我们将看到,B树的各种操作能使B树保持较低的高度,从而达到有效避免磁盘过于频繁的查找存取操作,从而有效提高查找效率)。

B-tree(B-tree树即B树,B即Balanced,平衡的意思)这棵神奇的树是在Rudolf BayerEdward M. McCreight(1970)写的一篇论文《Organization and Maintenance of Large Ordered Indices》中首次提出的(wikipedia中:http://en.wikipedia.org/wiki/B-tree,阐述了B-tree名字来源以及相关的开源地址)。

在开始介绍B~tree之前,先了解下相关的硬件知识,才能很好的了解为什么需要B~tree这种外存数据结构。

2.外存储器磁盘

计算机存储设备一般分为两种内存储器(main memory)和外存储器(external memory) 内存存取速度快,但容量小,价格昂贵,而且不能长期保存数据(在不通电情况下数据会消失)

外存储器—磁盘是一种直接存取的存储设备(DASD)。它是以存取时间变化不大为特征的。可以直接存取任何字符组,且容量大、速度较其它外存设备更快。

2.1磁盘的构造

磁盘是一个扁平的圆盘(与电唱机的唱片类似)。盘面上有许多称为磁道的圆圈,数据就记录在这些磁道上。磁盘可以是单片的,也可以是由若干盘片组成的盘组,每一盘片上有两个面。如下图11.3中所示的6片盘组为例,除去最顶端和最底端的外侧面不存储数据之外,一共有10个面可以用来保存信息。

当磁盘驱动器执行读/写功能时。盘片装在一个主轴上,并绕主轴高速旋转,当磁道在读/写头(又叫磁头下通过时,就可以进行数据的读 / 写了。

一般磁盘分为固定头盘(磁头固定)和活动头盘。固定头盘的每一个磁道上都有独立的磁头,它是固定不动的,专门负责这一磁道上数据的读/写。

活动头盘 (如上图)的磁头是可移动的。每一个盘面上只有一个磁头(磁头是双向的,因此正反盘面都能读写)。它可以从该面的一个磁道移动到另一个磁道。所有磁头都装在同一个动臂上,因此不同盘面上的所有磁头都是同时移动的(行动整齐划一)。当盘片绕主轴旋转的时候,磁头与旋转的盘片形成一个圆柱体。各个盘面上半径相同的磁道组成了一个圆柱面,我们称为柱面 。因此,柱面的个数也就是盘面上的磁道数。

2.2磁盘的读/写原理和效率

磁盘上数据必须用一个三维地址唯一标示:柱面号、盘面号、块号(磁道上的盘块)

/写磁盘上某一指定数据需要下面3个步骤:

(1) 首先移动臂根据柱面号使磁头移动到所需要的柱面上,这一过程被称为定位或查找 

(2) 如上图11.3中所示的6盘组示意图中,所有磁头都定位到了10个盘面的10条磁道上(磁头都是双向的)。这时根据盘面号来确定指定盘面上的磁道。

(3) 盘面确定以后,盘片开始旋转,将指定块号的磁道段移动至磁头下。

经过上面三个步骤,指定数据的存储位置就被找到。这时就可以开始读/写操作了。

访问某一具体信息,由3部分时间组成:

● 查找时间(seek time) Ts: 完成上述步骤(1)所需要的时间。这部分时间代价最高,最大可达到0.1s左右。

● 等待时间(latency time) Tl: 完成上述步骤(3)所需要的时间。由于盘片绕主轴旋转速度很快,一般为7200/(电脑硬盘的性能指标之一家用的普通硬盘的转速一般有5400rpm(笔记本)7200rpm几种)因此一般旋转一圈大约0.0083s

● 传输时间(transmission time) Tt: 数据通过系统总线传送到内存的时间,一般传输一个字节(byte)大概0.02us=2*10^(-8)s

磁盘读取数据是以盘块(block)为基本单位的。位于同一盘块中的所有数据都能被一次性全部读取出来。而磁盘IO代价主要花费在查找时间Ts上。因此我们应该尽量将相关信息存放在同一盘块,同一磁道中。或者至少放在同一柱面或相邻柱面上,以求在读/写信息时尽量减少磁头来回移动的次数,避免过多的查找时间Ts

所以,在大规模数据存储方面,大量数据存储在外存磁盘中,而在外存磁盘中读取/写入块(block)中某数据时,首先需要定位到磁盘中的某块,如何有效地查找磁盘中的数据,需要一种合理高效的外存数据结构,就是下面所要重点阐述的B-tree结构,以及相关的变种结构:B+-tree结构和B*-tree结构。

3.B- 树

具体讲解之前,有一点,再次强调下:B-树,即为B树。因为B树的原英文名称为B-tree,而国内很多人喜欢把B-tree译作B-树,其实,这是个非常不好的直译,很容易让人产生误解。如人们可能会以为B-树是一种树,而B树又是一种一种树。而事实上是,B-tree就是指的B树。特此说明。

我们知道,B 树是为了磁盘或其它存储设备而设计的一种多叉(下面你会看到,相对于二叉,B树每个内结点有多个分支,即多叉)平衡查找树。与本blog之前介绍的红黑树很相似,但在降低磁盘I/0操作方面要更好一些。许多数据库系统都一般使用B树或者B树的各种变形结构,如下文即将要介绍的B+树,B*树来存储信息。

B树与红黑树最大的不同在于,B树的结点可以有许多子女,从几个到几千个。那为什么又说B树与红黑树很相似呢?因为与红黑树一样,一棵含n个结点的B树的高度也为O(lgn),但可能比一棵红黑树的高度小许多,应为它的分支因子比较大。所以,B树可以在O(logn)时间内,实现各种如插入(insert),删除(delete)等动态集合操作。

如下图所示,即是一棵B树,一棵关键字为英语中辅音字母的B树,现在要从树种查找字母R(包含n[x]个关键字的内结点x,x有n[x]+1]个子女(也就是说,一个内结点x若含有n[x]个关键字,那么x将含有n[x]+1个子女)。所有的叶结点都处于相同的深度,带阴影的结点为查找字母R时要检查的结点):

相信,从上图你能轻易的看到,一个内结点x若含有n[x]个关键字,那么x将含有n[x]+1个子女。如含有2个关键字D H的内结点有3个子女,而含有3个关键字Q T X的内结点有4个子女。

树又叫平衡多路查找树。一棵m阶的B 树 (m叉树)的特性如下

  1. 树中每个结点最多含有m个孩子(m>=2);
  2. 除根结点和叶子结点外,其它每个结点至少有[ceil(m / 2)]个孩子(其中ceil(x)是一个取上限的函数);
  3. 若根结点不是叶子结点,则至少有2个孩子(特殊情况:没有孩子的根结点,即根结点为叶子结点,整棵树只有一个根节点);
  4. 所有叶子结点都出现在同一层,叶子结点不包含任何关键字信息(可以看做是外部接点或查询失败的接点,实际上这些结点不存在,指向这些结点的指针都为null);(读者反馈@冷岳这里有错,叶子节点只是没有孩子和指向孩子的指针,这些节点也存在,也有元素。@JULY:其实,关键是把什么当做叶子结点,因为如红黑树中,每一个NULL指针即当做叶子结点,只是没画出来而已)。
  5. 每个非终端结点中包含有n个关键字信息: (n,P0,K1,P1,K2,P2,......,Kn,Pn)。其中:
    a) Ki (i=1...n)为关键字,且关键字按顺序升序排序K(i-1)< Ki。 
    b) Pi为指向子树根的接点,且指针P(i-1)指向子树种所有结点的关键字均小于Ki,但都大于K(i-1)。 
    c) 关键字的个数n必须满足: [ceil(m / 2)-1]<= n <= m-1。
    如下图所示:


针对上面第5点,再阐述下:B树中每一个结点能包含的关键字(如之前上面的D H和Q T X)数有一个上界和下界。这个下界可以用一个称作B树的最小度数(算法导论中文版上译作度数,最小度数即内节点中节点最小孩子数目)t(t>=2)表示。

  • 每个非根的结点必须至少含有t-1个关键字。每个非根的内结点至少有t个子女。如果树是非空的,则根结点至少包含一个关键字;
  • 每个结点可包含之多2t-1个关键字。所以一个内结点至多可有2t个子女。如果一个结点恰好有2t-1个关键字,我们就说这个结点是满的(而稍后介绍的B*树作为B树的一种常用变形,B*树中要求每个内结点至少为2/3满,而不是像这里的B树所要求的至少半满);
  • 当关键字数t=2(t=2的意思是,t min=2,t可以>=2)时的B树是最简单的 有很多人会因此误认为B树就是二叉查找树,但二叉查找树就是二叉查找树,B树就是B树,B树的真正最准确的定义为:一棵含有t(t>=2)个关键字的平衡多路查找树。每个内结点可能因此而含有2个、3个或4个子女,亦即一棵2-3-4树,然而在实际中,通常采用大得多的t值。

B树中的每个结点根据实际情况可以包含大量的关键字信息和分支(当然是不能超过磁盘块的大小,根据磁盘驱动(disk drives)的不同,一般块的大小在1k~4k左右);这样树的深度降低了,这就意味着查找一个元素只要很少结点从外存磁盘中读入内存,很快访问到要查找的数据。

B树的类型和节点定义如下图所示:

为了简单,这里用少量数据构造一棵3叉树的形式,实际应用中的B树结点中关键字很多的。上面的图中比如根结点,其中17比表示一个磁盘文件的文件名;小红方块表示这个17文件内容在硬盘中的存储位置;p1表示指向17左子树的指针。

其结构可以简单定义为:

typedef struct {

/*文件数*/

int file_num;

/*文件名(key)*/

char * file_name[max_file_num];

/*指向子节点的指针*/

BTNode * BTptr[max_file_num+1];

/*文件在硬盘中的存储位置*/

FILE_HARD_ADDR offset[max_file_num];

}BTNode;

假如每个盘块可以正好存放一个B树的结点(正好存放2个文件名)。那么一个BTNODE结点就代表一个盘块,而子树指针就是存放另外一个盘块的地址。

下面,咱们来模拟下查找文件29的过程:

  1. 根据根结点指针找到文件目录的根磁盘块1,将其中的信息导入内存。【磁盘 IO 操作 1次】
  2. 此时内存中有两个文件名17、 35 和三个存储其他磁盘页面地址的数据。根据算法我们发现17<29<35,因此我们找到指针 p2
  3. 根据 p2 指针,我们定位到磁盘块3,并将其中的信息导入内存。【磁盘 IO 操作 2次】
  4. 此时内存中有两个文件名26, 30 和三个存储其他磁盘页面地址的数据。根据算法我们发,26<29<30,因此我们找到指针 p2
  5. 根据 p2 指针,我们定位到磁盘块8,并将其中的信息导入内存。【磁盘 IO 操作 3次】
  6. 此时内存中有两个文件名28, 29 。根据算法我们查找到文,29,并定位了该文件内存的磁盘地址。

分析上面的过程,发现需要3次磁盘IO操作和3次内存查找操作。关于内存中的文件名查找,由于是一个有序表结构,可以利用折半查找提高效率。至于IO操作时影响整个B树查找效率的决定因素。

当然,如果我们使用平衡二叉树的磁盘存储结构来进行查找,磁盘4次,最多5次,而且文件越多,B树比平衡二叉树所用的磁盘IO操作次数将越少,效率也越高

4.B+-tree

B+-tree:是应文件系统所需而产生的一种B-tree的变形树。

一棵m阶的B+树和m阶的B树的差异在于:

1.n棵子树的结点中含有n个关键字; (而B 树n棵子树有n-1个关键字)

2.所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大的顺序链接。 (而B 树的叶子节点并没有包括全部需要查找的信息)

3.所有的非终端结点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键字。 (而B 树的非终节点也包含需要查找的有效信息)

<span="b-tree+-tree<span="b+-tree+-tree<span="b+-tree<span="bytes<span="io

b+="span>b+" strong>b) B+-tree的应用: VSAM(虚拟存储存取法)文件(来源论文 the ubiquitous Btree 作者:D COMER - 1979 )

5.B*-tree

B*-treeB+-tree的变体,在B+ 树非根和非叶子结点再增加指向兄弟的指针;B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3(代替B+树的1/2)。给出了一个简单实例,如下图所示:

B+树的分裂:当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增加新结点的指针;B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针。

B*树的分裂:当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针。

所以,B*树分配新结点的概率比B+树要低,空间使用率更高;

6、B树的插入、删除操作

上面 第3小节 简单介绍了利用B树这种结构如何访问外存磁盘中的数据的情况,下面咱们通过另外一个实例来对这棵B树的插入(insert),删除(delete)基本操作进行详细的介绍。
但在此之前,咱们还得简单回顾下一棵m阶的B 树 (m叉树)的特性,如下:
  1. 树中每个结点含有最多含有m个孩子,即m满足:ceil(m/2)<=m<=m。
  2. 除根结点和叶子结点外,其它每个结点至少有[ceil(m / 2)]个孩子(其中ceil(x)是一个取上限的函数);
  3. 若根结点不是叶子结点,则至少有2个孩子(特殊情况:没有孩子的根结点,即根结点为叶子结点,整棵树只有一个根节点);
  4. 所有叶子结点都出现在同一层,叶子结点不包含任何关键字信息(可以看做是外部接点或查询失败的接点,实际上这些结点不存在,指向这些结点的指针都为null);
  5. 每个非终端结点中包含有n个关键字信息: (n,P0,K1,P1,K2,P2,......,Kn,Pn)。其中:
    a) Ki (i=1...n)为关键字,且关键字按顺序升序排序K(i-1)< Ki。 
    b) Pi为指向子树根的接点,且指针P(i-1)指向子树种所有结点的关键字均小于Ki,但都大于K(i-1)。 
    c) 除根结点之外的结点的关键字的个数n必须满足: [ceil(m / 2)-1]<= n <= m-1(叶子结点也必须满足此条关于关键字数的性质,根结点除外)。

ok,下面咱们以一棵5阶(即树中任一结点至多含有4个关键字,5棵子树)B树实例进行讲解(如下图所示):

备注:

  1. 关键字数(2-4个)针对--非根结点(包括叶子结点在内),孩子数(3-5个)--针对根结点和叶子结点之外的内结点。当然,根结点是必须至少有2个孩子的,不然就成直线型搜索树了
  2. 曾在一次面试中被问到,一棵m阶的B树的最大高度是多少?答曰:log_ceil(m/2)N (上面中关于m阶B树的第1点特性已经提到:树中每个结点含有最多含有m个孩子,即m满足:ceil(m/2)<=m<=m。而树中每个结点含孩子数越少,树的高度则越大,故如此)。

下图中关键字为大写字母,顺序为字母升序。

结点定义如下:

typedef struct{

int Count; // 当前节点中关键元素数目

ItemType Key[4]; // 存储关键字元素的数组

long Branch[5]; // 伪指针数组,(记录数目)方便判断合并和分裂的情况

} NodeType;

6.1、插入(insert)操作

插入一个元素时,首先在B树中是否存在,如果不存在,即在叶子结点处结束,然后在叶子结点中插入该新的元素,注意:如果叶子结点空间足够,这里需要向右移动该叶子结点中大于新插入关键字的元素,如果空间满了以致没有足够的空间去添加新的元素,则将该结点进行“分裂”,将一半数量的关键字元素分裂到新的其相邻右结点中,中间关键字元素上移到父结点中(当然,如果父结点空间满了,也同样需要“分裂”操作),而且当结点中关键元素向右移动了,相关的指针也需要向右移。如果在根结点插入新元素,空间满了,则进行分裂操作,这样原来的根结点中的中间关键字元素向上移动到新的根结点中,因此导致树的高度增加一层。如下图所示:


1、OK,下面咱们通过一个实例来逐步讲解下。插入以下字符字母到一棵空的B 树中(非根结点关键字数小了(小于2个)就合并,大了(超过4个)就分裂):C N G A H E K Q M F W L T Z D P R X Y S,首先,结点空间足够,4个字母插入相同的结点中,如下图:

2、当咱们试着插入H时,结点发现空间不够,以致将其分裂成2个结点,移动中间元素G上移到新的根结点中,在实现过程中,咱们把A和C留在当前结点中,而H和N放置新的其右邻居结点中。如下图:

3、当咱们插入E,K,Q时,不需要任何分裂操作

4、插入M需要一次分裂,注意M恰好是中间关键字元素,以致向上移到父节点中

5、插入F,W,L,T不需要任何分裂操作

6、插入Z时,最右的叶子结点空间满了,需要进行分裂操作,中间元素T上移到父节点中,注意通过上移中间元素,树最终还是保持平衡,分裂结果的结点存在2个关键字元素。

7、插入D时,导致最左边的叶子结点被分裂,D恰好也是中间元素,上移到父节点中,然后字母P,R,X,Y陆续插入不需要任何分裂操作(别忘了,树中至多5个孩子)。

8、最后,当插入S时,含有N,P,Q,R的结点需要分裂,把中间元素Q上移到父节点中,但是情况来了,父节点中空间已经满了,所以也要进行分裂,将父节点中的中间元素M上移到新形成的根结点中,注意以前在父节点中的第三个指针在修改后包括D和G节点中。这样具体插入操作的完成,下面介绍删除操作,删除操作相对于插入操作要考虑的情况多点。

6.2、删除(delete)操作

首先查找B树中需删除的元素,如果该元素在B树中存在,则将该元素在其结点中进行删除,如果删除该元素后,首先判断该元素是否有左右孩子结点,如果有,则上移孩子结点中的某相近元素到父节点中,然后移动之后情况;如果没有,直接删除后,移动之后的情况

删除元素,移动相应元素之后,如果某结点中元素数目(即关键字数)小于ceil(m/2)-1,则需要看其某相邻兄弟结点是否丰满(结点中元素个数大于ceil(m/2)-1)(还记得第一节中关于B树的第5个特性中的c点么?: c)除根结点之外的结点(包括叶子结点)的关键字的个数n必须满足: (ceil(m / 2)-1)<= n <= m-1。m表示最多含有m个孩子,n表示关键字数。在本小节中举的一颗B树的示例中,关键字数n满足:2<=n<=4),如果丰满,则向父节点借一个元素来满足条件;如果其相邻兄弟都刚脱贫,即借了之后其结点数目小于ceil(m/2)-1,则该结点与其相邻的某一兄弟结点进行“合并”成一个结点,以此来满足条件。那咱们通过下面实例来详细了解吧。

以上述插入操作构造的一棵5阶B树(树中最多含有m(m=5)个孩子,因此关键字数最小为ceil(m / 2)-1=2。还是这句话,关键字数小了(小于2个)就合并,大了(超过4个)就分裂)为例,依次删除H,T,R,E。

1、首先删除元素H,当然首先查找H,H在一个叶子结点中,且该叶子结点元素数目3大于最小元素数目ceil(m/2)-1=2,则操作很简单,咱们只需要移动K至原来H的位置,移动L至K的位置(也就是结点中删除元素后面的元素向前移动)

2、下一步,删除T,因为T没有在叶子结点中,而是在中间结点中找到,咱们发现他的继承者W(字母升序的下个元素),将W上移到T的位置,然后将原包含W的孩子结点中的W进行删除,这里恰好删除W后,该孩子结点中元素个数大于2,无需进行合并操作。

3、下一步删除R,R在叶子结点中,但是该结点中元素数目为2,删除导致只有1个元素,已经小于最小元素数目ceil(5/2)-1=2,而由前面我们已经知道:如果其某个相邻兄弟结点中比较丰满(元素个数大于ceil(5/2)-1=2),则可以向父结点借一个元素,然后将最丰满的相邻兄弟结点中上移最后或最前一个元素到父节点中(有没有看到红黑树中左旋操作的影子?),在这个实例中,右相邻兄弟结点中比较丰满(3个元素大于2),所以先向父节点借一个元素W下移到该叶子结点中,代替原来S的位置,S前移;然后X在相邻右兄弟结点中上移到父结点中,最后在相邻右兄弟结点中删除X,后面元素前移。

4、最后一步删除E, 删除后会导致很多问题,因为E所在的结点数目刚好达标,刚好满足最小元素个数(ceil(5/2)-1=2,而相邻的兄弟结点也是同样的情况,删除一个元素都不能满足条件,所以需要该节点与某相邻兄弟结点进行合并操作;首先移动父结点中的元素(该元素在两个需要合并的两个结点元素之间)下移到其子结点中,然后将这两个结点进行合并成一个结点。所以在该实例中,咱们首先将父节点中的元素D下移到已经删除E而只有F的结点中,然后将含有D和F的结点和含有A,C的相邻兄弟结点进行合并成一个结点。

5、也许你认为这样删除操作已经结束了,其实不然,在看看上图,对于这种特殊情况,你立即会发现父节点只包含一个元素G,没达标(因为非根节点包括叶子结点的关键字数n必须满足于2=<n<=4,而此处的n=1),这是不能够接受的。如果这个问题结点的相邻兄弟比较丰满,则可以向父结点借一个元素。假设这时右兄弟结点(含有Q,X)有一个以上的元素(Q右边还有元素),然后咱们将M下移到元素很少的子结点中,将Q上移到M的位置,这时,Q的左子树将变成M的右子树,也就是含有N,P结点被依附在M的右指针上。所以在这个实例中,咱们没有办法去借一个元素,只能与兄弟结点进行合并成一个结点,而根结点中的唯一元素M下移到子结点,这样,树的高度减少一层。

为了进一步详细讨论删除的情况,再举另外一个实例

这里是一棵不同的5序B树,那咱们试着删除C

于是将删除元素C的右子结点中的D元素上移到C的位置,但是出现上移元素后,只有一个元素的结点的情况。

又因为含有E的结点,其相邻兄弟结点才刚脱贫(最少元素个数为2),不可能向父节点借元素,所以只能进行合并操作,于是这里将含有A,B的左兄弟结点和含有E的结点进行合并成一个结点。

这样又出现只含有一个元素F结点的情况,这时,其相邻的兄弟结点是丰满的(元素个数为3>最小元素个数2,这样就可以想父结点借元素了,把父结点中的J下移到该结点中,相应的如果结点中J后有元素则前移,然后相邻兄弟结点中的第一个元素(或者最后一个元素)上移到父节点中,后面的元素(或者前面的元素)前移(或者后移);注意含有K,L的结点以前依附在M的左边,现在变为依附在J的右边。这样每个结点都满足B树结构性质。

从以上操作可看出:除根结点之外的结点(包括叶子结点)的关键字的个数n满足:(ceil(m / 2)-1)<= n <= m-1,即2<=n<=4。这也佐证了咱们之前的观点。删除操作完。

7.总结

通过以上介绍,大致将B树,B+树,B*树总结如下:

B树:有序数组+平衡多叉树;

B+树:有序数组链表+平衡多叉树;

B*树:一棵丰满的B+树。

在大规模数据存储的文件系统中,B~tree系列数据结构,起着很重要的作用,对于存储不同的数据,节点相关的信息也是有所不同,这里根据自己的理解,画的一个查找以职工号为关键字,职工号为38的记录的简单示意图。(这里假设每个物理块容纳3个索引,磁盘的I/O操作的基本单位是块(block),磁盘访问很费时,采用B+树有效的减少了访问磁盘的次数。)

对于像MySQLDB2Oracle等数据库中的索引结构得有较深入的了解才行,建议去找一些B 树相关的开源代码研究。

走进搜索引擎的作者梁斌老师针对B树、B+树给出了他的意见(为了真实性,特引用其原话,未作任何改动): “B+树还有一个最大的好处,方便扫库,B树必须用中序遍历的方法按序扫库,而B+树直接从叶子结点挨个扫一遍就完了,B+树支持range-query非常方便,而B树不支持。这是数据库选用B+树的最主要原因。

比如要查 5-10之间的,B+树一把到5这个标记,再一把到10,然后串起来就行了,B树就非常麻烦。B树的好处,就是成功查询特别有利,因为树的高度总体要比B+树矮。不成功的情况下,B树也比B+树稍稍占一点点便宜。

B树比如你的例子中查,17的话,一把就得到结果了,
有很多基于频率的搜索是选用B树,越频繁query的结点越往根上走,前提是需要对query做统计,而且要对key做一些变化。

另外B树也好B+树也好,根或者上面几层因为被反复query,所以这几块基本都在内存中,不会出现读磁盘IO,一般已启动的时候,就会主动换入内存。”非常感谢。

Bucket Li:"mysql 底层存储是用B+树实现的,知道为什么么。内存中B+树是没有优势的,但是一到磁盘,B+树的威力就出来了"。

第二节、R树:处理空间存储问题

相信经过上面第一节的介绍,你已经对B树或者B+树有所了解。这种树可以非常好的处理一维空间存储的问题。B树是一棵平衡树,它是把一维直线分为若干段线段,当我们查找满足某个要求的点的时候,只要去查找它所属的线段即可。依我看来,这种思想其实就是先找一个大的空间,再逐步缩小所要查找的空间,最终在一个自己设定的最小不可分空间内找出满足要求的解。一个典型的B树查找如下:

要查找某一满足条件的点,先去找到满足条件的线段,然后遍历所在线段上的点,即可找到答案。

B树是一种相对来说比较复杂的数据结构,尤其是在它的删除与插入操作过程中,因为它涉及到了叶子结点的分解与合并。由于本文第一节已经详细介绍了B树和B+树,下面直接开始介绍我们的第二个主角:R树。

简介

1984年,加州大学伯克利分校的Guttman发表了一篇题为“R-trees: a dynamic index structure for spatial searching”的论文,向世人介绍了R树这种处理高维空间存储问题的数据结构。本文便是基于这篇论文写作完成的,因此如果大家对R树非常有兴趣,我想最好还是参考一下原著吧:)。为表示对这位牛人的尊重,给个引用先:

Guttman, A.; “R-trees: a dynamic index structure for spatial searching,” ACM, 1984, 14

R树在数据库等领域做出的功绩是非常显著的。它很好的解决了在高维空间搜索等问题。举个R树在现实领域中能够解决的例子吧:查找20英里以内所有的餐厅。如果没有R树你会怎么解决?一般情况下我们会把餐厅的坐标(x,y)分为两个字段存放在数据库中,一个字段记录经度,另一个字段记录纬度。这样的话我们就需要遍历所有的餐厅获取其位置信息,然后计算是否满足要求。如果一个地区有100家餐厅的话,我们就要进行100次位置计算操作了,如果应用到谷歌地图这种超大数据库中,我想这种方法肯定不可行吧。

R树就很好的解决了这种高维空间搜索问题。它把B树的思想很好的扩展到了多维空间,采用了B树分割空间的思想,并在添加、删除操作时采用合并、分解结点的方法,保证树的平衡性。因此,R树就是一棵用来存储高维数据的平衡树。

好了简介就到此为止。以下,本文将详细介绍R树的数据结构以及R树的操作。至于R树的扩展与R树的性能问题,我就仅仅在文末简单介绍一下吧,这些问题最好查阅相关论文比较合适。

R树的数据结构

如上所述,R树是B树在高维空间的扩展,是一棵平衡树。每个R树的叶子结点包含了多个指向不同数据的指针,这些数据可以是存放在硬盘中的,也可以是存在内存中。根据R树的这种数据结构,当我们需要进行一个高维空间查询时,我们只需要遍历少数几个叶子结点所包含的指针,查看这些指针指向的数据是否满足要求即可。这种方式使我们不必遍历所有数据即可获得答案,效率显著提高。下图1是R树的一个简单实例:

我们在上面说过,R树运用了空间分割的理念,这种理念是如何实现的呢?R树采用了一种称为MBR(Minimal Bounding Rectangle)的方法,在此我把它译作“最小边界矩形”。从叶子结点开始用矩形(rectangle)将空间框起来,结点越往上,框住的空间就越大,以此对空间进行分割。有点不懂?没关系,继续往下看。在这里我还想提一下,R树中的R应该代表的是Rectangle(此处参考wikipedia),而不是大多数国内教材中所说的Region(很多书把R树称为区域树,这是有误的)。我们就拿二维空间来举例吧。下图是Guttman论文中的一幅图。

我来详细解释一下这张图。先来看图(b)吧。首先我们假设所有数据都是二维空间下的点,图中仅仅标志了R8区域中的数据,也就是那个shape of data object。别把那一块不规则图形看成一个数据,我们把它看作是多个数据围成的一个区域。为了实现R树结构,我们用一个最小边界矩形恰好框住这个不规则区域,这样,我们就构造出了一个区域:R8。R8的特点很明显,就是正正好好框住所有在此区域中的数据。其他实线包围住的区域,如R9,R10,R12等都是同样的道理。这样一来,我们一共得到了12个最最基本的最小矩形。这些矩形都将被存储在子结点中。下一步操作就是进行高一层次的处理。我们发现R8,R9,R10三个矩形距离最为靠近,因此就可以用一个更大的矩形R3恰好框住这3个矩形。同样道理,R15,R16被R6恰好框住,R11,R12被R4恰好框住,等等。所有最基本的最小边界矩形被框入更大的矩形中之后,再次迭代,用更大的框去框住这些矩形。我想大家都应该理解这个数据结构的特征了。用地图的例子来解释,就是所有的数据都是餐厅所对应的地点,先把相邻的餐厅划分到同一块区域,划分好所有餐厅之后,再把邻近的区域划分到更大的区域,划分完毕后再次进行更高层次的划分,直到划分到只剩下两个最大的区域为止。要查找的时候就方便了吧。

下面就可以把这些大大小小的矩形存入我们的R树中去了。根结点存放的是两个最大的矩形,这两个最大的矩形框住了所有的剩余的矩形,当然也就框住了所有的数据。下一层的结点存放了次大的矩形,这些矩形缩小了范围。每个叶子结点都是存放的最小的矩形,这些矩形中可能包含有n个数据。

在这里,读者先不要去纠结于如何划分数据到最小区域矩形,也不要纠结怎样用更大的矩形框住小矩形,这些都是下一节我们要讨论的。

讲完了基本的数据结构,我们来讲个实例,如何查询特定的数据吧。又以餐厅为例吧。假设我要查询广州市天河区天河城附近一公里的所有餐厅地址怎么办?打开地图(也就是整个R树),先选择国内还是国外(也就是根结点)。然后选择华南地区(对应第一层结点),选择广州市(对应第二层结点),再选择天河区(对应第三层结点),最后选择天河城所在的那个区域(对应叶子结点,存放有最小矩形),遍历所有在此区域内的结点,看是否满足我们的要求即可。怎么样,其实R树的查找规则跟查地图很像吧?对应下图:

一棵R树满足如下的性质:

<span="m

2.  对于所有在叶子中存储的记录(条目),I是最小的可以在空间中完全覆盖这些记录所代表的点的矩形(注意:此处所说的“矩形”是可以扩展到高维空间的)。

<span="m

4.  对于在非叶子结点上的每一个条目,i是最小的可以在空间上完全覆盖这些条目所代表的店的矩形(同性质2)。

<span="a

<p="adjusttree<span="adjusttree<span="at1<span="at2<span="at3<span="at4<span="at5<span="b-tree<span="b10<span="b8<span="b9<span="b<span="b)。<p="b树的部分)。1<=d<span="c<span="child-pointer)<span="child-pointer<p="chooseleaf<span="chooseleaf<span="cl1<span="cl2<span="cl3<span="cl4<span="code<p="condensetree<span="condensetree<span="ct1<span="ct2<span="ct3<span="ct4<span="ct5<span="ct6<span="d,e,f,g<span="d1<span="d2<span="d3<span="d4<span="d<p="delete<span="e.i<span="e<font="en<span="ennn<span="enn<span="en.i<span="f<p="findleaf<span="findleaf<span="fl1<span="fl2<span="function ="http://en.wikipedia.org/wiki/btree " http://slady.net/java/bt/view.php 0,I1,…,In-1)<span="i1<span="i2<span="i3<span="i4<span="i<p="indices<span="inert<p="insert<span="insert<span="l<span="ll<span="m.i<span="n.i<span="n<span="n<span="nn<span="p<span="pp<span="q<span="r*<span="r11<span="r12<span="r1<span="r2.i<span="r21<span="r2<span="r3<span="r4<span="r5<span="r6<span="r7<span="r<span="s1:[<span="s2:[<span="s<p="search<span="search
6.="search),插入(insert),删除(delete)操作。)
6." sizcache="1" size="2" sizset="53" source="source" span="span" span>="span>" span>

="span>

" span>

<span="splitnode <span="t <p="tree <span="tuple-identifier) <span="tuple-identifier <span="tuple


武汉华为机试题目-2013年9月13日

一、动态测试显示电量的格数(支持显示个位数)

#include <iostream.h>

void printHead()
{
	cout<<"+----------+"<<endl;	
}

void printTail()
{
	cout<<"+----------+"<<endl;	
}

void printFull()
{
	cout<<"|++++++++++|"<<endl;
}

void printEmpty()
{
	cout<<"|----------|"<<endl;
}

void printAdd(int n)
{
	cout<<"|";
	for (int i = 0; i < n; ++ i) {
		cout<<"+";
	}
}

void printMin(int n)
{	
	for (int i = 0; i < n; ++ i) {
		cout<<"-";
	}
	cout<<"|"<<endl;
}

void print(int n)
{
	int a[3] = {0};
	int i,j = n,nCount=0;
	while(j) {
		a[nCount++] = j % 10;
		j = j / 10;
	}
	nCount --;

	//100的电量
	if (nCount == 3) {
		printHead();
		for (i = 0; i < 10; ++ i) {
			printFull();
		}
		printTail();
		
	} else {
			
		printHead();

		//整行空
		for (i = 0; i < 10 - a[1] - 1; ++ i) {
			printEmpty();
		}

		//处理个位数
		printAdd(a[0]);
		printMin(10-a[0]);		

		//整行满
		for (i = 0; i < a[1]; ++ i) {
			printFull();
		}

		printTail();

	} 

}

int main(int argc, char **argv)
{
	int n,i;
	
	cin>>n;

	int *a = new int[n];

	for (i = 0; i < n; ++ i) {
		cin>>a[i];

		if (a[i] > 100 || a[i] < 0) {
			cout<<"输入参数错误."<<endl;
			break;
		}
	}

	for (i = 0; i < n; ++ i) {
		print(a[i]);
		cout<<"=================\n";
	}

	return 0;
}
二、在中国,形容夫妻恩爱的词汇中,大家用的比较多的就是“夫妻相”。所谓“夫妻相”,就是两个人看上去比较般配,长相、身材等某些方面有一定的相似度。本题则另辟蹊径,从人的姓名维度,以字母重复个数来寻找最具“夫妻相”的人。题目中预先给定一组女士的姓名拼音。输入男士的姓名拼音(拼音中间可以有空格,字母全部小写),依预先给定姓名拼音的先后遍历所有姓名,输出字母重复数最多的女士姓名。

规则1:如果字母重复数最多的女士有多位相同,则以最先匹配的女士做为最具“夫妻相”的人选。
规则2:人名中的相同字母,按重复一次处理。例如:li ling 与li lei 重复的字符个数为2,而不是4。
预置女士名单(先后循序必须保证):
"wang fei",
"zhang man yu",
"zhang zhi yi",
"li li",
"li xiao man",
"li yu cun",
"yang ni",
"xiao tong",
"li lei",
"zhang san" 
 
运行时间限制: 无限制 
内存限制: 无限制 
输入: 输入一个男士姓名,字符串 
输出: 输出最具“夫妻相”的女士姓名

#include <iostream>
#include <stdio.h>
#include <set>
#include <map>
#include <string>
using namespace std;

int main()
{
	
	int i,j;
	string nvName[] = {"wang fei",
					"zhang man yu",
					"zhang zhi yi",
					"li li",
					"li xiao man",
					"li yu cun",
					"yang ni",
					"xiao tong",
					"li lei",
					"zhang san"};

	int len = sizeof (nvName)  / sizeof (string);
	
	map<string, int> iMap;
	for (i = 0;  i < len; ++ i) {
		iMap.insert(make_pair(nvName[i], 0));
	}


	string cname = "tian mo";
	//cin>>cname;
	set<char> cset;
	int clen = cname.size();
	for (i = 0 ;i < clen; ++ i) {
		if (cname[i] != ' ') {
			cset.insert(cname[i]);
		}
	}

	
	set<char>::iterator iter;
	for (i = 0;  i < len; ++ i) {		
		for (iter = cset.begin(); iter != cset.end(); ++ iter) {
			if (nvName[i].find(*iter) != string::npos) {
				++ iMap[nvName[i]] ;
			}
		}

	}

	///
	string ret;	
	int max = iMap["wang fei"];			
	map<string,int>::iterator it = iMap.begin() ;
	for (; it != iMap.end(); ++ it) {
		if (it->second > max) {
			max = it->second;
			ret = it->first;
		}
		//cout<<it->first<<"  "<<it->second<<endl; 
	}
	printf("nvName = %s\n", ret.c_str());

	return 0;
}

三、CandyBrush游戏 

描述: CandyBrush一款流行的消除游戏,在一个方阵中布满各种糖果,任意交换两个糖果,如果交换后出现横向或者竖向有连续三个相同的情况,则可以消除糖果并得分。
输入25个字符组成的字符串(编号为1到25),表示5×5的方阵,每个字符表示不同类型的糖果(区分大小写),判断是否有解,即交换某两个糖果后能够消除糖果。

举例,
下面的方阵无解
T M T M O
X R U C q
A B C X R
U R M T O
T Q C R A

下面的方阵交换(0, 2)和(1, 2)两个糖果后,能消除M
T M T M O
X R M C q
A B C X R
U R M T O
T Q C R A

如果有解,则输出YES,同时输出所有交换方案中,被交换糖果最小编号(1到25),比如:上面的方阵中,被交换的糖果,编号最小的为糖果“T”,编号为3。
如果无解,则输出NO。
 
运行时间限制: 无限制 
内存限制: 无限制 
输入: 输入25个字符组成的字符串,不含空格,表示5×5的方阵 
输出: 如果有解,则输出YES,同时输出所有交换方案中,被交换糖果最小编号,以空格隔开。如果无解,则输出NO。 
 
样例输入: TMTMOXRMCqABCXRURMTOTQCRA 
样例输出: YES 3 
答案提示: 输出最小糖果编号的意思,就是从左至右,从上到下遍历糖果,找到第一个跟其他交换后能消除的糖果即可 


#include <stdio.h>
#include <string.h>

#define N 5

int findChar(char a[N][N], char c, char direct, char &ret, int i0, int j0)
{
	int i ,j;
	
	//纵向查找
	if (direct == '0') {
			//向上
			i = i0 - 1;
			if (i >= 0 && i < N ) {
			
				if (a[i][j0] == c) {
					ret = '0';
					return 1;
				}
			} 

			//向下
			i = i0 + 1;
			if (i >= 0 && i < N) {
			
				if (a[i][j0] == c) {
					ret = '1';
					return 1;
				}
			} 

	} else if (direct == '1') {//横向查找
		//向左
		j = j0 - 1;
		if (j >= 0 && j < N ) {
		
			if (a[i0][j] == c) {
				ret = '0';
				return 1;
			}
		} 

		//向右
		j = j0 + 1;
		if (j >= 0 && j < N) {
		
			if (a[i0][j] == c) {
				ret = '1';
				return 1;
			}
		} 

	} else {
		//没有找到
	}
	
	return 0;
}

int main()
{
	int i,j;
	int row = 0, col = 0;
	int rowPos = 0, colPos = 0;
		
	char pA, pB, pC;
	char direct = '0'; //向上或向左, direct = '1',表示向下或向右

	char a[N][N] = {{'T','M','T','M','O'},{'X','R','M','C','q'},{'A','B','C','X','R'},{'U','R','M','T','O'},{'T','Q','C','R','A'}};
	/*
	for (i = 0 ; i < N; ++ i) {
		for (j = 0; j  < N; ++ j) {
			scanf("%c", &a[i][j]);
		}
	}
	*/

	//每行
	for (i = 0; i < N; ++ i) {
		for (j = 0; j < N - 2; ++ j) {
			//记录每行连续的三个字符
			pA = a[i][j]; pB = a[i][j + 1]; pC = a[i][j + 2];

			//当连续的三个字符相同
			if (pA == pB && pA == pC && pB == pC) {	
				row = 1;
				rowPos =  i * N + j + 1;			
				break;			
			} else if (pA == pB) {
				if (findChar(a, pA, '0', direct, i, j + 2) == 1) {
					row = 1;
					if (direct == '0') {
						rowPos = (i - 1) * N + (j + 2)  + 1;;
					} else {
						rowPos = i * N + (j + 2)  + 1;						
					}
					break;					
				}
			} else if (pA == pC) {					
				if (findChar(a, pA, '0', direct, i, j + 1) == 1) {	
					row = 1;
					if (direct == '0') {
						rowPos = (i - 1) * N + (j + 1)  + 1;;
					} else {
						rowPos = i * N + (j + 1)  + 1;						
					}
					break;					
				}
			} else if (pB == pC) {
				if (findChar(a, pB, '0', direct, i, j) == 1) {
					row = 1;
					if (direct == '0') {
						rowPos = (i - 1) * N + (j)  + 1;;
					} else {
						rowPos = i * N + (j)  + 1;						
					}
					break;					
				}
			} else {
				//行中没有找到
			}		
		}
	}

	//每列
	direct = '0';
	for (i = 0; i < N; ++ i) {
		for (j = 0; j < N - 2; ++ j) {
			pA = a[j][i]; pB = a[j + 1][i]; pC = a[j + 2][i];
			if (pA == pB && pA == pC && pB == pC) {
				col = 1;
				colPos = j * N + i + 1;
			} else if (pA == pB) {
				if (findChar(a, pA, '1', direct, j + 2, i) == 1) {
					col = 1;
					if (direct == '0') {
						colPos = (j + 2) * N + (i - 1)  + 1;
					} else {
						colPos = (j + 2) * N + (i + 1)  + 1;						
					}
					break;					
				}
			} else if (pA == pC) {					
				if (findChar(a, pA, '1', direct, j + 1, i) == 1) {	
					col = 1;
					if (direct == '0') {
						colPos = (j + 1) * N + (i - 1)  + 1;
					} else {
						colPos = (j + 1) * N + (i + 1)  + 1;						
					}
					break;					
				}
			} else if (pB == pC) {					
				if (findChar(a, pB, '1', direct, j , i) == 1) {	
					col = 1;
					if (direct == '0') {
						colPos = j * N + (i - 1)  + 1;
					} else {
						colPos = j * N + (i + 1)  + 1;						
					}
					break;					
				}
			} else {
				//列中没有找到
				col = 0;
			}
		}
	}
	
	int minPos = -1;
	if (row == 1 && col == 1) {
		if (rowPos > colPos) {
			minPos = colPos;
		} else {
			minPos = rowPos;
		}
	} else if (row == 1) {
		minPos = rowPos;
	} else if (col == 1) {
		minPos = colPos;
	} 

	if (minPos == -1) {
		printf("NO \n");
	} else {
		printf("YES, %d\n", minPos);
	}

	return 0;
	
}


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值