B-树、B+树、键树

 


 
 

  

B-树

B-树

  B-树是一种平衡的多路查找树。

一棵 m 阶的 B-树,或为空树,或为满足下列特性的 m 叉树:
(1)树中每个结点至少有 m 棵子树;
(2)若根结点不是叶子结点,则至少有两棵子树;
(3)除根之外的所有非终端结点至少有⌈m/2⌉棵子树;
(4)所有的非终端结点中包含下列信息数据
          (n, A0, K1, A1, K2, A2, … , Kn, An)
  其中:Ki(i=1, … , n) 为关键字,且 Ki<Ki+1(i=1, … , n-1);Ai(i=0, … , n)为指向子树根结点的指针,且指针 Ai-1 所指子树中所有结点的关键字均小于 Ki(i=1, … , n),An 所指子树中所有结点的关键字均大于 Kn,n(⌈m/2⌉-1≤n≤m-1)为关键字的个数(或 n+1 为子树个数)。
(5)所有的叶子结点都出现在同一层次上,并且不带信息(可以看作是外部结点或查找失败的结点,实际上这些结点不存在,指向这些结点的指针为空)。

B-树示例
结点定义:

#define m e					//B-树的阶,暂设为3 
typedef struct BTNode{
	int keynum;				//结点中关键字个数,即结点的大小 
	struct BTNode *parent;	//指向双亲结点 
	KeyType key[m+1];		//关键字向量,0号单元未用 
	struct BTNode *ptr[m+1];//子树指针向量 
	Record *recptr[m+1];	//记录指针向量,0号单元未用 
}BTNode, *BTree;			//B-树结点和B-树的类型 
typedef struct{
	BTNode *pt;				//指向找到的结点 
	int i;					//1..m,在结点中的关键字序号 
	int tag;				//1:查找成功,0:查找失败 
}Result; 					//B-树的查找结果类型 

 
 

B-树查找

  在 B-树上进行查找的过程和二叉排序树的查找类似。

  B-树是多路查找,因为B-树结点内的关键字是有序的,在结点内进行查找时除了顺序查找外,还可以用折半查找来提升效率。B-树的具体查找步骤如下(假设查找的关键字为key):

  1. 先让key与根结点中的关键字比较,如果key等于k[i](k[i]为结点内的关键字数组),则查找成功。
  2. 若key<k[1],则到p[0]所指示的子树中进行继续查找(p[i]为结点内的指针数组),这里要注意B-树中每个结点的内部结构。
  3. 若key>k[n],则道p[n]所指示的子树中继续查找。
  4. 若k[i]<key<k[i+1],则沿着指针p[I]所指示的子树继续查找。
  5. 如果最后遇到空指针,则证明查找不成功。

例子
  在上图中的 B-树上查找关键字 47 的过程如下(查找成功):

  1. 首先从根结点开始,根据根结点指针 t 找到 *a 结点,因 *a 结点中只有一个关键字,且给定值 47>关键字 35,则若存在必在指针 A1 所指的子树内;
  2. 顺指针找到 *c 结点,该结点有两个关键字(43 和 78),而 43<47<78,则若存在必在指针 A1 所指的子树中;
  3. 同样,顺指针找到 *g 结点,在该结点中顺序查找找到关键字 47,由此,查找成功。

  在上图中的 B-树上查找关键字 23 的过程如下(查找不成功):

  1. 从根结点开始,因为 23<35,则顺该结点中指针 A0 找到 *b 结点;
  2. *b 结点中只有一个关键字 18,且 23>18,所以顺结点中第二个指针 A1 找到 *e 结点;
  3. *e 结点中只有一个关键字 27,且 23<27,则顺指针往下找,此时因指针所指为叶子结点,说明此棵 B-树中不存在关键字 23,查找失败。
