B树及其衍生树

B树及其衍生树

一、B树

1.    介绍

B树与其它几种查找树(二叉查找树、AVL树和红黑树)不同,它每个节点最多有N个子节点而不是两个子节点,所以,它的高度是,而不是其它几种树的

B树是用来为磁盘而设计的数据结构,它区别于针对随机存取的贮存而设计的数据结构。因为磁盘的IO操作比较耗时,大概比主存存取大5个数量级,也就是说,一次磁盘存取大概的时间可以使主存完成大概100000次的存取。

图:B树-每个内节点有1000关键字

上图可以看出,一个B树,每个结点有1000个关键字,高度为2,就能包含10亿个关键字,只有根节点存在主存中,查找一个关键字只需要最多读取两次磁盘就能完成查找。

图:B树的结构图

B树是由有序数组+平衡多叉树组成。

2.    B树性质

一棵BT(根为root[T])具有如下性质(《算法导论》):

(1)    每个结点x有以下域:

l  n[x],当前存储在结点x中的关键字数,n[x]个关键字本身,以非降序存放,有key1[x]<=key2[x]<=...<=keyn[x]

l  leaf[x],是一个布尔值,表示该结点是否为叶子结点

(2)    每个内结点x还包含n[x]+1个指向子结点的指针c1[x],c2[x],...,cn[x]+1[x],如果是叶子结点,则ci[x]指向空

(3)    各关键字keyi[x]对存储在各子树中的关键字范围加以分隔:如ki为存储在以ci[x]为根的子树中的关键字,则有:

 k1<=key1[x]<=k2<=key2[x]<=...<=kn[x]<=keyn[x][x]<=kn[x]+1

(4)    每个叶结点具有相同的深度,即树的高度h

(5)    每个结点能包含的关键字有一个上界和下界。这些界可以称为B树的最小度数的固定整数t>=2来表示

l  每个非根结点必须至少有t-1个关键字,每个非根的内结点至少有t个子女。如果树是非空的,则根结点至少包含一个关键字。

l  每个结点可包含至多2t-1个关键字,每个非根的内结点至多可有2t个子女。如果一个结点是满的,则它有2t-1个关键字。

B树的几项约束(网上对B树性质的解说):

一棵m阶的B树:

l  树中一个节点最多有m棵子树,这也是m阶的含义。

l  若根节点有孩子,至少有两棵子树。

l  除根节点之外的所有非叶子节点至少有[m/2](上界) 棵子树。

l  所有的非终端节点包括的信息有:
n(该节点含有的关键字数目),索引指针A0,关键字K1,索引指针A1,关键字K2...,关键字Kn,索引指针An
也就是说有n个关键字,会有n+1个索引指针,指向其孩子节点。并且,A0所指的孩子节点的关键字均小于K1,A1所指的孩子节点关键字均大于K1。显然n的范围: [m/2]到m-1的闭区间。

l  所有的叶子节点都出现在同一层次上,并且不带信息。即叶子节点不存关键字,是查找失败的节点,实际上不存在,只是一个虚拟的概念。

大体上的说法基本相似,只有对叶结点的看法上有所区别,《算法导论》上的叶结点可以存放关键字,但是没有子结点;而网络上理解的叶结点,是一个类似于红黑树的叶结点那样,不包含关键字,也就更没有子结点了。下面的一些分析都是基于《算法导论》上的说明进行的。

3.    B树操作

查找

搜索B树与搜索二叉树很相似,从根节点开始,由于每个结点已经是一个排好序的数组,可以采用顺序查找,或者二分查找等快速查找方法进行结点内的查找,如果没有查找到,则对对应的子结点进行查找,直到查找到或者查找到叶子结点。

伪代码:

BTSearch(T, value)
    if T is NULL
        return NULL
    x <- root[T]
    while !leaf[x]
        for i <- 0 to n[x]
            if value = x[i]
                return x[i]
            else if value < x[i]
                i <- i+1
            else
                break
        if i != n[x]
            x = c[i]
    return NULL

插入

插入操作是由几个操作组成的,先查找到对应的位置,然后在对应的位置进行插入结点,最后查看是否要对该结点进行分裂,如果分裂就继续要对上一层进行查看是否需要分裂。

