B树

1、前言

B树是为磁盘或其它直接存取的辅助存储设备而设计的一种平衡搜索树,它类似于红黑树,但它在降低磁盘I/O操作上更好,B树也常被用于数据库中。

B树的最大不同之处在于B树的结点可以有很多孩子,数十到数千都可以。因为此特性,B树的高度一般会比红黑树低很多。

B树_第1张图片

ps:有种观点是B树即为普通二叉搜索树,B-树才是本文中讨论的B树,本文不采纳此观点,B-树是从B-Tree翻译过来的,翻译成B-树,这也太粗糙了。

2、B树定义

从上图可知,B树结点中,如果有n个关键字,那么就有n+1个孩子。结点x中的关键字就是分隔点,分隔成n+1个子域。

每个结点x都有以下属性:

  • x.n,当前存储在结点x中的关键字个数
  • x.n个关键字本身x.key1,x.key2,x.keyx.n,以非降序存放,使得x.key1<=x.key2<=...<=x.keyx.n
  • x.leaf,一个布尔值,如果x是叶结点,则为true,如果是内部结点,则为false
  • 每个内部结点x还包含 x.n+1 个指向其孩子的指针,x.c1,x.c2,x.cx.n+1,叶结点没有孩子,所以它们的ci属性没有定义。
  • 关键字x.keyi对存储在各子树中的关键字范围加以分割,如果ki为任意一个存储在以 x.ci 为根的子树的关键字,那么:
    k1 <= x.key1 <= k2 <= x.key2 <= ... <= x.keyx.n <= kx.n+1
  • 每个叶结点具有相同的深度,即树的高度h
  • 每个结点所包含的关键字个数有上界和下界。用一个被称为B树的最小度数的固定整数 t>=2 来表示界。
  • 除了根结点以外的每个结点必须至少有t-1个关键字,因此除了根结点以外的每个内部结点至少有t个孩子。如果树非空,根结点至少有一个关键字
  • 每个结点最多可包含 2t -1 个关键字,因为一个内部结点至多有2t 个孩子,当一个结点有 2t-1 个关键字时,该结点是满的

B树的属性有点多,尤其是最后两点,注意是除根结点以外的每个结点最少有t-1个关键字,并不是所有结点都是。

B树的特色或者精髓就在于每个结点的子结点非常多,在树高非常低的情况下就能存储大数数据。B树高度有以下定理

3、向B树中插入关键字

在普通二叉搜索树中,插入关键字,一定会新增加一个节点。但在B树中,不能简单地创建一个新的节点,新增的关键字一定是被插入已经存在的叶结点上。

如果被插入的叶结点已满,它的关键字个数为 2t-1 个,此时需要分裂结点。如图:

B树_第2张图片

结点分裂是指,将一个满结点,按其中间关键字y.keyt分裂成两个各含t-1个关键字的结点,中间关键字被提升到y的父结点。如果y的父结点也是满的怎么办呢?将y的父结点也分裂即可。

/**
 * @param x 被分裂结点的父结点
 * @param i 被分列结点在父结点中的index
 * 分裂算法并不复杂,自己绘图,搞清楚上界下界,具体index等就可以了
 */
public static void btreeSplitChild(BNode x, int i){
    BNode z = new BNode();
    BNode y = x.c[i];
    //z为分裂得到的新结点
    z.leaf = y.leaf;
    z.n = t - 1;
    //将被分裂结点的后一半关键字复制给z,同时删除后一半关键字
    for (int j = 0; j < t-1; j++) {
        z.k[j] = y.k[j+t];
        y.k[j+t] = Integer.MIN_VALUE;
    }
    //将被分裂结点的后一半子结点复制给z,同时置空后一半子结点
    if (!y.leaf) {
        for (int j = 0; j < t; j++) {
            z.c[j] = y.c[j+t];
            y.c[j+t] = null;
        }
    }
    y.n = t-1;
    //后移一位x的关键字,给分裂上来的新关键字腾位置
    for (int j = x.n-1; j >= i; j--) {
        x.k[j+1] = x.k[j];
    }
    x.k[i] = y.k[t-1];
    y.k[t-1] = Integer.MIN_VALUE;
    //后移一位x的子结点,给分裂新增加的子结点腾位置
    for (int j = x.n; j >= i+1; j--) {
        x.c[j+1] = x.c[j];
    }
    x.c[i+1] = z;
    x.n = x.n+1;
}

到目前为止,提炼给B树插入新元素的三个关键点:

  • 元素一定是插入在叶结点上的

  • 如果在查找过程中,路径上的某个结点为满结点,分裂它

  • B树的高度增加只能通过分裂增长,通常是分裂根节点实现高度增长,而不能随便添加叶结点来增高,否则会违反B树性质

    public static void btreeInsertNotFull(BNode x, int k){
      int i = x.n - 1;
      if (x.leaf) {
          //如果是叶结点,根据关键字大小排序,找出k的位置即可
          while (x.n > 0 && i >= 0 && k < x.k[i]) {
              x.k[i+1] = x.k[i];
              i--;
          }
          x.k[i+1] = k;
          x.n = x.n + 1;
      }else {
          //如果是内部结点,找出k对应的子树区域
          while (x.n > 0 && i >= 0 && k < x.k[i]) {
              i= i-1;
          }
          i = i+1;
          //如果路径中有某个结点是满结点,分裂它
          if (x.c[i].n == 2*t - 1) {
              btreeSplitChild(x, i);
              if (k > x.k[i]) {
                  i = i+1;
              }
          }
          //再次在子树中递归插入
          btreeInsertNotFull(x.c[i], k);
      }
    }
    

4、B树查找

B树查找非常简单,先在当前结点中找不大于k的关键字,如果相等则返回,如果不相等,则去对应的子结点中,递归查找。

public static void find(BNode x, int k){
    int i = 0;
    while (x.n > 0 && k > x.k[i]) {
        i++;
    }
    if (i < x.n && k == x.k[i]) {
        x.print();
        System.out.println(i);
        return;
    }
    if (x.leaf) {
        System.out.println("no result");
        return;
    }
    find(x.c[i], k);
}

关于B树删除,比插入还要复杂一些,详情可看算法导论一书,本文暂时不添加这部分内容,后续再研究。

你可能感兴趣的:(B树)