Result SearchBTree(BTree T, KeyType K){
	//在m阶B-树T上查找关键字K,返回结果(pt,i,tag)。若查找成功,则特征值tag=1,指针pt 
	//所指结点中第i个关键字等于K;否则特征值tag=0,等于K的关键字应插入在指针pt所指 
	//结点中第i和第i+1个关键字之间 
	p = T;	q = NULL;	found = FALSE;	i = 0;//初始化,p指向待查结点,q指向p的双亲 
	while(p && !found){
		i = Search(p, K);	//在p->key[1..keynum]中查找
							//i使得:p->key[i]<=K<p->key[i+1] 
		
		if(i>0 && p->key[i]==K)//找到待查关键字 
			found = TRUE;
		else{
			q = p;
			p = p->ptr[i];
		}
	}
	if(found)//查找成功 
		return (p,i,1);
	else//查找不成功,返回K的插入位置信息 
		return (q,i,0);
} 

 

查找分析

  先考虑最坏的情况,即待查结点在 B-树上的最大层次树。也就是,含 N 个关键字的 m 阶 B-树的最大深度是多少?
  先看一棵 3 阶的 B-树。按 B-树的定义,3 阶的 B-树上所有非终端结点至多可有两个关键字,至少有一个关键字(即子树个数为 2 或 3,故又称 2-3 树)。因此,若关键字个数≤2 时,树的深度为 2(即叶子结点层次为 2);若关键字个数≤6 时,树的深度不超过 3。反之,若 B-树的深度为 4,则关键字的个数必须≥7(参见如下图),此时,每个结点都含有可能的关键字的最小数目。
不同数目的B-树
  根据 B-树的定义,第一层至少有 1 个结点;第二层至少有 2 个结点;由于除根之外的每个非终端结点至少有 ⌈m/2⌉ 棵子树,则第三层至少有 2(⌈m/2⌉) 个结点;…;依次类推,第 l+1 层至少有 2(⌈m/2⌉)l-1 个结点。而 l+1 层的结点为叶子结点。若 m 阶 B-树中具有 N 个关键字,则叶子结点即查找不成功的结点为 N+1,由此有:N+1≥ 2 * (⌈m/2⌉)l-1,反之,l ≤ log⌈m/2⌉((N+1)/2)+1
  也就是说,在含有 N 个关键字的 B-树上进行查找时,从根结点到关键字所在结点的路径上涉及的结点数不超过 log⌈m/2⌉((N+1)/2)+1
 
 

B-树的插入

  B-树的生成也是从空树起,逐个插入关键字而得。
  B-树结点中的关键字个数必须≥⌈m/2⌉-1,因此,每次插入一个关键字不是在树中添加一个叶子结点,而是首先在最底层的某个非终端结点中添加一个关键字,若该结点的关键字个数不超过 m-1,则插入成功,否则要产生结点的“分裂”。

示例
假设需依次插入关键字 30,26,85 和 7。
  首先通过查找确定应插入的位置。由根 *a 起进行查找,确定 30 应插入在 *d 结点中,由于 *d 中关键字数目不超过 2(即 m-1),故第一个关键字插入完成。如下图:
插入 30
  同样,通过查找确定关键字 26 亦应插入在 *d 结点中。但由于 *d 中关键字的数目超过 2,此时需将 *d 分裂成两个结点,关键字 26 及其前、后两个指针仍保留在 *d 结点中,而关键字 37 及其前、后两个指针存储到新产生的结点 *d’ 中。同时,将关键字 30 和指示结点 *d‘ 的指针插入到其双亲结点中。由于 *b 结点中的关键字数目没有超过 2,则插入完成。

查找到插入位置

查找到的结点

完成插入后的形态

插入完成
  同样,在 *g 中插入 85 之后需分裂成两个结点,而当 70 继而插入到双亲结点时,由于 *e 中关键字数目超过 2,则再次分裂为结点 *e 和 *e’,如下图:

查找到插入位置

插入位置

关键字个数不满足,分裂后,仍然不满足

分裂过程

插入完成

插入完成
  再插入关键字 7 时,*c、*b 和 *a 相继分裂,并生成一个新的根结点 *m,如下图:

插入位置

插入位置