伪代码:

BTSplit(x, i, y)
    n[z] <- t - 1
    leaf[z] <- leaf[y]
    for j <- 0 to t - 1
        key[z][j] <- key[y][j+t]
    if !leaf[y]
        for j <- 0 to t - 1
            c[z][j] <- c[y][j+t]
    n[y] <- t - 1
    for j <- n[x]+1 downto i+1
        key[x][j+1] <- key[x][j]
    key[x][i] <- key[y][t]
    n[x] <- n[x] + 1
    return x, y and z
BTInsertNode(s, x)
    for i <- 0 to n[x]
        if s < x[i]
            break
    if i < n[x]
        for j <- n[x] downto i
            x[i+1] <- x[i]
    else
        x[i+1] <- s
    n[x] = n[x] + 1
    
    return i

BTInsert(value, T)
    x <- root[T]
    s <- ALLOC(value)
    stack st
    if x = NULL
        root[T] <- s
        return
    while !leaf[x]
        for i <- 0 to n[x]
            if s < x[i]
                st.push(x)
                st.push(i)
                x <- c[i]
                break
    BTInsertNode(s, x)
    while n[x] > 2 * t - 1
        y <- x
        i <- st.pop
        x <- st.pop
        BTSplit(x, i, y)

说明:分裂函数比较简单,找到分裂的结点及其父结点,以及是的第个结点,然后,将分成两份,第一份的下标,第二份的下标为,第一份为老结点,第二份为新结点,而下标为的结点上升为这两个结点的根节点,插入结点的第个位置。

插入操作是先找到存放结点,如果该树不为空,该结点应该是叶结点(也有可能同时是根节点),然后在该结点内找到对应位置,进行插入,其后的关键字逐个向后移动一个位置。最后判断该结点中的关键字是否超过,如果是,则进行分裂,一旦分裂进行完,就需要迭代判断父结点是否也需要分裂,否则插入操作结束。

删除

删除操作的过程:

(1)    如果关键字在结点中,而且是个叶结点,则从中删除

(2)    如果关键字在结点中,而且是内结点,则操作如下:

(a)        如果结点中前于的子结点包含至少个关键字,则找出在以为根的子树中的前驱结点,迭代的删除,并用代替中的。

(b)        否则,如果结点位于之后的子结点包含至少个关键字,则找出在以为根的子树中的后继结点,迭代的删除,并用代替中的。

(c)         否则,如果和都只有个关键字,则将和中所有的关键字合并进,使失去对和指向的指针,这使包含了个关键字。然后,释放并将从中删除。

(3)    如果关键字不在内结点中,则确定必包含的子树的根。如果只有个关键字,执行步骤(3)-(a)或(3)-(b)以保证我们降至一个包含至少个关键字的结点。然后通过对的某个合适的子结点递归而结束。

(a)    如果只包含个关键字,但它的相邻兄弟包含至少个关键字,则将中的某个关键字降至中,将的相邻左兄弟或右兄弟中的某一个关键字升至,将该兄弟中合适的子女指针移到中,这样使得增加一个额外的关键字。

(b)    如果以及的所有兄弟都包含了个关键字,则将于一个兄弟合并,即将的一个关键字一致新合并的结点,成为该节点的一个中间关键字。

下面根据书上的例子,说明上面的步骤:

图:初始树 

图:删除F,步骤1 

图:删除M,步骤2a 

图:删除G,步骤2c

 

图:删除D,步骤1和3b(树的高度缩减)

 

图:删除B,步骤1和3a

删除操作比较麻烦,下面是其伪代码:

BTFindInNode(x, s, c)
    for i <- 0 to n[x]
        c <- c[x][i]
        if x[i] = s
            return i
        else if x[i] > s
            i <- i+1
        else
            return -(i-1)

BTRemoveInNode(x, i)
    t <- x[i]
    for j <- i to n[x]-1
        x[i] <- x[i+1]
    n[x] = n[x] - 1
    delete t

BTMerge(x, y, z)
    i <- n[y]
    if x != NULL
        y[i] <- x
        i <- i + 1
    for j <- i to n[z]
        y[j] <- z[j-i]
        c[y][j]
    n[y] <- j
    delete z

