ktpd 发表于 2016-11-12 18:51

B-树的理解

本帖最后由 ktpd 于 2016-11-12 18:53 编辑

B树是一种适合与外查找的的搜索树,他是一种平衡的多叉树。在B树的每个结点中包含一组指针recptr指向实际记录的存放地址。key与recptr形成一个索引项,通过key可以找到某个记录的存储地址recptr。


M阶的B树具有以下特点:
1、根节点要么是叶子结点,要么至少具有两个孩子。
2、非根结点(叶结点除外)都具有个孩子。
3、非根结点都具有个关键码,关键码以升序排列。
4、key和key之间的结点的值都介于key与key之间
5、数据项都保存在叶子结点中,且叶子结点都在同一层。




M/2是向上取整。
B树的搜索过程是在一个结点内搜索和循某一条路径向下一层搜索交替进行的过程,因此B树的搜索时间与B树的阶数M(决定内部搜索次数)和B树的高度h有关。在B树上进行搜索,搜索成功的时间取决于关键码所在的层次,搜索不成功取决于树的高度。


当数据太大,不能一次放入内存中,磁盘存取的数目就变成关键了。这时候我们就要使用外查找。B树是用于磁盘级查找最流行的数据结构。在B树中结点必须是半数填满的,这就保证了树不会退化成一颗简单的二叉树。




B树的插入:
首先我们要在B树中寻找这个关键码,如果已经存在则插入失败,如果不存在又分为以下两种情况:
1、如果要插入的叶子结点中的关键码的个数小于M-1,则直接插入,然后再重新组织叶子结点中的数据项。
2、如果要插入的叶子结点中的关键码的个数就是M-1,表示叶子结点已满,这时候我们就要经这片叶子分裂为两片叶子。


实现分裂的原则是:
设结点p中已经有了M-1个关键码,当再插入一个关键码后结点中的状态为
(m,p0,k1,p1,k2,p2,k3,....,km,pm)
因为关键码的个数必须是在之间,所以我们必须将p分裂为两个叶子结点p,q。
p:(-1,p0,k1,p1,....,k-1,p-1)
q:(m-,p,k+1,p+1,....,km,pm)
关键码k与指向新结点q的指针形成一个二元组插入到这两个结点p,q的父节点中去。如果父节点也满了则就再分裂,一直到根节点。如果分裂了根,那么我们就创建一个新根,将分裂的这两个根作为新创建的根的孩子,这也是B树增加高度唯一的方法。虽然分裂结点是比较费时的,但是分裂一次我们可以插入多次。
处理孩子溢出还有其他方法,一种方法就是将孩子结点让给具有空间的兄弟收养,但是这样的话会影响到键,所这种方法需要修改父亲,但是它会倾向于让叶子结点更加饱满,会更加节省空间。


B树的删除:
删除的过程与插入的过程相反,如果某叶子丢了孩子,他可能需要和另一片叶子进行合并,我们必须一直沿着树向上合并下去(一直向上合并出现的几率很小),但是再最坏的情况下,根结点丢失了一孩子,然后我们就要删除根,并使用另一个孩子作为根,这也是B树降低高度的唯一方法。
如果想要删除一个关键码我们首先要找到这个关键码,如果B树中没有这个关键码,那么就删除失败。
如果找到了这个关键码,但是这个关键码不再叶子结点中,则应该采用交换删除的思想,应该以结点P所指向的子树中的最小的关键码X来代替要删除的关键码,问题就又转换成了删除X了,直到X出现在叶子中间,我们才真正的删除X。在叶子结点之中删除关键码有以下四种情况。


1、要删除的关键码所在的叶子结点就是根节点,那么直接删除之后再修改根节点中放入数据项即可。


2、要被删除的关键码所在的叶节点不是根节点,且被删除前该叶节点中关键码的个数大于最低限度,即大于M/2-1。则直接删除即可。


3、要被删除的关键码所在的叶节点不是根节点,且被删除前该叶子结点中关键码的个数已经等于最小限度M/2-1,但是这个叶子结点的左兄弟或者右兄弟的关键码个数大于最低限M/2-1。
这时候就可以向他的兄弟收养一个孩子,以达到新的平衡。
收养的方法如下(以收养右兄弟的孩子为例):
   3.1、将父亲结点中刚刚大于被删除关键码的关键码K下移到被删除的位置。
   3.2、将右兄弟中最小的关键码上移到父节点中k的位置
   3.3、将右兄弟中的最左子树的指针平移到被删除关键码所在结点中的最后位置
   3.4、在右兄弟的结点中再将被移走的关键码和指针位置用剩余的关键码和指针填补,调整。


