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不是i
root->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];//开始插入x
else 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];//注意,这里是i
if(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+1
r->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;//通过引用 , 下面调用就能自动改变其count
r->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不是i
root->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];//开始插入x
else 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];//注意,这里是i
if(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+1
r->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;//通过引用 , 下面调用就能自动改变其count
r->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;
}