BTDelete(T, s)
    x <- root[T]
    while x
        c <- NULL
        i <- BTFindInNode(x, s, c)
        if i >= 0
            y <- c[x][i]
            z <- c[x][i+1]
            if leaf[x]                        //1
                BTRemoveInNode(x, s, i)
            else 
                while 1
                    if n[y] >= t
                        x <- y
                        k <- y[n[y]]
                        j <- n[y]
                        if leaf[y]
                            break
                        else
                            y <- c[y][n[y]-1]
                            z <- c[y][n[y]]
                    else if n[z] >= t
                        x <- z
                        k <- z[0]
                        j <- 0
                        if leaf[z]
                            break
                        y <- c[z][0]
                        z <- c[z][1]
                    else
                        break
                if n[y] >= t            //2a
                    s <- y[n[y]]
                    BTRemoveInNode(y, n[y])
                else if n[z] >= t        //2b
                    s <- z[0]
                    BTRemoveInNode(z, 0)
                else                    //2c
                    BTMerge(NULL, y, z)
                    BTRemoveInNode(x, j)
        else
            i <- -i
            newX <- c[x][i]
            if n[newX] = t-1
                y <- newX
                z <- newX
                if i > 0
                    y <- c[x][i-1]
                if i < n[x]
                    z <- c[x][i+1]
                if n[y] >= t or n[z] >= t        //3a
                    if n[y] >= t and y != newX
                        for j <- n[newX] downto 0
                            newX[j+1] <- newX[j]
                            if !leaf[newX]
                                c[newX][j+1] <- c[newX][j]
                        newX[0] <- y[n[y]]                        
                        n[newX] <- n[newX]+1
                        if !leaf[newX]
                            c[newX][0] <- c[y][n[y]]                        
                        BTRemoveInNode(y, 0)
                    else if n[z] >= t and z != newX
                        newX[n[newX]] <- z[0]
                        n[newX] <- n[newX]+1
                        if !leaf[newX]
                            c[newX][n[newX]] <- c[z][0]
                        BTRemoveInNode(z, 0)
                else                            //3b
                    BTMerge(x, y, z)
                    if x = root[T]
                        root[T] <- y
            x <- newX

二、B+树

1.    介绍

B+树是B树的一种变形树,是为了满足文件系统而产生的。B+树是由有序链表+平衡多叉树组成

2.    B+树性质

它与B树的差别在于(m阶B+树和m阶B树):

  1. 每个关键字对应一棵子树(B树每个关键字对应两棵子树,相邻的两个关键字对应三棵子树)。
  2. 叶子结点包含了所有的关键字(包括其父结点的关键字),以及叶子结点有指向其兄弟的指针。每个叶子结点是从小到大排好序的,因此通过兄弟指针能获取到整个关键字的顺序链接。
  3. 非叶子结点虽然也包含部分关键字(对应子结点的最小关键字或最大关键字),但只是为了方便查找到对应的叶子结点,所以非叶子结点完成了索引的功能。 

图:B+树结构图

B+树更适合做系统的文件索引和数据库索引的原因:

(1)    B+树的磁盘读写代价更低

B+树的内结点没有多余的关键字信息,所占的空间也就比较小,同样一个内结点在B树众占据的空间比在B+树中的要大,也就使在B树中可能需要多次读取才能完成的操作在B+树中只需要读取一次。

(2)    B+树的查询效率更稳定

由于只有叶子结点才有关键字信息,而所有叶子结点到根的路径长度是相同的,就保证了每次查询的效率是一样的,也就更稳定了。

3.    B+树操作

以下操作都假设在mB+树上进行:

查找

查找操作比较简单,由于B+树是由有序链表+平衡多叉树组成,所以有两种查找方式,一种是从平衡二叉树角度查找,即从根节点开始,一直查找到叶子结点;另一种是从有序链表角度查找,即从链表头开始顺序进行查找。

插入

B+树的插入是在叶子结点上进行的,假设要插入关键字a,找到叶子结点后插入a,然后进行以下判定:

(a)    如果插入后结点关键字数目小于等于m并且当前结点是根结点,则算法结束;

(b)    如果插入后结点关键字数目小于等于m并且当前结点是非根结点,则判断若a是新索引值时转步骤(d)后结束,若a不是新索引值则直接结束;

