一些概念:
当集合较小贮存在内存中时,相应的搜索方法称为内搜索。
如果文件较大,数据必须存放在外存中,则在外存中搜索给定关键字值的元素的方法称为外搜索。
对于磁盘上的大文件,用树形结构表示时,则其指针域的值不是内存地址而是磁盘存储器的地址。如果这里仍使用二叉树,则要磁盘读取多次才能找到正确的记录,而磁盘操作时间远慢于内存读写。为了提高速度,则考虑用空间换时间,使用多叉树代替二叉树。
对于B树,一般用于外搜索,在文件的一定位置存放索引即B树的节点,节点中存放关键值和具体地址指针,指针指向下一个节点或者磁盘中数据的位置。对磁盘数据搜索时,通过查询这些索引来加快搜索速度。
b树首先是m叉树。然后还拥有以下特性:
每个节点至多m棵子树。
除根节点外,其他每个分支节点至少有┌m/2┐ 即ceil(m / 2)个孩子(其中ceil(x)是为取不小于x的最小整数的函数,与开口向上取不小于x的最大整数的函数类似,都是可以等于x的,ceil函数在math.h中)
若根节点不是叶子节点,则至少有两个孩子。
所有叶子节点都在同一层。
有i个孩子的节点有i-1个关键值,且关键值递增。
然后考虑一下b树中的数学问题。
1.对于根节点,至少有两个孩子。则第1层至少2个节点,对于第1层,至少每个节点至少有┌m/2┐个孩子。则第2层至少有 2 * ┌m/2┐个节点,而对于之后的每一层的节点都至少有 ┌m/2┐个孩子,所以第3层上至少有 2 * ┌m/2┐ ^2,依次类推,第n层有 2* ┌m/2┐^(n-1)个节点。则对于x层有节点数 N个,则有N>= 2 * ┌m/2┐ ^(n-1),计算可获得相应的层数,即 x <= log ( ┌m/2┐)(N/2) +1 。
2.若B树的失败节点数为s,则其元素总数N为N = s-1。元素数目为拥有的关键字数目,失败节点为NULL。则对于n个非失败节点, 每个节点的指针数都比为自身元素数多1,而失败节点中无元素,则所有节点的指针数t比总元素数N多了n个,即 t = N + n。又指针总数为所有节点数减去根节点,即 t = n + s -1,则有 N + n = n + s - 1,即N = s - 1.
3.N个元素的m阶B树的高度h有 h <= 1 + log(┌m/2┐)((1+N)/2),是接1和2的分析,更加有用一点,这里N为元素总数,其失败节点的个数为 N+1,使用2,然后失败节点所在的层数满足1,则B树的高度为 失败节点的高度减1。这里的层数都是从0也就是以根节点为0层开始计算的。
b树的插入:
先搜索到固定位置,与当前节点比较,然后选择下一个节点,直到到达叶子节点,其指向的下一个节点(在我们建立的B数结构中是NULL,而在真实的外搜索中是一个数组的起始地址或链表的起始地址) 即为所要插入的位置。若遇到相同关键值,则说明重复,返回插入失败。将关键值插入当前节点中,添加响应的NULL子节点。然后判断当前节点是否需要分割。即当前节点关键值数量大于m-1或子节点数大于m,若不需要分割,则结束。若要分割,将┌m/2┐节点 向上插入到父节点中。然后再判断父节点是否需要分割,依次向上遍历。由于其是向上传递的,传到根节点后,根节点分割,产生一个新的根节点,也就是其高度是向上增长的,保证了所有叶子节点在同一层。
在插入这里要思考一下我要实现b树时的数据结构,每个节点最少有(┌m/2┐-1)个关键字,┌m/2┐个指针,最多有m-1个关键字,m个指针,那如何保存这些数据呢。首先肯定要有一个数据来标识当前节点的指针数量,如果超过固定值,就进行分割。如果使用一个动态数组,即任何时候,储存关键字和指针的内存大小都与现在储存的数据大小相同,那么每次插入时都要重新向操作系统申请一块新的空间来储存,看似节约了空间,但导致浪费在申请空间上的时间浪费以及新旧空间更替的空间浪费。而且b树是外查询,其结构是储存在磁盘上的,在磁盘上分配了一块空间储存数据后由于一个经常做的操作而要频繁的在磁盘中放弃空间以及寻找新的空间来储存数据,这是不现实的。所以最好是用一个固定最大大小的数组来储存这些数据,用当前指针数量来标识当前数组大小,浪费一定的空间,以保证磁盘储存的结构保存正确规范。
b树的删除:
b树也是先使用到向下旋转,将问题简化。这里若删除的元素不在叶子节点中,向下旋转,将其与关键字右边子树中的最小元素替代,然后在对替换后的元素进行判断,或者直接选择关键字的中序遍历的后继节点。这里还是一层一层地向下旋转比较好。
替换后,从叶子节点中删除关键字和一个空指针。删除后,若不满足B树的节点条件,则要发生下溢。下溢时,先看节点的左兄弟,再看节点的右兄弟,若有多于的关键字,则向其兄弟借一个关键字。若其左兄弟有多余的关键字,先将位于兄弟之间的父节点的关键字给予子节点,放在子节点的最左边,然后将左兄弟最右边的关键字给父节点,其最右边的指针放在删除节点的最左边。当右兄弟节点有多余的关键字时,也进行相似的操作:将父节点相应位置的关键字给删除节点,而兄弟节点最左边的关键字给父节点,最左边的指针换到删除节点的最右边。
若左右兄弟节点都无多于关键字时,进行连接,当有左兄弟节点时,左兄弟节点加位于中间的父节点关键字与删除节点结合,生成一个新的节点,且用父节点下溢关键字的左侧指针指向,而删除其右侧指针,或者说直接在做兄弟节点上增加关键字与指针。若没有左兄弟节点,则使用右兄弟节点,进行相似的操作。由于B树所有叶子节点在同一层,所以一个删除节点必定有其兄弟节点,因为所有的非叶子节点至少有2个指针。
试着写出代码,好吧,其实b树比红黑树还要复杂。。。而且我写的还只是简单的数据结构,没有结合外排序的特性,没有将失败节点设置为指向磁盘地址的指针。但这已经够复杂了。。。
#ifndef BTREE_FILE #define BTREE_FILE #include <iostream> template< typename T> struct BNode { unsigned int cnum;//当前节点 孩子数目 T* key;//多预留一位,导致插入时能插入,然后检测分割时,再归于正常。 BNode** child; BNode* father; BNode(unsigned int m){ key = new T[m+1]; child = new BNode*[m+2]; cnum = 0; father = 0; } ~BNode(){ delete[] key; delete[] child; } }; template<typename T> class BTree{ public: BTree(); BTree(unsigned int m); bool Insert(T t); bool DeleteKey(const T &t); const unsigned int M; const unsigned int MIN;//最小孩子树 ~BTree(); BNode<T>* root; void Print(){ if( 0 == root )return; BNode<T>* ns[2][10010]; BNode<T>* p; p = root; unsigned int n[2],i,j,old = 0,h = 0; n[old]++; n[0] = 1; n[1] = 0; ns[ old ][0] = root; while(p){ cout<<h++<<" 层: "<<endl; n[1-old] = 0; for(i = 0; i < n[old];i++){ p = ns[ old ][i]; for(j = 0 ; j < p-> cnum -1 ; j++){ cout<<p->key[j]<<","; } for(j = 0 ;j < p-> cnum;j++){ ns[ 1- old] [ n[1-old] ++ ] = p->child[j]; } cout<<" "; } cout<<"\n\n"; old = 1-old; p = p->child[0]; } } private: void SplitNode(BNode<T>* p); void UnionRInLNode(BNode<T>* fa,unsigned int &site);//将左右子树与父节点中响应关键字合并 void UnionLInRNode(BNode<T>* fa,unsigned int &site); void AddValueInNode(T &t,BNode<T>* &p,BNode<T>* child);//将两个调整操作提出来,增加时,要考虑孩子指针的插入 void DeleteSiteInNode(const unsigned int s,BNode<T>* &p);//只进行增加和删除,其他调整如分割和旋转由上层的函数进行 void DeleteNode(BNode<T>* p); void DownChange(BNode<T>* &p,unsigned int &site); void CheckNode(BNode<T>* p); unsigned int GetSiteInFa(BNode<T>* &p,BNode<T>* &c); }; template <typename T> BTree<T>::BTree():M(4),MIN(2){ root = 0; } template <typename T> BTree<T>::BTree(unsigned int m):M(m),MIN(ceil((double)m/2)){ root = 0; } template <typename T> BTree<T>::~BTree(){ } template <typename T> void BTree<T>::DeleteNode(BNode<T>* p){ if(0 == p) return ; for(int i =0 ; i < p->cnum;i++){ DeleteNode(p->child[i]); } delete p; return ; } template <typename T> bool BTree<T>::Insert(T t){ if(0 == root){ root = new BNode<T>(M); root->cnum = 2; root ->key[0] = t; root->child[0] = 0; root->child[1] = 0; root->father = 0; return true; } BNode<T>* fa,*p;// fa = p =root; unsigned int i ; while(p){ for(i = 0 ; i < p->cnum -1 ;i++){ if(t == p->key[i]) return false; else if( t < p->key[i]) break; } fa = p; p = p->child[i]; } AddValueInNode(t,fa,0); SplitNode(fa); return true; } template<typename T> void BTree<T>::SplitNode(BNode<T>* p){ if( 0 == p || p->cnum <= M) return; BNode<T> *l,*r,*newfa; unsigned int place = ceil( (double)M/2) -1;//place为关键字所在位置。 l = p; //分割节点时,将节点删除,不用删除,只要将位数确定即可。因为从place也就是上传节点开始之后的孩子指针以及关键字都是无效的了 l->cnum = place + 1; r = new BNode<T>(M); r->cnum = M - place; unsigned int i,j; for(i = place+1,j = 0 ; i < M ;){ r->key[j++] = l->key[i++]; } for(i = place+1,j=0;i<= M;){ r->child[j++] = l->child[i++]; } if(0 != r ->child[0]){ for(i = 0; i < r->cnum;i++){ //cout<<r->child[i]->key[0]<< "old f:"<< r->child[i]->father->key[0]<<endl; r->child[i]->father = r; //cout<<r->child[i]->key[0]<< "new f:"<< r->child[i]->father->key[0]<<endl; } } if( 0 == p->father){//如果为根节点,要创建新的根节点 newfa = new BNode<T>(M); root = newfa; root->child[0] = l; root->child[1] = r; root->key[0] = l->key[place]; root->father = 0; root->cnum = 2; l->father = newfa; r->father = newfa; }else{ newfa = p->father; //l->father = newfa; r->father = newfa; AddValueInNode(l->key[place],newfa,r); SplitNode(newfa); } return ; } template <typename T> void BTree<T>::AddValueInNode(T &t,BNode<T>* &p,BNode<T>* child){ T *buff = new T[ p->cnum]; BNode<T>** bufN = new BNode<T>*[p->cnum+1]; unsigned int i = 0,j,k; for(j = 0 ; j < p->cnum -1;j++){ if(t < p->key[j]) break; i++; } bufN[0] = p->child[0]; for(j = 0,k = 0 ; j < p->cnum ;j++){ if(j == i){ buff[j] = t; bufN[j+1] = child; } else{ buff[j] = p->key[k]; bufN[1+j] = p->child[k+1]; k++; } } for(i = 0;i<p->cnum;i++){ p->key[i] = buff[i]; p->child[i] = bufN[i]; } p->child[i] = bufN[i]; p->cnum ++; delete[] buff; delete[] bufN; } template <typename T> bool BTree<T>::DeleteKey(const T &t){ BNode<T> *fa,*p; if(0 == root)return false; p = root; fa = p; unsigned int i; bool Found = false; while( !Found && p ){ fa = p; for(i = 0 ; i < p->cnum - 1;i++){ if(t <p->key[i]) break; else if(t == p->key[i]){ Found = true; break; } } p = p->child[i]; } if(Found){ p = fa; if(0 != p->child[0]){//如果是叶子节点,要考虑一下是否为根节点的情况,之后的删除也要考虑根节点 DownChange(p,i); } //检测根节点时要注意,对于根节点,至少有一个关键字。 if(root == p && p->cnum == 2){//删除根节点,导致树为空 delete p; root = 0; return true; } DeleteSiteInNode(i,p); CheckNode(p); return true; }else return false; } template <typename T> void BTree<T>::DeleteSiteInNode(unsigned int s,BNode<T>* &p){ unsigned int i,j,k = 0 ; if(s == 0)k = 0;//对于位置为0即删除第一个关键字,删除其第一个指针。其他都为后一个指针 else k = s+1; for( i = 0,j = 0 ; j < p->cnum;j++){ if(j != k ){ p->child[i++] = p->child[j]; } } p->cnum--; for( i = 0,j = 0 ; j < p->cnum;j++){ if(j != s ){ p->key[i++] = p->key[j]; } } return; } template <typename T> void BTree<T>::CheckNode(BNode<T>* p){ if(p == root){ if( p->cnum > 1 ) return; //根节点没有关键字时,删除根节点,高度减一。对于造成空树的判断已在删除函数中进行了判断 if( p->cnum == 1){ BNode<T>* d = p; root = p->child[0]; delete d; return ; } } if(p->cnum >= MIN)return; BNode<T>* fa = p->father,*l = 0,*r = 0; unsigned int fsite = GetSiteInFa(fa,p); bool LEnough = false,REnough = false; if(fsite>0 ){ l = fa->child[fsite-1]; if(l->cnum > MIN) LEnough =true; }else if(fsite < fa->cnum-1 && fa->child[fsite + 1] ->cnum >MIN){ r = fa->child[fsite+1]; if(r->cnum > MIN) REnough = true; } if(LEnough){//取左兄弟的最右 关键字 //这里的删除操作,应该移动的是右兄弟的最左孩子指针,而不是第2个指针。 AddValueInNode(fa->key[fsite - 1],p,l->child[ l->cnum-1 ] ); if(0 != l->child[ l->cnum-1 ]) l->child[ l->cnum-1 ]->father = p; fa->key[fsite-1] =l->key[ l->cnum -2]; DeleteSiteInNode(l->cnum -2,l); }else if(REnough){//取右兄弟的最左 关键字 AddValueInNode(fa->key[ fsite ],p,r->child[ 0]); if(0 != r->child[ 0] ) r->child[ 0] -> father = p; fa->key[fsite ] = r->key[0]; DeleteSiteInNode(0,r); }else {//使用相应关键字与左右子树合并 if(0 != l) fsite --; UnionRInLNode(fa,fsite); CheckNode(fa); } return; } template <typename T> void BTree<T>::UnionRInLNode(BNode<T>* fa,unsigned int &site){//使用site为父节点对应的中间关键字的下标。 BNode<T>* l = fa->child[site],*r = fa->child[site+1]; unsigned int kn,cn,i; kn = l->cnum -1; cn = l->cnum; l->key[kn++] = fa->key[site]; for(i = 0 ; i < r->cnum;i++){ l->child[cn++] = r->child[i]; } for( i = 0; i < r->cnum -1;i++){ l->key[kn++] = r->key[i]; } //对于右合并入左子树,要删除其父节点中的关键字,一般都是删除关键字后的也就是指向右节点的指针,但当位置为0时,由于删除函数的特殊情况,要考虑 if( 0 == site) fa->child[site+1] = l; l->cnum = cn; DeleteSiteInNode(site,fa); delete r; } template <typename T> //有问题, void BTree<T>::UnionLInRNode(BNode<T>* fa,unsigned int &site){ BNode<T> *l = fa->child[site],*r = fa->child[site+1]; unsigned int kn,cn,i; kn = l->cnum -1; cn = l->cnum; l->key[kn++] = fa->key[site]; for(i = 0 ; i < r->cnum;i++){ l->child[cn++] = r->child[i]; } for( i = 0; i < r->cnum -1;i++){ l->key[kn++] = r->key[i]; } //对于右合并入左子树,要删除其父节点中的关键字,一般都是删除关键字后的也就是指向右节点的指针,但当位置为0时,由于删除函数的特殊情况,要考虑 //由于父节点删除关键字的右节点删去,而且关键字已删,且左节点中内容已改为 本该合并在右节点后的最终数据,所以,直接保留左节点即可 if( 0 == site) fa->child[site+1] = l; //结果发现,其实向右节点合并实际操作与向左节点合并操作完全一样。。。 DeleteSiteInNode(site,fa); delete r; } template <typename T> unsigned int BTree<T>::GetSiteInFa(BNode<T>* &p, BNode<T>* &c){//返回的是节点的位置 unsigned int i = 0; for(;i<p->cnum-1;i++){ if(p->child[i] == c) break; } return i; } template <typename T> void BTree<T>::DownChange(BNode<T>* &p,unsigned int &site){ if(0 == p->child[0]) return; BNode<T>* c = p->child[site+1],*fa = p; while(c){ fa = c; c = fa->child[0]; } T temp = p->key[site]; p->key[site] = fa->key[0]; fa->key[0] = temp; p = fa; site = 0; return ; } #endif
分裂 合并时要考虑节点的子孩子的父节点的指向问题,这个让我找了半天的错。感觉b树要比红黑树还要坑一点,二叉树的结构很容易理解 和操作,M叉树,我使用一个数组还保存节点以及关键字信息的话,比较任意出错,要仔细思考其中各值的关系才行。
原文地址:http://blog.csdn.net/luo_xianming/article/details/39436199