4、被删除的关键码所在的叶节点删除前的关键码个数已经达到最低限度M/2-1,并且它的右兄弟(或左兄弟)也是最低限度M/2-1,则这时候就要将这两个结点合并。
合并方法如下:
4.1、将父结点p中相应的关键码下移到选定保留的结点中,若要合并p子树指针p1和p2所指向的结点,且要保留p1所指结点,则把p中关键码K2下移到p1所指的结点之中。
4.2、把p中子树p2所指结点中的全部指针和关键码都搬到p1所指结点的后面,删除p2所指结点。
4.3、在父节点p中用后面剩余的关键码和指针填补关键码K2和指针p2。
4.4、在修改父节点p和选定保留的结点
在合并的过程中,父节点的关键码个数减少了,所以还要向上继续判断满不满足平衡,如果父节点不满足,则一直重复上面的步骤。最坏的情况下,这种自下而上的处理要到根节点。


//代码实现


#pragma once
#include<utility>
using namespace std;

template<typename K,int M>
struct BTreeNode
{
        K _keys;                         //M是孩子的个数,keys是关键值数组,多开辟一个便于处理
        BTreeNode<K, M> *_subs;      //孩子指针数组
        BTreeNode<K, M> *_parent;         //指向父亲的指针
        size_t _size;                     //记录关键值的个数

        BTreeNode()
                :_parent(NULL)
                , _size(0)
        {
                for (int i = 0; i < M; i++)
                {
                        _keys = K();
                        _subs = NULL;
                }
                _subs = NULL;
        }
};


template<typename K,int M>
class BTree
{
        typedef BTreeNode<K, M> Node;
public:
        BTree()
                :_root(NULL)
        {}

        pair<Node*,int>Find(const K& key)
        {
                Node* cur = _root;
                Node* parent = NULL;
                while (cur)
                {
                        int i = 0;
                        while (i<(int)cur->_size)
                        {
                                if (cur->_keys < key)
                                {
                                        i++;
                                }
                                else if (cur->_keys>key)
                                {
                                        break;
                                }
                                else
                                {
                                        return         pair<Node*, int>(cur,i);      //已经存在了
                                }
                        }
                        parent = cur;
                        cur = cur->_subs;
                }
                return        pair<Node*, int>(parent, -1);      //没找到,返回-1
        }

        bool Insert(const K& key)
        {
                if (_root==NULL)       //如果是空树的话
                {
                        _root = new Node;
                        _root->_keys = key;
                        _root->_size++;
                        _root->_parent = NULL;
                }
                //如果不是空树的话,先找这个key存在还是不存在
                pair<Node*, int>product= Find(key);
                if (product.second != -1)       //表示这个key已经存在了
                {
                        return false;               //已经存在,插入失败
                }

                Node* cur = product.first;
                Node* sub = NULL;
                K newKey = key;
                while (1)               
                {
                        _InsertKey(cur, newKey, sub);             //将关键值插入
                        if (cur->_size == M)                   //如果key已经大于M-1,则需要分裂
                        {
                                Node* tmp = new Node;            //创建一个分裂后的结点
                                int mid = cur->_size / 2;      //找到keys数组的中间位置的下标
                                int j = 0;
                                int i = mid+1;                     //从中间位置的下一个位置开始复制
                                for (; i <(int)cur->_size; ++i, ++j)
                                {
                                        tmp->_keys = cur->_keys;      //将关键字复制到tmp中
                                        tmp->_subs = cur->_subs;      //将孩子也复制到tmp中
                                        if (tmp->_subs)                  //如果孩子不为空,则让它指向分裂的结点
                                                tmp->_subs->_parent= tmp;
                                        cur->_keys = K();
                                        cur->_subs = NULL;
                                        tmp->_size++;
                                }
                                //将最后一个右孩子也复制过来
                                tmp->_subs = cur->_subs;
                                if (tmp->_subs)
                                        tmp->_subs->_parent = tmp;
                                cur->_subs = NULL;

                                newKey = cur->_keys;
                                cur->_keys = K();
                                sub = tmp;
                                cur->_size = mid;

                                if (cur == _root)                //如果分裂的结点是根节点
                                {
                                        _root = new Node;
                                        _root->_keys = newKey;
                                        _root->_subs = cur;
                                        _root->_subs = sub;
                                        cur->_parent = _root;
                                        sub->_parent = _root;
                                        _root->_size = 1;
                                        return true;
                                }
                                cur = cur->_parent;
                        }
                        else                              //不需要分裂,已经平衡
                                break;
                }
                return true;
        }