分裂过程

分裂过程

插入完成

插入完成

  一般情况下,结点可如下实现“分裂”。
  假设 *p 结点中已有 m-1个关键字,当插入一个关键字之后,结点中含有信息为:
         m,A0,(K1,A1),…,(Km,Am)  且其中Ki<Ki+1  1≤i<m
此时可将 *p 结点分裂为 *p 和 *p‘ 两个结点,其中 *p 结点中含有信息为
         ⌈m/2⌉-1,A0,(K1,A1),…,(K⌈m/2⌉-1,A⌈m/2⌉-1)
*p’ 结点中含有信息
         m-⌈m/2⌉,A⌈m/2⌉,(K⌈m/2+1⌉,A⌈m/2+1⌉),…,(Km,Am)
而关键字 K⌈m/2⌉ 和指针 *p’ 一起插入到 *p 的双亲结点中。

Status InsertBTree(BTree &T, KeyType K, BTree q, int i){
	//在m阶B-树T上结点*q的key[i]与key[i+1]之间插入关键字K 
	//若引起结点过大,则沿双亲链进行必要的结点分裂调整,使T仍是m阶B-树 
	x = K;	ap = NULL;	finished = TRUE;
	while(q && !finished){
		Insert(q, i, x, ap);//将x和ap分别插入到q->key[i+1]和q->ptr[i+1] 
		if(q->keynum < m)//插入完成 
			finished = TRUE;
		else{//分裂结点*q 
			s = (m+1)/2;	split(q, s, aq);	x = q->key[s];
			//将q->key[s+1..m],q->ptr[s..m]和q->recptr[s+1..m]移入新结点*ap 
			q = q->parent;
			if(q)//在双亲结点*q中查找x的插入位置 
				i = Search(q,x);
		}
	}
	if(!finished)//T是空树(参数q初值为NULL)或者根结点已分裂为结点*q和*ap 
		NewRoot(T, q, x, ap);//生成含信息(T,x,ap)的新的根结点*T,原T和ap为子树指针 
	return OK;
} 

 
 

B-树删除

  若在 B-树上删除一个关键字,则首先应找到该关键字所在结点,并从中删除,若该结点为最下层的非终端结点,且其中的关键字数目不少于⌈m/2⌉,则删除完成,否则要进行“合并”结点操作。
  假若所删关键字为非终端结点中的 Ki,则可以指针 Ai 所指子树中的最小关键字 Y 替代 Ki,然后在相应的结点中删去 Y。例如,下图的 B-树上删去 45,可以 *f 结点中的 50 替代 45,然后在 *f 结点中删去 50。
示例a
下面只需讨论删除最下层非终端结点中的关键字的情形。有下列 3 中可能:

  1. 被删关键字所在结点中的关键字数目不小于⌈m/2⌉则只需从该结点中删去该关键字 Ki 和相应指针 Ai,树的其他部分不变。 例如,从上图所示 B-树中删去关键字 12,删除后的 B-树如下图。
    删除12
  2. 被删关键字所在结点中的关键字数目等于⌈m/2⌉-1,而与该结点相邻的右兄弟(或左兄弟)结点中的关键字数目大于⌈m/2⌉-1,则需将其兄弟结点中的最小(或最大)的关键字上移至双亲结点中,而将双亲结点中小于(或大于)且紧靠该上移关键字的关键字下移至被删关键字所在结点中。 例如,从上图中删去 50,需将其右兄弟结点中的 61 上移至 *e 结点中,而将 *e 结点中的 53 移至 *f 和 *g 中关键字数目均不小于⌈m/2⌉-1,而双亲结点中的关键字数目不变,如下图。
    删除50
  3. 被删关键字所在结点和其相邻的兄弟结点中的关键字数目均等于⌈m/2⌉-1.假设该结点有右兄弟,且其右兄弟结点地址由双亲结点中的指针 Ai 所指,则在删去关键字之后,它所在结点中剩余的关键字和指针,加上双亲结点中的关键字 Ki 一起,合并到 Ai 所指兄弟结点中(若没有右兄弟,则合并至左兄弟结点中)。 例如,从上图中 B-树中删去 53,则应删去 *f 结点,并将 *f 中的剩余信息(指针“空”)和双亲 *e 结点中的 61 一起合并到右兄弟结点 *g 中。删除后的树如下图©所示。如果因此使双亲结点中的关键字数目小于⌈m/2⌉-1,则依次类推作相应处理。例如,在图©的 B-树中删去关键字 37 之后,双亲 b 结点中剩余信息(“指针 c”)应和其双亲 *a 结点中关键字 45 一起合并至右兄弟结点 *e 中,删除后的 B-树如下图(d)。
    删除
     
     

