算法导论-B树

来源:互联网 发布:unity3d socket服务端 编辑:程序博客网 时间:2024/06/13 17:52

B树

(1)B树的出现 

  • 首先我想先聊聊为什么要设计B树这种数据结构??对于查找数据的结构有链表,二叉搜索树、红黑树,这三种数据结构就查找而言,红黑树的效率最高,其时间复杂度为O(log(n)),似乎已经很好了,那还要B数干嘛??
  • 这里就需要明白B数在什么情况下产生的??B数是为磁盘或其他直接存储设备而设计的一种平衡搜索树,存储的数据量是很大的,假如我们用红黑树进行储存的话,由于n是一个非常大的数,O(log(n))也是一个非常大的数,这和链表就没有什么区别了,效率非常差。而B数其实和红黑树类似,只不过B树可以有很多孩子,这样就可以远远降低树的高度。

(2)磁盘的存储结构

要了解B数,需要先了解一下磁盘驱动器的对数据读取的具体方法(其实也可以直接看B的具体实现,将这个就是好理解一点)。
驱动器由盘片、磁道、动臂、柱面构成,我们要读写信息需要找到指定的位置,位置信息有三部分组成:1、哪个盘片2、哪个磁道3、磁道上的具体位置。这个原理和B数查找元素是一样的。

(3)B树的定义

首先给出B树的几条属性:
1.对于每个节点,节点包含如下属性:
  • a.x.n,表示存储在该节点中关键字的数目
  • b.x.n个关键字是以升序的方式排列的,使得x.key1<x.key2.......<x.key(x.n)
  • c.x.left代表一个bool值,用来判断该节点是否为叶子节点,若为叶子节点则为true,否则为false。
2.每个节点内部还包含了x.n+1个指向孩子节点的指针。
3.对于孩子节点内部的任意关键字都符合下列的性质
k1<x.key1<k2<x.key2.........<k(x.n)
其中k1代表x节点第一个子节点的任意关键字,也就是说节点1的所有关键字都小于x.key1,第二个子节点关键字都介于x.key1和x.key2之间,依次类推。
4.每个叶节点都有相同的深度,即树的深度h。
5.每个节点的关键字的数目不能无限制,有一个上界和下界,这个界限可以通过固定整数t来表示,t>=2。
a.除了根节点以外关键字不能少于t-1,换句话说节点的孩子不能少于t个。
b.每个节点的关键字不能多于2t-1个,同样节点的孩子不能多于2t个
上述这两条性质是保证叶节点有相同深度很重要的性质。

(4)B数的高度

  • B数的大部分操作的时间复杂度都和B树的高度成正比,现在来分析B树的最坏情况下的高度。
定理:如果n>=1,那么对于任意一棵包含n个关键字,高度为h,最小度数t>=2的B数T,有:
  • 假设每个节点有t-1个关键字,n个节点的B树高度h达到最大值,则有:高度为h的B树T深度为1的时候至少包含2个节点,深度为2至少包含2t个节点,深度为3则有2t(2)个节点。
  • 如果对上面不理解的话,可以根据下面图来理解:
这样

(5)B树的基本操作

  • B树的基本操作有查找,插入,删除。下面我们来对它们进行介绍。

1.B树的搜索

  • B树的搜索其实和二叉树的搜索类似,也是一层一层向下搜索,通过搜索的伪代码,我来介绍搜索的整个过程:
B-TREE-SEARCH(x,k)    //x代表B数的根节点,k表示需要查找的节点    i=1    while ix.key(i)      //找到k所在的区间        i=i+1    if ix.n,且x节点为叶节点,说明没找到        return NIL                  //返回nil    else        return B-TREE-SEARCH(x.ci,k) //继续向其子节点查找
  • 伪代码的注释可以看出,整个搜索代码分为3个部分,首先找到最小下标i,如果第i个元素值与需要搜索的值相等,则直接输出;第二部分如果该节点为叶子节点,如果没有找到的话就说明B树中不存在这个元素;第三部分搜索该节点的第i个子节点,一直递归寻找。
  • 下面我们来分析一下搜索的时间复杂度,对于每个节点找到最小下标i最坏情况需要2t次搜索,搜索的节点数目是B树的高度,由上可知,高度为,所以搜索的时间复杂度为

2.创建一棵B树

  • 创建一棵B树其实就是创建一个B树的根节点,有了根节点之后就可以采用B树插入的方式对B树进行初始化操作,下面来看创建B树根节点的代码:
    B-TREE-CREATE(T)    x=ALLOCATE-NODE()    x.leaf=true    x.n=0    T.root=x

