[QQ群:
189191838,对算法和C++感兴趣可以进来]
五一前就筹划着写下这篇文章,但是迫于自己从来没有实现过B-树(如果大家感兴趣,我可以考虑写一篇B+树的文章),手中没有源代码,另外自己以前对B-树也是一知半解状态中,担心误人子弟,在4月30日终于把代码写完,今天调完之前的bug之后,那种感觉就像在鸟无人烟的大荒漠中走了好久,看到一间有水的屋子,长舒一口气!好的废话不多说,下面直接切入正题!
链表,树,图是最基本的数据结构了,链表有单链表、双链表,有环和无环等等;树有二叉树、多叉树,平衡树、不平衡树等等;图有有向、无向图等等。如果把算法建模看成是建筑师建造一座大厦,那么数据结构就是地基,地基的好坏直接关系到房子能建多高。
今天我们要讲到的是树的一种,即B-树。要说B-树,就必然要说平衡二叉树,否则这是不科学的,没有平衡二叉树哪来B-树。它的定义是这样的:
平衡二叉树(Balanced
Binary Tree)又被称为AVL树(有别于AVL算法),且具有以下性质:它是一
棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
图一
这个是一颗典型的平衡二叉树,对根节点50而言,左右子树的高度都是3,高度差绝对值不超过1;对左子树而言,根节点是17,但是他的左右两子树高度都是2,差的绝对值也不超过1.我们递归的观察,显然它符合平衡二叉树的基本条件。
如果我们把右子树剪掉,他就不是平衡二叉树了。左边高度为3,右边高度为0;3-0=3>1.
图二
OK,说到这里,我想我们可以讲讲B-树了。B-树其实可以理解为多叉平衡树!它的定义是这样的:(其中T一般大于3)
1)不仅高度平衡,而且所有的叶子节点都在同一层。
2)除根节点外其他每个节点最少保存(T-1)个关键字,至多保存(2T-1)个关键字;至少保存(T)个孩子结点,至多(2T)个孩子结点。
3)对于关键字K而言,左孩子结点的所有关键字都小于K,右孩子结点的所有关键字都大于K。在同一个结点上,关键字是从小到大依次排列。结点中除叶子节点关键字外,均有左右孩子。
随便提一下,现在有很多B-树的变形,有些要求每个节点至少保存2/3(2T-1)个关键字等等,也是可以的。
如下图就是一颗典型的B-树:
图三
OK,对于B-树,想必大家有了一个直观的认识。那么B-树有什么用呢?当今社会太浮躁了,没用的东西留着干嘛?果断弃之。显然B-树是非常有用的。大家用数据库进行开发吗?如果你用,那么其实你每天都在用B-树有没有,因为大部分数据库的底层实现都是用B-树。
B-树对大数据特别有用,特别是查询情况多的时候(比如数据库);因为一旦数据量特别大之后,内存肯定不能把所有数据都装到内存中,我们唯有通过硬盘存储,存储在硬盘中之后,怎样才能高效的进行查询呢?我们仔细观察B-绝对是一个很好的选择,因为它最糟糕的时候效率都是logT(N);硬盘的速度相对内存来说是比较慢的,较少一次硬盘读取对用户体验是一个莫大的支持!
老规矩:如果需要全部源代码请点赞后留下email地址。
其实基于B-的数据库,数据操作基本可以归纳为四个部分:
读取硬盘数据:根据给定页面Id读取硬盘数据到内存,然后返回内存相应位置;
硬盘写操作:给定内存需写入数据地址信息,写入到给定页面id硬盘中。
申请硬盘页面:如果页面中id不存在,开拓硬盘页面,然后返回内存地址。
释放页面:给定一个硬盘页面id,释放该资源。
OK,我们了解到了B-树的重要性之后,也就知道了学习B-树的现实意义了。那我们现在可以继续开扒了。
B-树应该怎么设计呢?看了B-树的定义之后,B-树数据结构应该包括:1)关键字个数,2)指向父节点的指针,3)关键字集合,4)孩子结点集合。
根据以上论述,我们把B-树的数据结构代码化之后如下:
1 struct BTreeNode{
2 int keyNum;//关键字个数。
3 BTreeNode* parentNode;//父结点指针
4 vector<NodeValueType> nodeValues;
5 vector<BTreeNode*> childs;
6 BTreeNode(){
7 keyNum=0;
8 parentNode=NULL;
9 }
10 };
对于B-树而言最常用的就是增删改查了。改可以认为是删,然后增。所以可以理解成主要操作有增、删、查!下面分三部分详解他们。
B-树的查询操作:
B-树的查询可以从根节点开始查找,根节点没有查到,再查找某一个孩子结点。
因为B-树结点都有很多孩子节点,查找哪个孩子结点就显得特别重要了,总不能乱查吧。它应该是查找第一个比关键字大的左结点,如果没有一个比该关键字大的关键字,则查找最右孩子。如此递归。直到查到叶子结点,如果叶子结点也没有找到,那么B-树中不存在该关键字。
比如对于图三,查找关键字G。首先在根节点中查找,根节点只有M一个关键字,显然不符合条件。查找到第一个大于G的关键字,这里M比G大,所有积蓄查找M的左子树,左子树的根节点关键字有:D、H。同样第一个比G大的是H。H的左孩子结点关键字是F、G。这样,G被找到了。返回。
图四
算法具体描述为:
1 bool SearchBTreeByValue(BTreeNode* q,NodeValueType k){
2 int i=0;
3 while(i<q->keyNum&&q->nodeValues[i]<=k){
4 if (q->nodeValues[i]==k){
5 return true;//相等则返回q
6 }
7 i++;
8 }
9 if (!q->childs.size()){
10 return false;//没有找到直接返回NULL
11 }
12 return SearchBTreeByValue(q->childs[i],k);
13 }
B-树的增加(插入)操作
【插入操作要寻找到叶子节点插入】因为B-树中最多只能保存2T-1个关键字,如果当前的关键字个数已经达到2T-1,还需插入一个的话就需要分裂成两个结点。如下图,T=2.如果还需插入一个18.由于结点X已经有3个关键字了。已经full了。如果还需要插入18,就不符合条件了,就需要分裂成两个结点,同时增加一层。再进行插入。也可以插入完成后,再分裂。最终还是要分裂。
如果B-树没有满的话,直接插入就好。
图五
图六
1 BTreeNode* InsertNodeToBTreeByValue(BTreeNode* p,NodeValueType theValue){
2 if (SearchBTreeByValue(p,theValue)){
3 cout<<theValue<<"已经存在了,骚年!"<<endl;
4 return p;
5 }
6 while(p->childs.size()!=0){//一直找到叶子结点
7 int i=0;
8 while(i<p->keyNum&&p->nodeValues[i]<theValue){
9 i++;
10 }
11 p=p->childs[i];
12 }
13 p->nodeValues=InsertKeyToNodeByValue(p->nodeValues,theValue);//插入到当前叶子结点
14 p->keyNum++;
15 return adjustBTree(p);//使得最大不超过maxKey
16 }
B-树的删除操作
说实话,删除操作比增加操作复杂多了,这也是我的程序一直出现bug的地方。假设要在结点x中删除关键字k。
因为删除操作需要考虑删除后结点会少于T-1和树不平衡的情况。
删除操作主要考虑三种情况:
1)x是叶子结点,包含k
2)x是内部结点,包含k
3)x是内部结点,不包含k.
对于1),直接删除,同时更新keynum;
对于2),考虑k的左右孩子,
若有一个孩子的关键字个数大于T-1,若左孩子y,用最右边的关键字和k交换,同时递归删除delete(y,k);
若右孩子(z)数大于T-1,则递归删除delete(z,k);
若左右孩子都等于T-1,则需要合并左孩子,k,右孩子,同时删除右孩子。
对于3),y=x.child[k];若y的关键字个数大于T-1,则递归删除delete(y,k)
若y的关键字个数等于T-1,则寻找他的兄弟节点,兄弟节点若有大于T-1个数的,补一个过来。
若没有的话,就进行和其中兄弟节点合并,再递归delete.
1 BTreeNode* DeleteBTreeNode(BTreeNode* p,NodeValueType theValue){
2 int theValueOrderInNode=GetOrderInNodeByValue(p,theValue);//返回当前序号,theValueOrderInNode
3 //BTreeNode* w=p->parentNode;
4 if (theValueOrderInNode!=-1&&p->childs.size()==0){
5 p->nodeValues.erase(p->nodeValues.begin()+theValueOrderInNode,p->nodeValues.begin()+theValueOrderInNode+1);
6 p->keyNum--;
7 return GetBTreeRoot(p);
8 }else if (theValueOrderInNode==-1){//该结点中不存在theValue
9 int i=0;
10 while(i<p->keyNum&&p->nodeValues[i]<theValue){
11 i++;//得到第I个孩子。
12 }
13 BTreeNode* y=p->childs[i];//找到i+1个孩子结点
14 if (y->keyNum>atLeastKeyNum){
15 return DeleteBTreeNode(y,theValue);
16 }else if(y->keyNum==atLeastKeyNum){
17 BTreeNode* leftSlibing=new BTreeNode(),*rightSlibing=new BTreeNode();//左右结点出现啦!
18 leftSlibing=NULL,rightSlibing=NULL;
19 if (i!=0){//有左兄弟
20 leftSlibing=p->childs[i-1];
21 }
22 if (i!=p->keyNum){//有右兄弟
23 rightSlibing=p->childs[i+1];
24 }
25 if (leftSlibing!=NULL&&leftSlibing->keyNum>atLeastKeyNum){//左兄弟移动一个到y中
26 y->nodeValues.insert(y->nodeValues.begin(),p->nodeValues[i-1]);
27 y->keyNum++;
28 p->nodeValues[i-1]=leftSlibing->nodeValues[leftSlibing->keyNum-1];
29 if (y->childs.size()!=0){
30 y->childs.insert(y->childs.begin(),*(leftSlibing->childs.end()-1));
31 leftSlibing->childs.erase(leftSlibing->childs.end()-1,leftSlibing->childs.end());
32 }
33 leftSlibing->nodeValues.erase(leftSlibing->nodeValues.end()-1,leftSlibing->nodeValues.end());
34 leftSlibing->keyNum--;
35 return DeleteBTreeNode(y,theValue);
36 }else if(rightSlibing!=NULL&&rightSlibing->keyNum>atLeastKeyNum){//右兄弟移动一个到y中
37 y->nodeValues.insert(y->nodeValues.end(),p->nodeValues[i]);
38 y->keyNum++;
39 if (y->childs.size()!=0){
40 y->childs.insert(y->childs.end(),*(rightSlibing->childs.begin()));
41 rightSlibing->childs.erase(rightSlibing->childs.begin(),rightSlibing->childs.begin()+1);
42 }
43 p->nodeValues[i]=rightSlibing->nodeValues[0];
44 rightSlibing->nodeValues.erase(rightSlibing->nodeValues.begin(),rightSlibing->nodeValues.begin()+1);
45 rightSlibing->keyNum--;
46 return DeleteBTreeNode(y,theValue);
47 }else{//合并成一个
48 if (leftSlibing!=NULL){//同左合并
49 leftSlibing->nodeValues.insert(leftSlibing->nodeValues.end(),p->nodeValues[i-1]);
50 leftSlibing->keyNum++;
51 p->nodeValues.erase(p->nodeValues.begin()+i-1,p->nodeValues.begin()+i);
52 MoveBTreeNode(leftSlibing,y);
53 p->childs.erase(p->childs.begin()+i-1,p->childs.begin()+i);
54 p->keyNum--;
55 if (p->keyNum==0){
56 leftSlibing->parentNode=NULL;
57 }
58 return DeleteBTreeNode(leftSlibing,theValue);
59 }else{//同右合并
60 y->nodeValues.insert(y->nodeValues.end(),p->nodeValues[i]);
61 y->keyNum++;
62 p->nodeValues.erase(p->nodeValues.begin()+i,p->nodeValues.begin()+i+1);
63 MoveBTreeNode(y,rightSlibing);
64 p->childs.erase(p->childs.begin()+i+1,p->childs.begin()+i+2);
65 p->keyNum--;
66 if (p->keyNum==0){
67 y->parentNode=NULL;
68 }
69 return DeleteBTreeNode(y,theValue);
70 }
71 }
72 }
73 }else if (theValueOrderInNode!=-1){//在该结点中找到了。
74 BTreeNode* leftChild=GetPreChildByNodeValue(p,theValue);
75 BTreeNode* rightChild=GetSuccessByNodeValue(p,theValue);
76 if (leftChild!=NULL&&leftChild->keyNum>atLeastKeyNum){//左
77 p->nodeValues[theValueOrderInNode]=leftChild->nodeValues[leftChild->keyNum-1];
78 leftChild->nodeValues[leftChild->keyNum-1]=theValue;
79 //leftChild->parentNode=p;
80 return DeleteBTreeNode(leftChild,theValue);
81 }
82 else if (rightChild!=NULL&&rightChild->keyNum>atLeastKeyNum){//右
83 p->nodeValues[theValueOrderInNode]=rightChild->nodeValues[0];
84 rightChild->nodeValues[0]=theValue;
85 //rightChild->parentNode=p;
86 return DeleteBTreeNode(rightChild,theValue);
87 }
88 else if (leftChild!=NULL){//需要合并了
89 leftChild->nodeValues.insert(leftChild->nodeValues.end(),p->nodeValues[theValueOrderInNode]);
90 leftChild->keyNum++;
91 MoveBTreeNode(leftChild,rightChild);//从左移到右
92 p->nodeValues.erase(p->nodeValues.begin()+theValueOrderInNode,p->nodeValues.begin()+theValueOrderInNode+1);
93 p->childs.erase(p->childs.begin()+theValueOrderInNode+1,p->childs.begin()+theValueOrderInNode+2);
94 p->keyNum--;
95 if (p->keyNum==0){
96 leftChild->parentNode=NULL;
97 }
98 return DeleteBTreeNode(leftChild,theValue);
99 }
100 }
101 return NULL;
102 }
操作数据:用106,103,109,130,145,165,42,60,136,107,108对B-树进行初始化。然后再进行查找删除操作。
图七
参考文献:http://zgking.com:8080/home/donghui/publications/books/dshandbook_BTree.pdf
http://webdocs.cs.ualberta.ca/~holte/T26/del-b-tree.html
http://www.cs.nott.ac.uk/~nza/G52ADS/btrees2.pdf
版权所有,欢迎转载,但是转载请注明出处:潇一
数据结构之B-树,你每天都在用的,源码发布!,布布扣,bubuko.com