        bool Remove(const K& key)
        {
                if (_root == NULL)               //树为空树,则删除失败
                {
                        return false;
                }
                pair<Node*, int> product = Find(key);
                if (product.second == -1)            //如果key不再树中,则删除失败
                {
                        return false;
                }
                Node* cur = product.first;         
                int pos = product.second;                //记录要删除的位置
                //将判断要删除的关键码所在结点是不是叶子结点,如果不是的话,要先将关键码交换到叶子结点中
                if (cur->_subs != NULL)    //如果要删除的关键码的右子树不为空,则是非叶子结点的删除
                {
                        Node* minkey = cur->_subs;          //用来记录要删除的关键码的右子树的最小的键值
                        while (minkey->_subs)
                        {
                                minkey = minkey->_subs;
                        }
                        //转换成删除这个最小的关键码
                        cur->_keys = minkey->_keys;
                        cur = minkey;
                        _MoveLeft(cur, 0);         //把交换后的关键码删除掉
                }
                else
                        _MoveLeft(cur, pos);

                //判断是否满足B树的条件,不满足的话就要调整
                int mid =(M+1)/2-1;      //求出关键码个数的下限,关键码最少为M/2-1,向上取整
                while (1)
                {
                        if ((int)cur->_size < mid)   //关键码的个数小于上限值,则要进行调整
                        {
                                if (cur == _root)
                                        break;
                                Node* parent = cur->_parent;
                                pos = 0;
                                while (parent->_subs != cur&&pos < (int)parent->_size)
                                        pos++;
                                if (pos == 0)   //进行左调整
                                        _LeftAdjust(parent, cur, mid, pos);
                                else
                                        _RightAdjust(parent, cur, mid, pos-1);
                                cur = parent;
                        }
                        else                     //如果不小于上限值的话,则表示已经满足B树的条件,则直接退出
                                break;
                }

                if (_root->_size == 0)         //如果调整之后根的关键码个数已经减为0,则要把当前根删除掉,把他的孩子当做根
                {
                        Node* del = _root;            //记录要删的这个根
                        _root = _root->_subs;
                        if (_root)
                          _root->_parent = NULL;      //将新根的父亲置空
                        delete del;                  
                }

                return true;
        }

        void InOder()
        {
                _InOder(_root);
        }

        ~BTree()
        {
                _Destory(_root);
        }
protected:
        void _LeftAdjust(Node* parent, Node* cur, int mid, int pos)      //当前结点cur在他父亲的左边,cur与他的右兄弟进行调整
        {
                //如果cur的右兄弟的关键码的个数已经大于关键码的上限,则就通过收养解决
                Node* right = parent->_subs;       //right指向cur的左兄弟
                if ((int)right->_size>mid)      //大于关键码的上限,通过收养解决
                {
                        cur->_size++;
                        cur->_keys = parent->_keys;       //把父节点的相应关键码下移
                        parent->_keys = right->_keys;                  //把右兄弟的最小关键码上移到父亲关键码位置
                        //把右兄弟的最左孩子移动到cur的最右孩子处
                        cur->_subs = right->_subs;
                        if (right->_subs)
                        {
                                right->_subs->_parent = cur;            //让这个孩子的父亲指向cur
                        }
                        right->_subs = right->_subs;
                        _MoveLeft(right, 0);                                    //把右兄弟中剩余的关键码向左移动
                }
                else            //只能通过合并解决
                        _Merge(parent,cur,right,pos);
        }

