B树的理解和实现

来源:互联网 发布:会计从业资格考试软件 编辑:程序博客网 时间:2024/06/08 05:51

B树是多路搜索树的一个演变。由于在大数据存储中,二叉查找树由于深度过大,而造成磁盘I/O读写过于频繁,进而导致效率底下,因此产生了B树

什么是B树?假设下面是一颗m阶的B树

1、树中每个节点至多有m颗子树

2、如果根节点不是叶子节点,则至少有2颗子树(也就是至少有一个关键字)

3、除根节点外,所有非终端节点至少有m/2颗子树(也就是至少有(m/2-1)个关键字)

4、所有叶子节点都出现在一个层次上

5、所有非终端节点都包括以下信息数据:(n , A0 , K1,A1,K2,........,Kn,An) ,  其中Ki表示关键字,Ki < K(i+1) , Ai表示指向子树的指针 , n表示有多少关键字。A0中的数据 < K1 , A1中的数据  > K1 并且 < k2 ..............An中的数据 > K(n)


B树实现:

1、B树的插入

刚开始学习B树时 , 看到B树的定义要求说:所有叶子节点出现在同一层次。对于这点我就非常疑惑,让所有叶子出在同一层次,这要怎么样才能实现。看到后面才懂得B树实现的智慧。

B树和二叉树的上的插入不同,关键字不是在叶子节点上插入,而是在底层的某个非终端点中添加,这就是自低向上增长,也就是说,在插入的过程中,如果这个点是最底层的一个点,那么这个点永远都是最底层的一个点。

在插入过程中,如果底层的某个结点上关键字的数量达到了m,那么这个结点就进行分裂,然后把分裂出来的那个关键字放到父亲结点中,如果该父亲结点中的关键字也达到了m,那么就继续分裂,分裂出的关键字也放到它的父亲节点中,一直持续这样向上,到达根结点时,如果根结点也满足条件,那么这时就对根结点进行分裂,进而产生一个新的根结点,这颗B树也增加了一个高度。

这就是插入时的思想。


插入代码:

