算法导论学习笔记(12)——B树

前言:  

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

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

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

    这样我们就提出了一个新的查找树结构——多路查找树。根据平衡二叉树的启发,自然就想到平衡多路查找树结构,也就是这篇文章所要阐述的第一个主题B~tree,即B树结构

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个子女。

B 树又叫平衡多路查找树。一棵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左右);这样树的深度降低了,这就意味着查找一个元素只要很少结点从外存磁盘中读入内存,很快访问到要查找的数据。

C++代码:
#pragma once
//
// B树(B-Tree)
// By Rendy
// C++版(vs2008编译通过)
// 按照《算法导论》的算法实现,节点没有父指针,不需要回溯
///

template <typename T>
class CBTree
{
  //最小度数M=2
  static const int M = 2;
  static const int KEY_MAX = 2*M-1;
  static const int KEY_MIN = M-1;
  static const int CHILD_MAX = 2*M;
  static const int CHILD_MIN = M;

  typedef struct Node{
    int   n;           //关键码的个数
    T     k[KEY_MAX];       //关键码
    Node*   c[CHILD_MAX];     //子节点指针
    bool   leaf;         //是否是叶子节点
  } NODE, *PNODE;

public:
  CBTree(void)
  {
    m_pRoot = NULL;
  }
  ~CBTree(void)
  {
    DestoryTree(m_pRoot);
    m_pRoot = NULL;
  }
 
public:
  bool Insert(T key)
  {
    //已经插入了,直接返回成功
    if (Search(m_pRoot, key))
      return true;
   
    if (NULL == m_pRoot)
      m_pRoot = CreateEmptyTree();

    if (m_pRoot->n == KEY_MAX)
    {
      PNODE pNew = NewNode();
      pNew->n = 0;
      pNew->leaf = false;
      pNew->c[0] = m_pRoot;
      SplitChild(pNew, 0);
      InsertNoneFull(pNew, key);
      m_pRoot = pNew;
    }
    else
    {
      InsertNoneFull(m_pRoot, key);
    }

    return true;
  }

  bool Delete(T key)
  {
    if (NULL == m_pRoot)
      return true;

    //不存在,不用删了
    if (!Search(m_pRoot, key))
      return true;

    if (m_pRoot->n==1)       //特殊情况处理
    {
      if (m_pRoot->leaf)
      {
        DestoryTree(m_pRoot);
        m_pRoot = NULL;
        return true;
      }
      else
      {
        PNODE pC1 = m_pRoot->c[0];
        PNODE pC2 = m_pRoot->c[1];
        if (pC1->n==KEY_MIN && pC2->n==KEY_MIN)
        {
          MergeChild(m_pRoot,0);
          DeleteNode(m_pRoot);
          m_pRoot = pC1;
        }
      }
    }
    DeleteNoneEmpty(m_pRoot, key);

    return true;
  }

  bool Find(T key)
  {
    return Search(m_pRoot, key);
  }

protected:
  bool Search(PNODE pNode, T key)
  {
    if (pNode == NULL)
      return false;

    int i = 0;
    while(i<pNode->n && key>pNode->k[i])
    {
    i++;
    }

    if (i<pNode->n && (key==pNode->k[i]))
    {
    return true;
    }
    else if (pNode->leaf)
    {
    return false;
    }
    else
    {
    return Search(pNode->c[i], key);
    }
  }

  //将pParent的第nChildIndex个孩子分裂
  bool SplitChild(PNODE pParent, int nChildIndex)
  {
    PNODE pChild = pParent->c[nChildIndex];
    //为新分裂出的节点分配空间
    PNODE pChild2 = NewNode();
    if (NULL == pChild2)
      return false;
    //与被分裂点同级
    pChild2->leaf = pChild->leaf;
    //设置分裂节点key数
    pChild2->n = KEY_MIN;

    //复制数据
    for (int i=0;i<KEY_MIN;i++)
    {
      pChild2->k[i] = pChild->k[i+M];
    }
    //如果不是叶节点,复制指针
    if (!pChild->leaf)
    {
      for (int i=0;i<CHILD_MIN;i++)
      {
        pChild2->c[i] = pChild->c[i+M];
      }
    }
    pChild->n = KEY_MIN;

    //将中间数作为索引插入到双亲节点中
    //插入点后面的关键字和指针都往后移动一个位置
    for (int i=pParent->n; i>nChildIndex; i--)
    {
      pParent->k[i] = pParent->k[i-1];
      pParent->c[i+1] = pParent->c[i];
    }
    pParent->k[nChildIndex] = pChild->k[M-1];
    pParent->n++;
    pParent->c[nChildIndex+1] = pChild2;

    return true;
  }

