搜索结构之B-Tree

本文详细介绍了B树的基本概念、查找和插入操作,以及其实现代码。通过具体的例子展示了B树的特性,如平衡性和如何通过分裂结点来维持这种平衡。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值