B+树

  B+树是应文件系统所需而出的一种 B-树的变型树。
  一棵 m 阶的 B+树和 m 阶的 B-树的差异在于:

  1. 有 n 棵子树的结点中含有 n 个关键字。
  2. 所有的叶子结点中包含了全部关键字的信息,及指向包含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
  3. 所有的非终端结点可以看成是索引部分,结点中仅含有其子树(根结点)中的最大(或最小)关键字。

B+树示例
  在 B+树上进行随机查找、插入和删除的过程基本上与 B-树类似。
  只是在查找时,若非终端结点上的关键字等于给定值,并不终止,而是继续向下直到叶子结点。因此,在 B+树,不管查找成功与否,每次查找都是走了一条从根到叶子结点的路径。B+树查找的分析类似于 B-树。
  B+树的插入仅在叶子结点上进行,当结点中的关键字个数大于 m 时要分裂成两个结点,它们所含关键字的个数分别为⌈(m+1)/2⌉和⌈(m+1)/2⌉。并且,它们的双亲结点中应同时包含这两个结点中的最大关键字。
  B+树的删除也仅在叶子结点进行,当叶子节点中的最大关键字被删除时,其在非终端节点中的值可以作为一个“分界关键字”存在。若因删除而使节点中关键字的个数少于⌈m/2⌉时,其和兄弟结点的合并过程亦和 B-树类似。
 
 

键树

  键树又称数字查找树
  它是一棵度≥2的树,树中的每个结点中不包含一个或几个关键字,而是只含有组成关键字的符号。 例如,若关键字是数值,则结点中只包含一个数位;若关键字是单词,则结点中只包含一个字母字符。这种树会给某种类型关键字的表的查找带来方便。

  假设有如下 16 个关键字的集合
  {CAI、CAO、LI、LAN、CHA、CHANG、WEN、CHAO、YUN、YANG、LONG、WANG、ZHAO、LIU、WU、CHEN}
可对此集合作如下的逐层分割。
首先按其首字符不同将它们分成 5 个子集:
  {CAI、CAO、CHA、CHANG、CHAO、CHEN},{WEN、WANG、WU},{ZHAO},{LI、LAN、LONG、LIU},{YUN、YANG}
然后对其中 4 个关键字个数大于 1 的子集再按其第二个字符不同进行分割。若所得子集的关键字多余 1 个,则还需按其第三个字符不同进行再分割。依此类推,直至每个小子集中只包含一个关键字为止。例如对首字符为 C 的集合可进行如下的分割:
  { {(CAI)、(CAO)}、{ {(CHA),(CHANG),(CHAO)}、(CHEN) } }
显然,如此集合、子集和元素之间的层次关系可以用一棵树来表示,这棵树便为键树。例如,上述集合及其分割的键树如下图。
键树示例
树中根结点的五棵子树分别表示首字符为C、L、W、Y 和 Z 的 5 个关键字子集。从根到叶子结点路径中结点的字符组成的字符串表示一个关键字,叶子结点中的特殊符号 $ 表示字符串的结束。在叶子结点还含有指向该关键字记录的指针。
  为了查找和插入方便,我们约定键树是有序树,即同一层中兄弟结点之间依所含符号自左至右有序,并约定结束符 $ 小于任何字符。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值