  void InsertNoneFull(PNODE pNode , T key)
  {
    int i = pNode->n;
    PNODE pChild = NULL;
    if (pNode->leaf)
    {
      while (i>0 && (key < pNode->k[i-1]))
      {
        pNode->k[i] = pNode->k[i-1];
        i--;
      }
      pNode->k[i] = key;
      pNode->n++;
    }
    else
    {
      while(i>0 && key < pNode->k[i-1])
      {
        i--;
      }
      pChild = pNode->c[i];
      //如果子节点key满了
      if (pChild->n == KEY_MAX)
      {
        //分裂子节点
        SplitChild(pNode, i);
        //如果大于分裂后提上来的key,插入分裂后的右边,否则左边
        if (key > pNode->k[i])
        {
          i++;
          pChild = pNode->c[i];//分裂后的右部分
        }
      }
      InsertNoneFull(pChild, key);
    }
  }

  void MergeChild(PNODE pParent, int index)
  {
    PNODE pChild1 = pParent->c[index];
    PNODE pChild2 = pParent->c[index+1];
   
    //将c2数据合并到c1
    pChild1->n = KEY_MAX;
    pChild1->k[M-1] = pParent->k[index];
    for (int i=M ; i<KEY_MAX ; i++)
    {
      pChild1->k[i] = pChild2->k[i-M];
    }
    if (!pChild1->leaf)
    {
      for (int i=M ; i<CHILD_MAX ; i++)
      {
        pChild1->c[i] = pChild2->c[i-M];
      }
    }

    //父节点删除index的key,index后的往前移动一位
    pParent->n--;
    for (int i=index ; i<(pParent->n) ; i++)
    {
      pParent->k[i] = pParent->k[i+1];
      pParent->c[i+1] = pParent->c[i+2];
    }
   
    //删除child2
    DeleteNode(pChild2);
  }