3.插入关键字

  • B树上插入一个关键字要比二叉树上插入关键字复杂得多,因为元素插进去之后,还要维护B树的性质。对于插入操作,影响最大性质的就是节点关键字个数不能超过2t-1个,如果一个节点的关键字为2t-1个,如果再进行插入,就会破坏B树的性质。
  • 对于上述问题,引入一个新的操作-分裂,就是将一个满的节点分裂从中间分裂成两个节点,左节点是t-1个关键字,右节点是t-1个关键字,中间关键字提取到父节点中,作为父节点的关键字,这里注意当父节点也是满的话,对父节点也进行分裂,具体操作同上,最终满节点的分裂会沿树向上传播。
  • 为了做到上述过程,并不是等找到B树中关键字的位置才分裂,当在沿着树往下查找新关键字的位置的过程中如果遇到满节点就产生分裂。这样,分裂一个节点时就能保证其父节点不是满的。
  • 分裂过程如上图所示,下面通过伪代码来分析分裂的整个过程:
    B-TREE-SPLIT-CHILD(x,i)  //x为分裂节点的父节点    z=ALLOCATE-NODE()       //创建一个新节点    y=x.ci                  //y指向分裂节点    z.left=y.left           //z为分裂产生的新节点,判断是否为叶节点    z.n=t-1    for j=1 to t-1        z.key(j)=y.key(j+t) //对z中节点赋值    if not y.left       //如果y为内部节点        for j=1 to t            z.c(t)=y.c(j+t)     //对子节点指针赋值    y.n=t-1             //改变y子节点的个数    for j=x.n+1 downto i+1      //将中间关键字提取上去以后,关键字后面的指针依次右移        x.c(j+1)=x.c(j)    x.c(i+1)=z    for j=x.n down to i         //x中的关键字右移        x.key(j+1)=x.key(j)    x.key=y.key(t)    x.n=x.n+1           //修改x中关键字的个数            
  • 通过上述伪代码的注释对分裂过程有了一个初步的了解,节点的分裂主要分为两部分,一部分是创建一个新节点,将原来满节点的关键字和指向子节点的指针赋值给该新节点;一部分是修改父节点,将父节点中的指针和关键字都右移一位,再修改父节点中关键字的个数。
  • 在分裂的基础上,对B树的插入过程进行详细介绍,同样还是利用伪代码来了解整个过程:
  • B-TREE-INSERT(T,k)    r=T.root        //将r指向B树的根节点if r.n==2t-1        //如果该节点已满    s=ALLOCATE-NODE()   //产生一个新节点,    T.root=s           // 如果根节点已满,就产生一个新的节点作为根节点    s.left=false    s.n=0    s.c1=r    B-TREE-SPLIT-CHILD(s,1)     //分裂s的第一个子节点,即原来的根节点    B-TREE-INSERT-NONFULL(s,k)  //向下寻找k的位置else    B-TREE-INSERT-NONFULL(s,k)  //直接寻找k的位置    
  • B-TREE-INSERT-NONFULL(x,k)    i=x.n       //i记录下x节点的数目    if(x.left)  //如果x的叶节点        while i>=1 and k=1 and kx.key(i)       //判断k应该在分裂后的左右节点的作为子节点进行插入                i=i+1        B-TREE-INSERT-NONFULL(x.c(i),k)                


  • 第一段伪代码的过程如上图所示,由于根节点较特殊,我们将其分开进行处理,首先判断根节点是否为满,如果满了的话产生一个新节点作为B树的根节点,同时将图中节点r分裂成两个子节点,B-TREE-INSERT-NONFULL完成了将关键字k插入非满的根节点的B树中。
  • 对于伪代码B-TREE-INSERT-NONFULL,这段伪代码主要由两部分组成,第一部分是当节点x为叶节点时,k后面的元素依次右移,然后留出一个元素来放插入元素k,x节点关键数个数x.n加一。第二部分是当x不为叶节点时,首先找到关键字k插入的子节点x.c(i),如果该节点已满就分裂,然后递归调用B-TREE-INSERT-NONFULL查找到k所在的叶节点,进行插入。

  • 上图中简要介绍了插入操作中各种可能的过程,有助于理解插入操作流程。

4.删除关键字

  • B树的删除操作和插入操作类似,都不仅仅将元素删除就完成了,而是还要对B树的性质进行维护,因为B树每个节点的关键字只要要有t-1个,删除关键字可能会出现节点关键字少于t-1个关键字的情况。
  • 下面针对删除操作会出现的几种情况来进行分析:
  • 情况1:如果删除关键字在叶节点中,则直接将其删除,如上图(b)所示(这里不用担心叶节点的关键字少于t-1个,因为下面要说的情况3会保证每个子节点都至少有t个关键字)。
  • 情况2:如果删除关键字为内部节点,则又分成如下几种情况进行分析:

a.如果删除关键字前一个子节点y的关键字的个数至少为t个,这将该关键字删除,同时将y中最后一个关键字k'(也称为y的前驱)代替关键字k的位置(如上图c所示)。

b.如果删除关键字前一个子节点y的关键字的个数为t-1个,则检测后于关键字k的节点z,如果z的关键字至少为t个,则将关键字k删除,然后将节点z的第一个关键字k'(也称为节点z的后驱)代替k的位置。

c.如果上述两个节点关键字个数都只有t-1个,则将这两个节点合并成一个节点,同时删除关键字k以及指向节点z的指针(如上图G所示)

  • 情况3:如果关键字k不在内部节点当中,则需要确定包含关键字k的节点x.c(i),如果节点x.c(i)只有t-1个关键字,就需要采取如下措施保证子节点的个数至少为t

a.如果x.c(i)的关键字只有t-1个,但是其兄弟节点的关键字至少有t个,则将x的一个关键字降到x.c(i)中,然后从左兄弟或右兄弟提取一个关键字升到节点x中。

b.如果x.c(i)的关键字只有t-1个,但是其兄弟节点的关键字也只有t-1个,则将x.c(i)节点和一个兄弟节点合并,并将x中的一个关键字(大小介于两个节点之间)放到合并节点中,作为中间关键字。

尽管删除操作看起来很复杂,但是其时间复杂度只有,总体而言效果还不错。