B树
B树与红黑树最大的不同在于在降低磁盘的操作次数方面要好一些。B树的分支因子是由使用的磁盘的特性决定的。
我们的CPU所提供的存储能力是有限的,于是我们有了辅存的概念,利用磁盘进行存储,信息量会增大很多。
现在来了解辅存,磁盘的物理构造决定着磁盘的I/O操作,物理记录的位置必须由柱面号,盘面号,扇区号3个参数共同确定。由查找时间,旋转延迟时间、传输时间3部分之和构成了一次磁盘的访问时间。所以应该尽量的减少磁盘的访问次数。每个盘片通过磁臂末端的磁头来读写。这些磁臂一起同时移动磁头。这些磁臂绕着一个共同的传动轴转动,当读/写磁头静止时。由它下方经过的次盘表面成为一个磁道。柱面是由一组垂直的磁道构成。磁盘会一次存取多个数据项。信息被分割成一些在柱面内连续出现的相等大小的位的页面,每次磁盘读写都是一个或者多个完整的页面。磁盘的存取时间是依赖于当前磁道与所需磁道之间的距离以及磁盘的初始旋转状态。所以对应的磁盘驱动调度算法就是为了减少访问时间,增加单位时间的吞吐量进行的管理策略。
现在我们终于回到我们的B树中,B树中的数据量一次无法装入主存,B树中将所需的页选择出来复制到主存中,而后将修改过的页面写回到磁盘中。一个B树所拥有的子女的数量就由磁盘页的大小决定。
B树中一些很重要的性质:
1)每个节点具有相同的深度。
2)每个节点都有一个上界或者下届。最小度数为t。
3)每个非根节点必须至少有t-1个关键字。每个非根的内节点至少有t个子女。
4)每个节点最多有2t-1个关键字,一个内节点至多有2t个子女。h<=logt(n+1)/2.
5)对大多数树来说,要查找的节点数在B树中要比在红黑树中少大约lgt的因子。
B树的基本操作。
1)搜索
2)插入,在插入的时候我们会有键值数目已满的情况,也就是等于2t-1时,此时是唯一一次进行分裂
#define NODE_NUM 3
template<class T>
class BNode
{
public:
T key[NODE_NUM*2-1];//在算法中要增加一个元素是添加一个数组,但是在进行算法演算时候除非是使用的是链表
//否则如何实现元素的复制,但是如果不是利用从上到下的这种方式,就如同红黑树一样的进行从下到上的回溯怎么样子呢?
int keyNum;
bool leaf;
BNode<T> *child[NODE_NUM*2];
BNode()
{
for (int i=0;i<NODE_NUM*2-1;i++)
{
key[i]=0;
}
keyNum=0;
leaf=true;
}
};
//进行分割
template<class T>
void BTree<T>::B_TREE_SPLIT_CHILD(BNode<T>*parentNode,int insertPot,BNode<T>*splitNode)
{
BNode<T>*newNode=new BNode<T>();
newNode->leaf=splitNode->leaf;//是否为叶子节点
newNode->keyNum=NODE_NUM-1;//孩子节点的个数
//newNode->key=new T[newNode->keyNum];//关键字
for (int index=0;index<NODE_NUM-1;index++)
{
newNode->key[index]=splitNode->key[index+NODE_NUM];//进行键值的复制
splitNode->key[index+NODE_NUM]=0;
}
if (!newNode->leaf)
{
for (int i=0;i<NODE_NUM;i++)
{
newNode->child[i]=splitNode->child[i+NODE_NUM];
splitNode->child[i+NODE_NUM]=NULL;
}
}//这是树中唯一一种树的宽度增加的情形
for (int j=parentNode->keyNum;j>insertPot;j--)
{
parentNode->child[j+1]=parentNode->child[j];//将x的孩子节点的指针进行复制
}
(parentNode->child[insertPot+1])=newNode;//对newNode这个位置的节点进行赋值
for (int i=parentNode->keyNum;i>insertPot;i--)
{
parentNode->key[i]=parentNode->key[i-1];//对X的后面的键值进行复制
}
parentNode->key[insertPot]=splitNode->key[NODE_NUM-1];//对i这个位置的键值进行赋值
splitNode->key[NODE_NUM-1]=0;
splitNode->keyNum=NODE_NUM-1;
parentNode->keyNum++;//对x的键值个数加一
}
在插入算法中我们要使得不进行递归,那么就要进行在插入时候每个节点进行判断,发现要满时就进行分裂
//插入键值
template<class T>
bool BTree<T>::B_TREE_INSERT(BTree<T>*bTree,T key)
{
BNode<T> *r=bTree->root;
if((r->keyNum)==(2*NODE_NUM-1))//当根节点的键值数目已经满了,根节点进行分割
{
BNode<T>* newRoot=new BNode<T>();//重新创建一个节点
bTree->root=newRoot;//将这个新创建的节点作为新的根节点
newRoot->leaf=false;//为非叶子节点
newRoot->keyNum=0;//键值原来的数目,这个与在其他节点是不同的,这上面是唯一的一种可以增加数层次的情况
//下面调用的split这个函数,那么现在要做的是找到这个插入值的位置0,被分割的节点为原来的根节点
newRoot->child[0]=r;
B_TREE_SPLIT_CHILD(newRoot,0,r);
B_TREE_INSERT_NUNFULL(bTree,newRoot,key);
}
else B_TREE_INSERT_NUNFULL(bTree,r,key);//否则将键值从根节点向下找进行查询并插入节点
return true;
}
//插入到非空的节点中去
template<class T>
bool BTree<T>::B_TREE_INSERT_NUNFULL(BTree<T>*bTree,BNode<T>*&insertNode,T key)
{
int index=insertNode->keyNum;//index指定节点最后的元素
if (insertNode->leaf)//若插入的是一个叶子节点,插入的节点只能是叶子节点
{
while(key<insertNode->key[index-1]&&index>=0)//又遇到不能插入的情况,,,,指针问题
{
insertNode->key[index]=insertNode->key[index-1];//将所有的值都向后移一位
index--;//index减1
}
insertNode->keyNum++;
insertNode->key[index]=key;
}
else
{
while(index>=0&&key<insertNode->key[index-1])//比较元素的键值
{
index--;
}
if((insertNode->child[index])->keyNum==(NODE_NUM*2-1))
{
B_TREE_SPLIT_CHILD(insertNode,index,(insertNode->child[index]));//分裂
if(key>insertNode->key[index])
{
index++;
}
}
B_TREE_INSERT_NUNFULL(bTree,(insertNode->child[index]),key);//递归,继续查看下面结点
}
return true;
}
1)删除
//合并节点,将结点与其兄弟结点进行合并,还要分是左边的还是右边的兄弟
//其中涉及到根节点下面的两个节点合并,作为唯一一种可能使树的高度降低的情况具体的在删除程序中
//采用先合并后删除
template<class T>
void BTree<T>::B_TREE_MERGE_CHILD(BNode<T>*parentNode,int deletePos,BNode<T>*mergeNode,BNode<T>*brotherNode)
{
mergeNode->key[mergeNode->keyNum]=parentNode->key[deletePos];//将父节点的键值赋予这个结点的后一个键值
mergeNode->keyNum+=1;
for(int index=0;index<brotherNode->keyNum;index++)
{
mergeNode->key[mergeNode->keyNum+index]=brotherNode->key[index];//将兄弟结点的值赋予
}
mergeNode->keyNum+=brotherNode->keyNum;
if (!mergeNode->leaf)//若是内结点
{
for(int index=0;index<brotherNode->keyNum;index++)
{
mergeNode->child[mergeNode->keyNum+index+1]=brotherNode->child[index];//将兄弟的节点赋值。
}
delete brotherNode;//删除兄弟结点
}
int index=deletePos;
for (;index<parentNode->keyNum-1;index++)
{
parentNode->key[index]=parentNode->key[index+1];//将键值往前面复制
parentNode->child[index+1]=parentNode->child[index+2];//孩子结点复制
}
parentNode->key[index]=0;
parentNode->keyNum--;
parentNode->child[index+1]=NULL;
parentNode->child[deletePos]=mergeNode;//合并之后的结点串到原来的结点中去
}
//删除节点
template<class T>
bool BTree<T>::B_TREE_DELETE(BTree<T>*bTree,T key)//这里不能传递整棵树来,使用的是节点,因为在删除进行递归的时候,一个键值可能会出现几次,不是唯一
{
/*
删除的几种情况:1、从上到下进行删除,若为叶子结点
1)若要删除的结点中有大于等于t的元素,直接进行删除
2)若删除结点中小于t的元素,查找其兄弟节点,将元素重新的进行调整,
2、若是内结点
1)在删除时若左右的结点中至少有一个的元素的键值个数大于等于t则进行代替
2)若都是小玉t的则先进行合并,然后删除这个键值
在通常的情况下,我们在删除结点后,很可能会回溯,我们进行在删除的时候考虑到关键字的个数,如此便无需进行回溯处理
*/
//首先是从根节点开始,从头开始进行判断所拥有的结点,保证不会进行回溯
bool bfind=false;
BNode<T>*temp=bTree->root;
BNode<T>*parentNode=bTree->root;
int parentPos=0;//在父结点中的位置
while(temp)
{
BNode<T>*brotherNode;
int index=0;
while(index<temp->keyNum&&key>temp->key[index]) //为了找到这个节点
{
index++;
}
if(temp->key[index]==key)//当已经找到这个节点
{
bfind=true;
if (temp->leaf)//若是叶子结点
{
int i=index;
while(i<temp->keyNum-1)//无论是哪种情况首先要进行的删除操作
{
temp->key[i]=temp->key[i+1];
i++;
}
temp->key[i]=0;
if(temp->keyNum>=NODE_NUM)//情况1:若删除的是叶子结点且节点中键值数目是大于t的,这是最好的情形
{
temp->keyNum--;
break;
}
else if (temp->keyNum<NODE_NUM)//情况2:删除的是叶子结点且叶子结点中的键值小于t,但是兄弟节点中键值数目有大于等于t的结点的
{
if (temp==bTree->root)//这里会出现根节点就是当前删除的节点,而且这个节点是叶子节点。若是内节点的话会将子结点中的值
{ //进行交换
temp->keyNum--;
break;
}
bool left=true;
int i=index;
//对兄弟结点中键值数目进行判断
if (parentPos==0)
{
brotherNode=parentNode->child[parentPos+1];
left=false;
}
else if (parentPos==parentNode->keyNum)
{
brotherNode=parentNode->child[parentPos-1];
}
else
{
brotherNode=parentNode->child[parentPos-1];
if (brotherNode->keyNum<NODE_NUM)//若是左兄弟节点的数目小于t,则判断右边的结点
{
brotherNode=parentNode->child[parentPos+1];
left=false;
}
}
if (brotherNode->keyNum>=NODE_NUM)//1)若兄弟结点的键值数目大于等于t
{
if (left)//若是左边的兄弟
{
for(i=temp->keyNum;i>0;i--)
{
temp->key[i]=temp->key[i-1];//从后往前进行赋值.
}
temp->key[0]=parentNode->key[parentPos-1];//将父结点的值给temp节点
parentNode->key[parentPos-1]=brotherNode->key[brotherNode->keyNum-1];
//将左边节点的最后一个值赋值于父结点相对应的位置上
}
else if(!left)//若是右边的兄弟
{
temp->key[temp->keyNum-1]=parentNode->key[parentPos];//将父结点的值复制于temp这个结点
parentNode->key[parentPos]=brotherNode->key[0];//将兄弟节点的第一个键值赋值于父亲节点
for (i=0;i<brotherNode->keyNum-1;i++)
{
brotherNode->key[i]=brotherNode->key[i+1];//将后面键值赋值于前面的
}
}
brotherNode->key[brotherNode->keyNum-1]=0;
brotherNode->keyNum--;
}
else if (brotherNode->keyNum<NODE_NUM)//2)若是兄弟节点的数目与本身的结点的数目都是小于t的,要进行合并的操作
{
//左右边兄弟节点的不同引之的是父节点中位置也是不同的
if (!left)
{
B_TREE_MERGE_CHILD(parentNode,parentPos,temp,brotherNode);
}
else
{
B_TREE_MERGE_CHILD(parentNode,parentPos-1,brotherNode,temp);
}
break;
}
}
break;
}
else if (!temp->leaf)//3、若是内结点
{
//1)若是节点的左右结点的键值数目都是小于t的,要进行合并
if (temp->child[index]->keyNum<NODE_NUM&&temp->child[index+1]->keyNum<NODE_NUM)
{
parentNode=temp;
B_TREE_MERGE_CHILD(temp,index,temp->child[index],temp->child[index+1]);//下面会进行重新的对这个temp节点的key删除
if (temp->keyNum==0)
{
bTree->root=temp->child[index];
temp=bTree->root;
}
}
//2)若是节点的左右孩子的键值数目有一个是大于等于t的,将这个节点的键值放入父结点中。而此时待删除的节点是这个孩子结点
else if (temp->child[index]->keyNum>=NODE_NUM||temp->child[index+1]->keyNum>=NODE_NUM)
{
bool left=false;
if (temp->child[index]->keyNum>=NODE_NUM)
{
left=true;
}
if (left)
{
temp->key[index]=temp->child[index]->key[temp->child[index]->keyNum-1];//将子结点的键值覆盖于当前待删的结点
key=temp->key[index];
temp=temp->child[index];//进行递归
}
else if(!left)
{
temp->key[index]=temp->child[index+1]->key[0];
key=temp->key[index];
temp=temp->child[index+1];//进行递归
}
}
}
}
else
{
parentNode=temp;
parentPos=index;
temp=temp->child[index];
if (temp->keyNum<NODE_NUM)//当结点的键值数目小于t时,进行合并的操作
{
bool left=false;
if (index==0)//index为找到结点在父结点中的位置,从0开始
{
brotherNode=parentNode->child[index+1];
}
else if(index==parentNode->keyNum)
{
left=true;
brotherNode=parentNode->child[index-1];
}
else
{
brotherNode=parentNode->child[index-1];
if (brotherNode->keyNum<NODE_NUM)
{
left=true;
brotherNode=parentNode->child[index+1];
}
}
if (brotherNode->keyNum<NODE_NUM)//若兄弟结点的键值数目小于t
{
if(left)
{
BNode<T>*t=temp;
temp=brotherNode;
brotherNode=t;
parentPos-=1;
}
B_TREE_MERGE_CHILD(parentNode,parentPos,temp,brotherNode);
if (parentNode->keyNum==0)
{
bTree->root=temp;
parentNode=temp;
}
}
}
}
//还没有找到结点
}
return bfind;//也许存在不存在关键字的情况,那么在调用的函数中可以有输出信息等
}
小结:
1、在这个程序中所欠缺的是,也就是我一开始尝试的是如何关键字的存放不是利用数组,而是动态的。待以后好好的再研究
2、在程序中可以说插入部分在算法导论中已经有伪代码了,但是删除部分,我发现比插入要难很多,首先它的情况就分很多种,出现bug的地方
1)在查找节点时候的index与ParentPos的值错误,删除位置出错
2)在合并时有的时候是要分左右兄弟的,首先我的合并节点是默认是右边的,果断是种错误的想法,其次在合并的时候没有考虑合并的位置会改变,就是说parentPso的值
3)删除节点要考虑当前只有一个节点,就是这个节点为根节点且是叶子结点。就无需进行判断其孩子节点的键值数目。
3、利用B-树的确是相当的不方便,首先删除键值时还要判断是否是在内节点中,若是在内节点中要进行递归。
4、此算法最大的亮点在于避免了回溯,当然也会有回溯,比如说是在合并之后会回到原来的节点中,这就是为什么会有parentNode这一变量的存在。