void tree_B::insert(int x){if(find(x))  return ;  //如果该关键字已经存在 , 那么就不需要插入了int i ;node *r , *p , *s;int pd = (len+1)/2;if(root->isLeaf)//如果在当前树中只有一个节点 {if(root->count<len){i = root->count;for( ; i >= 1; i--){if(x > root->data[i])break;else root->data[i+1] = root->data[i];}root->data[i+1] = x;//注意是i+1不是iroot->count += 1;}else{p = root;r = new(node);r->count = 1;r->isLeaf = false;r->data[1] = root->data[pd];root = r;splitChild(r , p , 0 , pd);if(x < root->data[1])  p = root->next[0];//开始插入xelse  p = root->next[1];for(i = p->count ; i >= 1; i--){if(x > p->data[i])break;else p->data[i+1] = p->data[i];}p->data[i+1] = x;p->count += 1;//disp();}}else{s = root;if(s->count == len)//判断根节点上的关键子是否已经达到了len个{node *r1 = new(node);r1->count = 1;r1->isLeaf = false;r1->data[1] = root->data[pd];root = r1;//新的根节点splitChild(r1 , s , 0 , pd);root->next[0]->isLeaf = root->next[1]->isLeaf = false; //在这里可以知道,拆分出来的两个节点肯定是内节点}s = root;/*因为在插入的时候,我们不能插在关键字已经达到len个的叶子上,因此,我们在寻找该叶子时,就把路径上的一些已经达到len个关键的节点进行拆分*/while(1) {if(s->isLeaf)  break;r = s;for(i = s->count; i >= 1;i--) //查找x的下一个点{if(x > s->data[i]) break;}int j = i;s = s->next[i];//注意,这里是iif(s->count == len){for(i = r->count; i>= j+1;i--){r->data[i+1] = r->data[i];r->next[i+1] = r->next[i];}r->data[i+1] = s->data[pd];//注意,是i+1r->count += 1;splitChild(r , s , i , pd);if(!s->isLeaf)  r->next[i+1]->isLeaf = false;  //如果s节点是内节点,那么拆分出的节点肯定也是内节点if(x > r->data[i+1])  s = r->next[i+1];//因为这里已经改变了B树 , 所以也要改变s的指向}}for(i = s->count; i >= 1; i--){if(x > s->data[i])  break;s->data[i+1] = s->data[i];}s->data[i+1] = x;s->count += 1;}}

2、B树的删除

B树的删除相对于排序二叉树来说,更麻烦。因为B树始终要保持(1、所有叶子节点都出现在同一层次; 2、除根节点外,其他所有节点的至少有m/2颗子树)。

1、如果删除的关键字在底层结点上,那么就先直接删除。

2、如果删除的关键字没在底层节点上,那么我们就用该结点的儿子上的某个关键字来代替,直到删除的关键字在底层结点上。

3、对于上面两种情况,如果最后的底层结点不满足B树的要求,那么我们要进行调整:

         (1)、被删除关键字结点的剩余关键字数 + 该结点旁边结点的关键字树 >= 2*(m/2-1) , 那么我们就从旁边的结点借一个关键字,进而进行调整,从而实现了B树的要求,推出调整

          (2)、如果条件(1)不满足,那么就要让该结点跟其旁边的结点进行合并,合并之后,我们需要判断其父亲结点是否满足B树的要求,进而回到3 , 继续进行调整。


B树删除实现代码:

void tree_B::dele(int x){if(!find(x))  return ;//该节点不存在stack<node *>rb;//存储下面遍历时,每个被遍历点的父亲节点stack<int>rm;  //储存在节点在父亲节点中的位置node *s  = root, *r , *p;int i , j , pd = (len+1)/2;bool find_x = false;while(true) //先找到删除点x在树中的位置{for(i = s->count; i >= 1; i--){if(x > s->data[i]) break;else if(x == s->data[i]){find_x = true;break;}}if(find_x)  break;rb.push(s);  //存储下一个节点的父亲节点rm.push(i);  //存储下一个节点在父亲节点的位置s = s->next[i];}if(s->isLeaf) //当该节点是叶子节点时{for(j = i+1; j <= s->count; j++)//先对改叶子进行调整s->data[j-1] = s->data[j];s->count -= 1;}else //当该节点不是叶子节点时 , 调整到叶子处{/*如果被删除的点不是在叶子上,那么我们就先用该点的儿子把其取代*/while(s){/*这里我们在考虑用(关键字数多的儿子)来取代被删除点*/if(s->next[i]->count >= s->next[i-1]->count) {s->data[i] = s->next[i]->data[1];rb.push(s);rm.push(i);s = s->next[i];j = 1;if(s->isLeaf)  break;i = 1;}else{s->data[i] = s->next[i-1]->data[s->next[i-1]->count];rb.push(s);rm.push(i-1);s = s->next[i-1];j = s->count;if(s->isLeaf)  break;i = s->next[i-1]->count;}}//一直用下面的节点取代上面的节点 , 最后到达了叶子 , 这时就调整该叶子节点for( ; j < s->count; j++){s->data[j] = s->data[j+1];s->next[j] = s->next[j+1];}s->count -= 1;}//如果这个叶子的关键字 < (pd-1) , 那么就要进行调整while(!rb.empty())  //向上开始调整{if(s->count >= (pd-1))  break; //被调整那个点满足条件,那么就可以推出了p = rb.top();  rb.pop();i = rm.top();  rm.pop();/*如果被调整的点跟其左相邻或右相邻点的‘关键字’之和 > 2*(pd-1) , 那么我们就可以从其左相邻点或右相邻点借一个关键字 (这里我们知道只需要接一个就行了)*/if(i != 0 && (p->next[i]->count + p->next[i-1]->count) >= 2*(pd-1)){for(j = s->count; j >= 1; j--){s->data[j+1] = s->data[j];s->next[j+1] = s->next[j];}s->count += 1;s->data[1] = p->data[i];s->next[1] = p->next[i-1]->next[p->next[i-1]->count];p->data[i] = p->next[i-1]->data[p->next[i-1]->count];p->next[i-1]->count -= 1;break;}else if(i != p->count && (p->next[i]->count + p->next[i+1]->count) >= 2*(pd-1)){s->count += 1;s->data[s->count] = p->data[i+1];s->next[s->count] = p->next[i+1]->next[1];p->data[i+1] = p->next[i+1]->data[1];for(j = 2; j <= p->next[i+1]->count; j++){p->next[i+1]->next[j-1] = p->next[i+1]->next[j];p->next[i+1]->data[j-1] = p->next[i+1]->data[j];}p->next[i+1]->count -= 1;break;}/*被删除点不满足借代的条件,那么就只能让被删除点跟其相邻的一个点进行合并。(这里我们确定合并之后的关键字肯定会 < len)*/if(i == 0) {r = p->next[i];s = p->next[++i];}else r = p->next[i-1];int &next_x = r->count;//通过引用 , 下面调用就能自动改变其countr->data[++next_x] = p->data[i];r->next[next_x] = s->next[0];//两个节点合并,先把0位置的节点安置好for(j = 1; j <= s->count;j++){r->data[++next_x] = s->data[j];r->next[next_x] = s->next[j];}delete(s); //删除if(rb.empty() && p->count == 1) //如果根节点root只有一个关键字,那么这时根节点就是r了 {delete(root);root = r;break;}for(j = i+1; j <= p->count; j++)  //调整根节点{p->data[j-1] = p->data[j];p->next[j-1] = p->next[j];}p->count -= 1;s = p;}}

以上就是B树的实现,把代码写出之后,有一种豁然开朗的感觉,倍儿爽啊。


下面给出B树实现的整体代码:

//B树一个非常的特点就是,从上往下增长#include <iostream>#include <stdio.h>#include <stack>#include <queue>using namespace std;const int order = 100;typedef struct B//节点结构体{int count; //记录有多少关键子bool isLeaf; //记录这个节点是否是叶子节点int data[order]; //存储关键字struct B *next[order]; //存储指针B(bool b=true, int n=0)  //类似于class中的构造函数:isLeaf(b),count(n){} }node;class tree_B//{public:tree_B() {}tree_B(int x){len = x;root = new(node);}void insert(int x); //插入元素void dele(int x); //删除元素int find(int x); //查找元素void disp();~tree_B()  {  }private:int len;node *root;};void splitChild(node *r , node *p , int wb ,int pd); //拆分节点 , wb是指拆分后的节点放在wb这个位置void tree_B::insert(int x){if(find(x))  return ;  //如果该关键字已经存在 , 那么就不需要插入了int i ;node *r , *p , *s;int pd = (len+1)/2;if(root->isLeaf)//如果在当前树中只有一个节点 {if(root->count<len){i = root->count;for( ; i >= 1; i--){if(x > root->data[i])break;else root->data[i+1] = root->data[i];}root->data[i+1] = x;//注意是i+1不是iroot->count += 1;}else{p = root;r = new(node);r->count = 1;r->isLeaf = false;r->data[1] = root->data[pd];root = r;splitChild(r , p , 0 , pd);if(x < root->data[1])  p = root->next[0];//开始插入xelse  p = root->next[1];for(i = p->count ; i >= 1; i--){if(x > p->data[i])break;else p->data[i+1] = p->data[i];}p->data[i+1] = x;p->count += 1;//disp();}}else{s = root;if(s->count == len)//判断根节点上的关键子是否已经达到了len个{node *r1 = new(node);r1->count = 1;r1->isLeaf = false;r1->data[1] = root->data[pd];root = r1;//新的根节点splitChild(r1 , s , 0 , pd);root->next[0]->isLeaf = root->next[1]->isLeaf = false; //在这里可以知道,拆分出来的两个节点肯定是内节点}s = root;/*因为在插入的时候,我们不能插在关键字已经达到len个的叶子上,因此,我们在寻找该叶子时,就把路径上的一些已经达到len个关键的节点进行拆分*/while(1) {if(s->isLeaf)  break;r = s;for(i = s->count; i >= 1;i--) //查找x的下一个点{if(x > s->data[i]) break;}int j = i;s = s->next[i];//注意,这里是iif(s->count == len){for(i = r->count; i>= j+1;i--){r->data[i+1] = r->data[i];r->next[i+1] = r->next[i];}r->data[i+1] = s->data[pd];//注意,是i+1r->count += 1;splitChild(r , s , i , pd);if(!s->isLeaf)  r->next[i+1]->isLeaf = false;  //如果s节点是内节点,那么拆分出的节点肯定也是内节点if(x > r->data[i+1])  s = r->next[i+1];//因为这里已经改变了B树 , 所以也要改变s的指向}}for(i = s->count; i >= 1; i--){if(x > s->data[i])  break;s->data[i+1] = s->data[i];}s->data[i+1] = x;s->count += 1;}}void tree_B::dele(int x){if(!find(x))  return ;//该节点不存在stack<node *>rb;//存储下面遍历时,每个被遍历点的父亲节点stack<int>rm;  //储存在节点在父亲节点中的位置node *s  = root, *r , *p;int i , j , pd = (len+1)/2;bool find_x = false;while(true) //先找到删除点x在树中的位置{for(i = s->count; i >= 1; i--){if(x > s->data[i]) break;else if(x == s->data[i]){find_x = true;break;}}if(find_x)  break;rb.push(s);  //存储下一个节点的父亲节点rm.push(i);  //存储下一个节点在父亲节点的位置s = s->next[i];}if(s->isLeaf) //当该节点是叶子节点时{for(j = i+1; j <= s->count; j++)//先对改叶子进行调整s->data[j-1] = s->data[j];s->count -= 1;}else //当该节点不是叶子节点时 , 调整到叶子处{/*如果被删除的点不是在叶子上,那么我们就先用该点的儿子把其取代*/while(s){/*这里我们在考虑用(关键字数多的儿子)来取代被删除点*/if(s->next[i]->count >= s->next[i-1]->count) {s->data[i] = s->next[i]->data[1];rb.push(s);rm.push(i);s = s->next[i];j = 1;if(s->isLeaf)  break;i = 1;}else{s->data[i] = s->next[i-1]->data[s->next[i-1]->count];rb.push(s);rm.push(i-1);s = s->next[i-1];j = s->count;if(s->isLeaf)  break;i = s->next[i-1]->count;}}//一直用下面的节点取代上面的节点 , 最后到达了叶子 , 这时就调整该叶子节点for( ; j < s->count; j++){s->data[j] = s->data[j+1];s->next[j] = s->next[j+1];}s->count -= 1;}//如果这个叶子的关键字 < (pd-1) , 那么就要进行调整while(!rb.empty())  //向上开始调整{if(s->count >= (pd-1))  break; //被调整那个点满足条件,那么就可以推出了p = rb.top();  rb.pop();i = rm.top();  rm.pop();/*如果被调整的点跟其左相邻或右相邻点的‘关键字’之和 > 2*(pd-1) , 那么我们就可以从其左相邻点或右相邻点借一个关键字 (这里我们知道只需要接一个就行了)*/if(i != 0 && (p->next[i]->count + p->next[i-1]->count) >= 2*(pd-1)){for(j = s->count; j >= 1; j--){s->data[j+1] = s->data[j];s->next[j+1] = s->next[j];}s->count += 1;s->data[1] = p->data[i];s->next[1] = p->next[i-1]->next[p->next[i-1]->count];p->data[i] = p->next[i-1]->data[p->next[i-1]->count];p->next[i-1]->count -= 1;break;}else if(i != p->count && (p->next[i]->count + p->next[i+1]->count) >= 2*(pd-1)){s->count += 1;s->data[s->count] = p->data[i+1];s->next[s->count] = p->next[i+1]->next[1];p->data[i+1] = p->next[i+1]->data[1];for(j = 2; j <= p->next[i+1]->count; j++){p->next[i+1]->next[j-1] = p->next[i+1]->next[j];p->next[i+1]->data[j-1] = p->next[i+1]->data[j];}p->next[i+1]->count -= 1;break;}/*被删除点不满足借代的条件,那么就只能让被删除点跟其相邻的一个点进行合并。(这里我们确定合并之后的关键字肯定会 < len)*/if(i == 0) {r = p->next[i];s = p->next[++i];}else r = p->next[i-1];int &next_x = r->count;//通过引用 , 下面调用就能自动改变其countr->data[++next_x] = p->data[i];r->next[next_x] = s->next[0];//两个节点合并,先把0位置的节点安置好for(j = 1; j <= s->count;j++){r->data[++next_x] = s->data[j];r->next[next_x] = s->next[j];}delete(s); //删除if(rb.empty() && p->count == 1) //如果根节点root只有一个关键字,那么这时根节点就是r了 {delete(root);root = r;break;}for(j = i+1; j <= p->count; j++)  //调整根节点{p->data[j-1] = p->data[j];p->next[j-1] = p->next[j];}p->count -= 1;s = p;}}int tree_B::find(int x){node *s = root;int i;while(s){for(i = s->count; i >= 1; i--){if(s->data[i] < x)  break;else if(s->data[i] == x)  return 1;}if(s->isLeaf)  break;s = s->next[i];}return 0;}void splitChild(node *r , node *p , int bw , int pd)//进行拆分节点{node *s = new(node);int i ;for(i = pd+1; i <= p->count; i++)//把被拆分的节点p,拆分成p 和 s{s->data[i-pd] = p->data[i];if(!p->isLeaf)s->next[i-pd] = p->next[i];}if(!p->isLeaf)s->next[0] = p->next[pd];  //这里要注意 0 位置的节点s->count = p->count-pd;  //大于data[pd]的p->count = pd-1;r->next[bw] = p;r->next[bw+1] = s;}void tree_B::disp(){queue<node *>s;queue<node *>t;int i;node *p = root;s.push(p);while(1){while(!s.empty()){p = s.front();s.pop();if(!p->isLeaf)t.push(p->next[0]);for(i = 1; i <= p->count; i++){cout<<p->data[i]<<",";if(!p->isLeaf)t.push(p->next[i]);}cout<<" , ";}cout<<endl;if(p->isLeaf)  break;while(!t.empty()){p = t.front();t.pop();if(!p->isLeaf)s.push(p->next[0]);for(i = 1; i <= p->count; i++){cout<<p->data[i]<<",";if(!p->isLeaf)s.push(p->next[i]);}cout<<" , ";}cout<<endl;if(p->isLeaf)  break;}}int main(){int a[] = {20 , 54 , 69,84,71,30,78,25,93,41,7,76,51,66,68,53,3,79,35,12,15,65};int i;tree_B test(5);for(i = 0; i < 22; i++)test.insert(a[i]);test.disp();test.dele(12);test.disp();test.dele(15);test.disp();return 0;}






0 0