B树的理解和实现

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;
}



    原文作者:B树
    原文地址: https://blog.csdn.net/zengchen__acmer/article/details/23822977
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