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)所有的叶子结点都出现在同一层次上,并且不带信息(可以看作是外部结点或查找失败的结点,实际上这些结点不存在,指向这些结点的指针为空)。
结点定义:
#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):
- 先让key与根结点中的关键字比较,如果key等于k[i](k[i]为结点内的关键字数组),则查找成功。
- 若key<k[1],则到p[0]所指示的子树中进行继续查找(p[i]为结点内的指针数组),这里要注意B-树中每个结点的内部结构。
- 若key>k[n],则道p[n]所指示的子树中继续查找。
- 若k[i]<key<k[i+1],则沿着指针p[I]所指示的子树继续查找。
- 如果最后遇到空指针,则证明查找不成功。
在上图中的 B-树上查找关键字 47 的过程如下(查找成功):
- 首先从根结点开始,根据根结点指针 t 找到 *a 结点,因 *a 结点中只有一个关键字,且给定值 47>关键字 35,则若存在必在指针 A1 所指的子树内;
- 顺指针找到 *c 结点,该结点有两个关键字(43 和 78),而 43<47<78,则若存在必在指针 A1 所指的子树中;
- 同样,顺指针找到 *g 结点,在该结点中顺序查找找到关键字 47,由此,查找成功。
在上图中的 B-树上查找关键字 23 的过程如下(查找不成功):
- 从根结点开始,因为 23<35,则顺该结点中指针 A0 找到 *b 结点;
- *b 结点中只有一个关键字 18,且 23>18,所以顺结点中第二个指针 A1 找到 *e 结点;
- *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-树的定义,第一层至少有 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),故第一个关键字插入完成。如下图:
同样,通过查找确定关键字 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。
下面只需讨论删除最下层非终端结点中的关键字的情形。有下列 3 中可能:
- 被删关键字所在结点中的关键字数目不小于⌈m/2⌉则只需从该结点中删去该关键字 Ki 和相应指针 Ai,树的其他部分不变。 例如,从上图所示 B-树中删去关键字 12,删除后的 B-树如下图。
- 被删关键字所在结点中的关键字数目等于⌈m/2⌉-1,而与该结点相邻的右兄弟(或左兄弟)结点中的关键字数目大于⌈m/2⌉-1,则需将其兄弟结点中的最小(或最大)的关键字上移至双亲结点中,而将双亲结点中小于(或大于)且紧靠该上移关键字的关键字下移至被删关键字所在结点中。 例如,从上图中删去 50,需将其右兄弟结点中的 61 上移至 *e 结点中,而将 *e 结点中的 53 移至 *f 和 *g 中关键字数目均不小于⌈m/2⌉-1,而双亲结点中的关键字数目不变,如下图。
- 被删关键字所在结点和其相邻的兄弟结点中的关键字数目均等于⌈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-树的差异在于:
- 有 n 棵子树的结点中含有 n 个关键字。
- 所有的叶子结点中包含了全部关键字的信息,及指向包含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
- 所有的非终端结点可以看成是索引部分,结点中仅含有其子树(根结点)中的最大(或最小)关键字。
在 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 个关键字子集。从根到叶子结点路径中结点的字符组成的字符串表示一个关键字,叶子结点中的特殊符号 $ 表示字符串的结束。在叶子结点还含有指向该关键字记录的指针。
为了查找和插入方便,我们约定键树是有序树,即同一层中兄弟结点之间依所含符号自左至右有序,并约定结束符 $ 小于任何字符。