(c)     如果插入后关键字数目大于m(阶数),则结点先分裂成两个结点X和Y,并且他们各自所含的关键字个数分别为:u=大于(m+1)/2的最小整数,v=小于(m+1)/2的最大整数;

由于索引值位于结点的最左端或者最右端,不妨假设索引值位于结点最右端,有如下操作:

如果当前分裂成的X和Y结点原来所属的结点是根结点,则从X和Y中取出索引的关键字,将这两个关键字组成新的根结点,并且这个根结点指向X和Y,算法结束;

如果当前分裂成的X和Y结点原来所属的结点是非根结点,依据假设条件判断,如果a成为Y的新索引值,则得到Y的双亲结点P转步骤(d);

如果a不是Y结点的新索引值,则求出X和Y结点的双亲结点P,然后提取X结点中的新索引值a’,在P中插入关键字a’,从P开始,继续进行插入算法;

(d)    提取结点原来的索引值b,自顶向下,先判断根是否含有b,是则需要先将b替换为a,然后从根结点开始,记录结点地址P,判断P的孩子是否含有索引值b而不含有索引值a,是则先将孩子结点中的b替换为a,然后将P的孩子的地址赋值给P,继续搜索,直到发现P的孩子中已经含有a值时,停止搜索,返回地址P。

删除

B+树的删除也是在叶子结点上进行的,假设要删除关键字a,找到叶子结点后删除a,然后进行以下判定:

(a)    如果当前节点X为根节点,则算法结束;否则到(b)

(b)    如果删除后结点关键字数目大于等于m/2(取大于m/2的最小整数),则判断若a是索引值时转步骤(d)后结束,若a不是索引值则直接结束;

(c)     如果当前结点X关键字数目小于m/2(取大于m/2的最小整数),则取它的兄弟节点Y,进行下面判定(由于索引值位于结点的最左端或者最右端,不妨假设索引值位于结点最右端):

l  如果Y的关键字数目大于m/2(取大于m/2的最小整数),则将Y中的最左关键字移植X中,并更改X在其父结点的索引。

l  如果Y的关键字数目小于等于m/2(取大于m/2的最小整数),则将Y和并到X中,更改X在父结点中的索引,删除Y在父结点对应的索引a’,变为删除a’,将X对应的索引值变为Y的索引值,当前结点变为X的父结点,转到(a)

l  如果Y不存在,如果a是索引值,转到(d);否则算法结束

(d)    提取结点原来的索引值b,自顶向下,先判断根是否含有b,是则需要先将b替换为a,然后从根结点开始,记录结点地址P,判断P的孩子是否含有索引值b而不含有索引值a,是则先将孩子结点中的b替换为a,然后将P的孩子的地址赋值给P,继续搜索,直到发现P的孩子中已经含有a值时,停止搜索,返回地址P。

三、B*树

1.    介绍

B*树是B+树的变体,在B+ 树非根和非叶子结点再增加指向兄弟的指针,且结点的使用率也比B+树高。

B*树是丰满的B+树。

2.    B*树性质

B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3(代替B+树的1/2)。叶子结点和内结点都有指向兄弟的指针。 

图:B*树结构图

3.    B*树操作

查找

与B+树查找方法相同。

插入

插入本身与B+树相同,区别在于B*树的分裂方式不同:

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

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

在分裂完成后,需要对到有影响的结点的索引进行更新,也就是查看老的索引是否能匹配操作后的结点,如果不匹配,则找出操作后结点的索引值来替换老的索引值。

删除

删除本身也与B+树相同,区别在于B*树的合并方式不同,区别在于:

B+树的合并:当一个结点中关键字数目小于m/2(取大于m/2的最小整数)时进行合并,将根据兄弟节点的数目决定使用何种合并方式(拆借还是合并)。

B*树的合并:当一个结点中关键字数目小于m2/3(取大于m2/3的最小整数)时进行合并,将根据兄弟节点的数目决定使用何种合并方式(拆借还是合并)。

其它的操作与B+树基本无异。

posted @ 2012-07-17 13:40  Geek_Ma  阅读(444)  评论(0编辑  收藏  举报