在B-树中查找给定关键字的方法是,首先把根结点取来,在根结点所包含的关键字K1,…,Kn查找给定的关键字(可用顺序查找或二分查找法),若找到等于给定值的关键字,则查找成功;否则,一定可以确定要查找的关键字在Ki与Ki+1之间,Pi为指向子树根节点的指针,此时取指针Pi所指的结点继续查找,直至找到,或指针Pi为空时查找失败。
1970年,R.Bayer和E.mccreight提出了一种适用于外查找的树,它是一种平衡的多叉树,称为B树(或B-树、B_树)。
一棵m阶B树(balanced tree of order m)是一棵平衡的m路搜索树。它或者是空树,或者是满足下列性质的树:
1、根结点至少有两个子女;
2、每个非根节点所包含的关键字个数 j 满足:┌m/2┐ - 1 <= j <= m - 1;
3、除根结点以外的所有结点(不包括叶子结点)的度数正好是关键字总数加1,故内部子树个数 k 满足:┌m/2┐ <= k <= m ;
4、所有的叶子结点都位于同一层。
在B-树中,每个结点中关键字从小到大排列,并且当该结点的孩子是非叶子结点时,该k-1个关键字正好是k个孩子包含的关键字的值域的分划。
因为叶子结点不包含关键字,所以可以把叶子结点看成在树里实际上并不存在外部结点,指向这些外部结点的指针为空,叶子结点的数目正好等于树中所包含的关键字总个数加1。
B-树中的一个包含n个关键字,n+1个指针的结点的一般形式为: (n,P0,K1,P1,K2,P2,…,Kn,Pn)
其中,Ki为关键字,K1
struct BTreeNode
{
BTreeNode()
:_pParent(NULL)
,_size(0)
{
for(int idx=0; idx<M; ++idx)
{
_pSub[idx] = NULL;
}
}
size_t _size; //节点中关键字个数,有效元素集合即节点的大小
BTreeNode* _pParent; //双亲节点
T _key[M]; //关键字集合,最后单元未用,简化分裂逻辑
BTreeNode* _pSub[M+1]; //孩子指针集合
};
假如每个盘块可以正好存放一个B树的结点(正好存放2个文件名)。那么一个BTreeNode结点就代表一个盘块,而子树指针就是存放另外一个盘块的地址。
下面,咱们来模拟下查找文件29的过程:
(1)根据根结点指针找到文件目录的根磁盘块1,将其中的信息导入内存。【磁盘IO操作 1次】
(2)此时内存中有两个文件名17、35和三个存储其他磁盘页面地址的数据。根据算法我们发现:17<29<35,因此我们找到指针p2。
根据p2指针,我们定位到磁盘块3,并将其中的信息导入内存。【磁盘IO操作 2次】
(2)此时内存中有两个文件名26,30和三个存储其他磁盘页面地址的数据。根据算法我们发现:26<29<30,因此我们找到指针p2。
根据p2指针,我们定位到磁盘块8,并将其中的信息导入内存。【磁盘IO操作 3次】
(4)此时内存中有两个文件名28,29。根据算法我们查找到文件名29,并定位了该文件内存的磁盘地址。
分析上面的过程,发现需要3次磁盘IO操作和3次内存查找操作。关于内存中的文件名查找,由于是一个有序表结构,可以利用折半查找提高效率。至于IO操作是影响整个B树查找效率的决定因素。
当然,如果我们使用平衡二叉树的磁盘存储结构来进行查找,磁盘4次,最多5次,而且文件越多,B树比平衡二叉树所用的磁盘IO操作次数将越少,效率也越高。
**
插入(insert)操作
**
插入一个元素时,首先在B树中是否存在,如果不存在,即在叶子结点处结束,然后在叶子结点中插入该新的元素,注意:如果叶子结点空间足够,这里需要向右移动该叶子结点中大于新插入关键字的元素,如果空间满了以致没有足够的空间去添加新的元素,则将该结点进行“分裂”,将一半数量的关键字元素分裂到新的其相邻右结点中,中间关键字元素上移到父结点中(当然,如果父结点空间满了,也同样需要“分裂”操作),而且当结点中关键元素向右移动了,相关的指针也需要向右移。如果在根结点插入新元素,空间满了,则进行分裂操作,这样原来的根结点中的中间关键字元素向上移动到新的根结点中,因此导致树的高度增加一层。如下图所示:
下面咱们通过一个实例来逐步讲解下。
1. 插入以下字符字母到一棵空的B 树中(非根结点关键字数小了(小于2个)就合并,大了(超过4个)就分裂):C N G A H E K Q M F W L T Z D P R X Y S,首先,结点空间足够,4个字母插入相同的结点中,如下图:
2、当咱们试着插入H时,结点发现空间不够,以致将其分裂成2个结点,移动中间元素G上移到新的根结点中,在实现过程中,咱们把A和C留在当前结点中,而H和N放置新的其右邻居结点中。如下图:
3、当咱们插入E,K,Q时,不需要任何分裂操作
4、插入M需要一次分裂,注意M恰好是中间关键字元素,以致向上移到父节点中
5、插入F,W,L,T不需要任何分裂操作
6、插入Z时,最右的叶子结点空间满了,需要进行分裂操作,中间元素T上移到父节点中,注意通过上移中间元素,树最终还是保持平衡,分裂结果的结点存在2个关键字元素。
7、插入D时,导致最左边的叶子结点被分裂,D恰好也是中间元素,上移到父节点中,然后字母P,R,X,Y陆续插入不需要任何分裂操作(别忘了,树中至多5个孩子)。
8、最后,当插入S时,含有N,P,Q,R的结点需要分裂,把中间元素Q上移到父节点中,但是情况来了,父节点中空间已经满了,所以也要进行分裂,将父节点中的中间元素M上移到新形成的根结点中,注意以前在父节点中的第三个指针在修改后包括D和G节点中。这样具体插入操作的完成,下面介绍删除操作,删除操作相对于插入操作要考虑的情况多点。
代码如下:
#include <iostream>
using namespace std;
#include <assert.h>
template<class T, size_t M>
struct BTreeNode
{
BTreeNode()
:_pParent(NULL)
,_size(0)
{
for(int idx=0; idx<M; ++idx)
{
_pSub[idx] = NULL;
}
}
size_t _size; //节点中关键字个数,有效元素集合即节点的大小
BTreeNode* _pParent; //双亲节点
T _key[M]; //关键字集合,最后单元未用,简化分裂逻辑
BTreeNode* _pSub[M+1]; //孩子指针集合
};
template<class T, size_t M>
class BTree
{
typedef BTreeNode<T, M> Node;
public:
BTree()
:_pRoot(NULL)
{}
BTree(const BTree<T, M>& b)
:_pRoot(b._pRoot)
{}
pair<Node*, int> Find(const T key)
{
return _Find(key);
}
bool Insert(T key)
{
if(_pRoot == NULL)
{
_pRoot = new Node;
_pRoot->_key[0] = key; //根节点
_pRoot->_size = 1;
return true;
}
Node* pSub = NULL;
//找插入位置
pair<Node*, int> ret = Find(key);
if(ret.second > -1) //找到这个元素,不用插入
{
return false;
}
Node* pCur = ret.first;
while(true)
{
//插入
_Insert(pCur, key, pSub);
if(pCur->_size < M)//y叶节点空间足够,即该节点的关键字数小于M,直接插入叶节点的左边或右边
return true;
//如果空间满了以致没有足够的空间添加新元素,即该节点的关键字数为M个,需要对此节点进行分裂,
//将一半的关键字分裂到新的其相邻右节点中,中间关键字元素上移到双亲节点中,当节点中关键字右移时,
//相关的指针也要随之移动
size_t mid = M/2;
Node* pNewNode = new Node;
size_t count = 0;
size_t idx=mid+1;
//将一半的关键字分裂到新的其相邻右节点中,相关的指针也要随之移动
for(; idx < pCur->_size; ++idx)
{
pNewNode->_key[count] = pCur->_key[idx];
pNewNode->_pSub[count++] = pCur->_pSub[idx];
//搬移元素及指针后,相应的双亲也要随之更新
if(pCur->_pSub[idx])
{
pCur->_pSub[idx]->_pParent = pNewNode;
pCur->_pSub[idx] = NULL; //去掉原来的指针指向
}
pNewNode->_size++; //新节点中关键字个数增加一个
}
////多搬移一个孩子,多搬移的是向上更新的根节点的孩子
pNewNode->_key[count] = pCur->_key[idx];
pNewNode->_pSub[count++] = pCur->_pSub[idx];
if(pCur->_pSub[idx])//搬移元素及指针后,相应的双亲也要随之更新
{
pCur->_pSub[idx]->_pParent = pNewNode;
pCur->_pSub[idx] = NULL; //去掉原来的指针指向
}
//计算原节点中剩余的个数
pCur->_size = pCur->_size - pNewNode->_size - 1;
if(pCur->_pParent == NULL)
{
Node* pRoot = new Node;
pRoot->_key[0] = pCur->_key[mid];
pRoot->_pSub[0] = pCur;
pRoot->_pSub[1] = pNewNode;
//更新双亲
pCur->_pParent = pRoot;
pNewNode->_pParent = pRoot;
pRoot->_size = 1;
_pRoot = pRoot;
return true;
}
//如果由于“中间关键字元素上移到双亲节点”的过程中导致根节点空间满了,根节点也要进行分裂,这样,
//原来的根节点的中间关键字元素上移到新的根节点中,导致树的高度会增加1
else
{
key = pCur->_key[mid];
pCur = pCur->_pParent;
pSub = pNewNode;
}
}
}
void InOrder()
{
cout<<"中序遍历:"<<endl;
_Inorder(_pRoot);
}
private:
pair<Node*, int> _Find(const T key)
{
Node* pCur = _pRoot;
Node* parent = NULL;
size_t idx = 0;
while(pCur)
{
while(idx < pCur->_size)
{
if(key < pCur->_key[idx])
break;
else if(key > pCur->_key[idx])
idx++;
else
return make_pair(pCur, idx);
}
parent = pCur;
pCur = pCur->_pSub[idx]; //idx == pCur->_size
}
return make_pair(parent, -1);
}
void _Insert(Node* pCur, const T key, Node* pSub)
{
assert(pCur);
int end = pCur->_size-1;
while(end >= 0)
{
if(pCur->_key[end] > key)//如果当前插入的元素大于最后的某些元素,需要将
//这些元素全部向后搬移一个位置,然后再插入该元素
{
pCur->_key[end+1] = pCur->_key[end];
pCur->_pSub[end+2] = pCur->_pSub[end+1];//指针也要搬移
}
else
{
break;
}
end--;
}
pCur->_key[end+1] = key; //找到插入元素所要处的正确位置,插入该元素
pCur->_pSub[end+2] = pSub; //指针也要正确改变插入
if(pSub)
pSub->_pParent = pCur; //更新pSub的双亲
pCur->_size++;
}
void _Inorder(Node *pRoot)
{
if (pRoot == NULL)
{
return;
}
int i = 0;
for ( ;i < pRoot->_size; i++) //遍历所有的key才是这个节点遍历完
{
_Inorder(pRoot->_pSub[i]);
cout << pRoot->_key[i] << " ";
}
_Inorder(pRoot->_pSub[i]);
}
private:
BTreeNode<T, M>* _pRoot;
};
void TestBTree()
{
BTree<int, 3> b;
b.Insert(53);
b.Insert(75);
b.Insert(139);
b.Insert(49);
b.Insert(145);
b.Insert(36);
b.Insert(101);
}
int main()
{
TestBTree();
system("pause");
return 0;
}