        void _RightAdjust(Node* parent, Node* cur,int mid, int pos)//当前结点cur在他父亲的右边,cur与他的左兄弟进行调整
        {
                Node* left = parent->_subs;          //left指向cur的左兄弟
                if ((int)left->_size>mid)               //左兄弟的关键码个数大于关键码上限的值
                {
                        //cur先把最左边的位置空出来,再把父亲结点的相应关键码下移
                        _MoveRight(cur,0);
                        cur->_keys = parent->_keys;      //父亲相应关键码下移
                        parent->_keys = left->_keys;    //将左兄弟中的最大关键码上移到父节点位置
                        cur->_subs = left->_subs;         //将左兄弟中的最后一个孩子移到cur的左边
                        if (left->_subs)
                        {
                                left->_subs->_parent = cur;         //将这个孩子结点的父亲指向cur
                        }

                        left->_keys = K();
                        left->_subs = NULL;
                        left->_size--;
                }
                else
                        _Merge(parent,left,cur,pos);
        }


        void _Merge(Node* parent,Node* cur,Node* brother,int pos)      //保留cur,合并兄弟结点
        {
                int i = cur->_size;   //要插入的位置
                cur->_keys = parent->_keys;      //先把父亲结点相应的关键码的值移动到左孩子中
                cur->_subs = brother->_subs;      //把右兄弟的最左孩子移动过来
                if (brother->_subs)
                        brother->_subs->_parent = cur;
                i++;
                cur->_size++;
                for (int j = 0; j < (int)brother->_size; ++i, ++j)   //将兄弟结点指针拷贝过来
                {
                        cur->_keys = brother->_keys;
                        cur->_subs = brother->_subs;
                        if (brother->_subs)
                                brother->_subs->_parent = cur;
                        cur->_size++;
                }
                if (parent->_subs == brother)
                        parent->_subs = NULL;
                else
                        parent->_subs = NULL;

                _MoveLeft(parent,pos);       
                delete brother;
        }

        void _Destory(Node* cur)
        {
                if (cur == NULL)
                        return;
                int i = 0;
                for (i = 0; i < (int)cur->_size; i++)
                {
                        _Destory(cur->_subs);
                        delete cur->_subs;
                }
                _Destory(cur->_subs);                  //遍历最右边的孩子
                delete cur->_subs;
        }

        void _InOder(Node* cur)
        {
                if (cur == NULL)
                        return;
                int i = 0;
                for (i = 0; i < (int)cur->_size; i++)
                {
                        _InOder(cur->_subs);
                        cout << cur->_keys << "";
                }
                _InOder(cur->_subs);                  //遍历最右边的孩子
        }

        void _InsertKey(Node* cur,const K& key,Node* sub)
        {
                int i = cur->_size-1;
                while (i >= 0)      //将key插入到B树中
                {
                        if (key < cur->_keys)
                        {
                                cur->_keys = cur->_keys;
                                cur->_subs = cur->_subs;
                                --i;
                        }
                        else                                 //key比当前关键值小,则进行插入
                                break;
                }

                //key是这组keys中最小的,放在第0个位置
                cur->_keys = key;
                cur->_subs = sub;
                if (sub != NULL)                  //孩子不为空
                        sub->_parent = cur;          //孩子的parent指向cur
                cur->_size++;
        }

        void _MoveLeft(Node* cur,int pos)                  //在叶子结点中删除一个关键码
        {
                if (cur == NULL)
                        return;
                int i = 0;
                for (i = pos; i < (int)cur->_size; ++i)
                {
                        cur->_keys=cur->_keys;               //将要删除的关键码覆盖掉
                        cur->_subs = cur->_subs;
                }
                cur->_size--;                                    
        }

        void _MoveRight(Node* cur,int pos)
        {
                int i = cur->_size-1;       //当前结点最右关键码下标
                for (; i >=pos; --i)
                {
                        cur->_keys = cur->_keys;
                        cur->_subs = cur->_subs;
                }
                cur->_subs = cur->_subs;
                cur->_size++;
        }
private:
        Node* _root;
};


dxdeng 发表于 2016-11-12 18:59

学习了大神

南南暖 发表于 2016-11-12 23:07

好厉害,看看

飘荡的心 发表于 2016-11-18 16:12

大神能不能加上一个测试的主函数,表示太难了看不太懂
页: [1]
查看完整版本: B-树的理解