  void DeleteNoneEmpty(PNODE pNode, T key)
  {
    int i = pNode->n;
    if (pNode->leaf)                
    {
      while (i>0 && (key < pNode->k[i-1])) { i--; }  
      if (key == pNode->k[i-1])                 //1
      {
        for(int j=i ; j<pNode->n ; j++)
        {
          pNode->k[j-1] = pNode->k[j];
        }
        pNode->n--;
      }
    }
    else
    {
      while (i>0 && (key < pNode->k[i-1])) { i--; }
      if (i>0 && key==pNode->k[i-1])                 //2
      {
        PNODE pChild1 = pNode->c[i-1];
        PNODE pChild2 = pNode->c[i];
        //左Child多,从左Child移一个
        if (pChild1->n > KEY_MIN)               //2a
        {
          T preKey = GetPredecessor(pChild1);
          DeleteNoneEmpty(pChild1, preKey);
          pNode->k[i-1] = preKey;
        }
        //右Child多,从右Child移一个
        else if (pChild2->n > KEY_MIN)             //2b
        {
          T sucKey = GetSuccessor(pChild2);
          DeleteNoneEmpty(pChild2, sucKey);
          pNode->k[i-1] = sucKey;
        }
        //左右都是M-1,合并
        else                         //2c
        {
          MergeChild(pNode, i-1);
          DeleteNoneEmpty(pChild1, key);
        }
      }
      else                           //3
      {
        //含有key的子树
        PNODE pSub = pNode->c[i];
        if (pSub->n == KEY_MIN)
        {
          PNODE pLeft = (i>0 ? pNode->c[i-1] : NULL);
          PNODE pRight = (i<pNode->n ? pNode->c[i+1] : NULL);
          if (pLeft && pLeft->n > KEY_MIN)         //3a1
          {
            GetFromLeftBrother(pNode, i-1, pSub, pLeft);
          }
          else if (pRight && pRight->n > KEY_MIN)       //3a2
          {
            GetFromRightBrother(pNode, i, pSub, pRight);
          }
          else if (pLeft && pLeft->n == KEY_MIN)       //3b1
          {
            MergeChild(pNode, i-1);
            pSub = pLeft;
          }
          else if (pRight && pRight->n == KEY_MIN)     //3b2
          {
            MergeChild(pNode, i);
          }
        }
        DeleteNoneEmpty(pSub, key);  
      }
    }
  }
  T GetPredecessor(PNODE pNode)
  {
    while(!pNode->leaf)
    {
      pNode = pNode->c[pNode->n];
    }
    return pNode->k[pNode->n-1];
  }
  T GetSuccessor(PNODE pNode)
  {
    while(!pNode->leaf)
    {
      pNode = pNode->c[0];
    }
    return pNode->k[0];
  }
  //把left最大key给pParent(index),把pParent(index)给pNode
  void GetFromLeftBrother(PNODE pParent, int index, PNODE pNode, PNODE pLeft)
  {
    for(int i=pNode->n ; i>0 ; i--)
    {
      pNode->k[i] = pNode->k[i-1];
    }
    pNode->k[0] = pParent->k[index];
    if (!pNode->leaf)
    {
      for(int i=pNode->n ; i>=0 ; i--)
      {
        pNode->c[i+1] = pNode->c[i];
      }
      pNode->c[0] = pLeft->c[pLeft->n];
    }
    pNode->n++;

    //parent
    pParent->k[index] = pLeft->k[pLeft->n-1];

    //left
    pLeft->n--;


  }
  //把right最小key给pParent(index),把pParent(index)给pNode
  void GetFromRightBrother(PNODE pParent, int index, PNODE pNode, PNODE pRight)
  {
    pNode->k[pNode->n] = pParent->k[index];
    if (!pNode->leaf)
    {
      pNode->c[pNode->n+1] = pRight->c[0];
    }
    pNode->n++;

    //parent
    pParent->k[index] = pRight->k[0];

    //right
    for(int i=1 ; i<pRight->n ; i++)
    {
      pRight->k[i-1] = pRight->k[i];
    }
    if (!pRight->leaf)
    {
      for(int i=0 ; i<pRight->n ; i++)
      {
        pRight->c[i] = pRight->c[i+1];
      }
    }
    pRight->n--;
  }

  //
 
  PNODE NewNode()
  {
    PNODE pNode = new NODE;
    if (NULL == pNode)
      return pNode;

    //init key count
    pNode->n = 0;
   
    //init children
    for (int i=0 ; i<CHILD_MAX ; i++)
      pNode->c[i] = NULL; 

    return pNode;
  }
  void DeleteNode(PNODE &pNode)
  {
    if (pNode)
    {
      delete pNode;
      pNode = NULL;
    }
  }

  //

  PNODE CreateEmptyTree (){
    PNODE pRet = NewNode();
    pRet->n = 0;
    pRet->leaf = true;
    return pRet;
  }
  void DestoryTree(PNODE pNode)
  {
    if (pNode)
    {
      for(int i=0 ; i<=pNode->n ; i++)
      {
        if (pNode->leaf)
          break;
        if (pNode->c[i])
          DestoryTree(pNode->c[i]);
      }
      DeleteNode(pNode);
    }
  }

public:
  void Display()
  {
    Display(m_pRoot, 2);
    printf ( "\n" );  
  }
private:
  void Display(PNODE t, int level)
  {
    int i=0;
    if (t)
    {
      while(i<level-2)
      {
        printf(" ");
        i++;
      }
      printf ( "[ " );
      for (int i=0; i<t->n; i++)
      {
        printf ( "%d " , t->k[i]);
      }
      printf ( "]\n" );

      for (int i=0; i<=t->n; i++)
      {
        Display(t->c[i], level+3);
      }
    }
  }

private:
  PNODE m_pRoot;

};

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值