跳过正文
  1. Posts/

手写B树全代码实现

·10673 字·22 分钟· loading
DogDu
作者
DogDu
相信代码的力量,用实现驱动学习
目录

前言
#

建议先了解 B 树的基本定义,再来看这篇实现记录;本文更关注“怎么把它写出来”,而不是只停留在性质介绍上。

如果说 AVL 树的难点在于旋转时机,那么 B 树更折磨人的地方在于删除策略:借位、合并、向上回溯修正,任何一个细节漏掉都可能让整棵树失去结构约束。也正因为这一点,网上很多资料会停留在插入示意或局部代码,真正把删除过程完整讲清楚的内容并不多。

这篇文章尝试把 B 树从节点定义、查找、分裂、删除到调试方法完整串起来,并配合打印函数去验证结构变化。虽然它不是拿 P3369 那类模板题直接校验出来的版本,但足够适合作为一次从概念到实现的完整练习。

另外,文中也会提到一篇对我帮助很大的参考文章:它把插入思路讲得很清楚,也帮我梳理了删除逻辑,只是原始版本在 erase 上还有一些考虑不够严密的地方。这里会在吸收其思路的基础上,把实现补完整。

定义
#

来看定义:

CPP 代码 · 共 34 行
#define m 5

struct node
{
    int key, value;
    bool operator<(const node &it) const
    {
        return key < it.key;
    }
    bool operator>(const node &it) const
    {
        return key > it.key;
    }
    bool operator==(const node &it) const
    {
        return key == it.key;
    }
};

struct BTnode
{
    BTnode *parent = nullptr;
    int keynum = 0;
    BTnode *child[m + 1] = {0};
    node key[m + 1];
    int Size[m+1]={0};
};

struct Result
{
    BTnode *ptr;
    bool tag;
    int i;
};

与 数据结构-严蔚敏 课本上的一致,除了删除了record节点,增加了node节点用来方便桶排检验代码正确性。

CPP 代码 · 共 15 行
class B_Tree
{
private:
    int Size;
    BTnode *root;
    const int key_min = (m - 1) / 2;
    const int mid = (m + 1) / 2;
    const int key_max = m - 1;

public:
    B_Tree()
    {
        Size = 0;
        root = nullptr;
    }

定义,为什么AVL树中选择搞一个头结点,这里却没有呢?显然是没有必要,有没有对代码都没有明显的帮助作用。注意private中 mid表示中间的节点,key_max表示最大节点个数,key_min表示最小节点个数。

辅助函数与查找函数
#

先看辅助函数:

CPP 代码 · 共 78 行
    int findpos(BTnode *&q, node &x) //作用:找这个节点中第一个大于等于x的下标,PS:永远不可能为0
    {
        return lower_bound(q->key + 1, q->key + 1 + q->keynum, x) - q->key;
    }
    
//    下面三个是调试用打印函数,非常重要!!!

    /* 计算出整棵树上记录条数的总和 */
    int CountKeyNum(BTnode *tree)
    {
        // 空树则为 0
        if (tree == NULL)
            return 0;

        // 计算所有子树记录条数总和
        int childTotal = 0;
        for (int i = 0; i <= tree->keynum; i++)
        {
            childTotal += CountKeyNum(tree->child[i]);
        }

        // 子树加上自身的记录条数,即为总记录条数
        return tree->keynum + childTotal;
    }

    /* 打印 B 树 */
    void TraverseBTree(BTnode *tree = nullptr)
    { // 看的别人代码直接复制粘贴的调试函数

        // 动态创建一个数组,用于存放当前结点是否为最后结点的标识
        tree = this->root;
        int nowNum = 0;
        int *isLast = (int *)malloc(sizeof(int) * (CountKeyNum(tree) + 10));

        // 打印树形
        printf("\n");
        PrintBTree(tree, isLast, nowNum);
    }

    /* 递归打印树形 */
    void PrintBTree(BTnode *tree, int isLast[], int nowNum)
    {
        // 空树直接跳过
        if (tree == nullptr)
            return;

        // 打印新节点先打印一下平台
        printf("-|\n");
        for (int i = 0; i <= tree->keynum; i++)
        {
            if (tree->child[i] != NULL)
            {
                printBranch(isLast, nowNum);
                printf("|----");
                isLast[nowNum++] = (i == tree->keynum);
                PrintBTree(tree->child[i], isLast, nowNum);
                nowNum--;
            }
            if (i != tree->keynum)
            {
                printBranch(isLast, nowNum);
                printf("|- %d\n", tree->key[i + 1].key);
            }
        }
    }

    /* 打印树枝 */
    void printBranch(int isLast[], int nowNum)
    {
        for (int i = 0; i < nowNum; i++)
        {
            if (isLast[i])
                printf(" ");
            else
                printf("|");
            printf("      ");
        }
    }

讲真的,这个打印函数在编代码的帮助真的是非常之多,可以快速用来发现错误。尤其是erase中的调整树型。

先看最简单的search函数:

CPP 代码 · 共 25 行
    Result search(int x) // 找位置,函数很简单,没啥好说的
    {
        BTnode *p = root, *q = nullptr;
        int i;
        bool flag = 0;
        node key = {x, 1};

        while (p != nullptr && !flag)
        {
            i = findpos(p, key);

            if (i <= p->keynum && p->key[i] == key)
                flag = 1;
            else
            {
                q = p;
                p = p->child[i - 1];
            }
        }

        if (flag)//查找成功,返回节点
            return {p, flag, i};
        else//查找失败,返回插入节点
            return {q, flag, i};
    }

逻辑简单易懂,也没啥好说的,主要是要注意一下 i 表示的范围是 [1,keynum+1] ,这一点在后面很有用。

插入函数
#

再看insert函数及其辅助函数:

CPP 代码 · 共 99 行
    bool insert(int x)
    {
        Result res = search(x);

        if (res.tag) // 找到了,就个数+1
        {
            ++res.ptr->key[res.i].value;
            return false;
        }
        else//没找到,就插入,
        {
            insert(root, {x, 1}, res);
            ++Size;
            return true;
        }
    }
    void newroot(BTnode *&root, BTnode *&child1, BTnode *&child2, node &key) // 如果根节点出现了分裂或者初始为空,需要新建一个节点,这个时候才会出现这个函数
    {
        root = new BTnode;
        root->keynum = 1;
        root->key[1] = key;
        root->child[0] = child1;
        root->child[1] = child2;
        root->parent = nullptr;

        if (child1 != nullptr)
            child1->parent = root;
        if (child2 != nullptr)
            child2->parent = root;
        return;
    }

    void split(BTnode *&p, BTnode *&ap) // p分裂,并分给ap
    {
        ap = new BTnode;
        ap->child[0] = p->child[mid]; // 别忘了

        for (int i = mid + 1, j = 1; i <= p->keynum; ++i, ++j)//赋值
            ap->key[j] = p->key[i],
            ap->child[j] = p->child[i];

        ap->parent = p->parent;
        ap->keynum = p->keynum - mid;
        p->keynum = mid - 1;

        // 双亲和孩子的双向绑定
        for (int i = 0; i <= ap->keynum; ++i)
            if (ap->child[i] != nullptr)
                ap->child[i]->parent = ap;

        return;
    }

    void InsertBTnode(BTnode *&p, node &key, BTnode *ap) // 给出节点,值和指针,不要给出i!!!不然会变得不幸!
    {
        int i = findpos(p, key); // i在这里现场获得!!!

        for (int j = p->keynum; j >= i; --j)
            p->key[j + 1] = p->key[j],
                       p->child[j + 1] = p->child[j];

        p->child[i] = ap;
        p->key[i] = key;
        p->keynum++;
        if (ap != nullptr)//双向绑定
            ap->parent = p;
        return;
    }

    void insert(BTnode *&root, node key, Result &res) // 插入
    {
        BTnode *ap = nullptr, *p = res.ptr;
        bool finished = false;
        // int i = res.i;//这个i其实屁用没有,因为我坚持i一定要现场获得,不然会变得不幸!

        while (p != nullptr && !finished)
        {
            InsertBTnode(p, key, ap);

            if (p->keynum <= key_max)
                finished = 1;
            else
            {
                split(p, ap);
                key = p->key[mid];

                if (p->parent != nullptr)
                {
                    p = p->parent;
                    // i = findpos(p, key);//不要管i!!!
                }
                else
                    break;
            }
        }

        if (!finished) // finished为空,说明p为根,如果p是根,说明要么根分裂了,要么树为空
            newroot(root, p, ap, key);
    }

注意三点:

1.这里可以看到我多次注释,不要管i,也就是位置,把它放在InsertBTnode函数中自己处理,这个原则一定要坚持,换句话说就是函数的接口要尽可能简单,实现的功能可以少,就像AVL树中结构体中不要定义bf一样,只有吃过屎你才知道为什么这么干而不那么干。

2.一定要注意父亲和孩子的双向绑定,不能我能找到你,你找不到我。

3.这一点非常重要,不要着急去写删除函数!!!在这里停下来,因为有了insert函数和search函数就可以用来桶排序了,可以现在洛谷P1177中检验insert函数正确性,还有用打印函数判断B-树的树型是否正确,这非常重要!!!

删除函数
#

接下来是难中之难,删除函数:

先看完整代码体验一下其恐怖:

CPP 代码 · 共 176 行
    bool erase(int x)
    {
        Result res = search(x);

        if (res.tag)
        {
            if (--res.ptr->key[res.i].value == 0)
                erase(root, {x, 1}, res);
            return true;
        }
        else
            return false;
    }
    void Merge(BTnode *&left, BTnode *&right) // 把右边归并到左边,并删除右边的节点
    {
        for (int i = left->keynum + 1, j = 1; j <= right->keynum; ++j, ++i)
        {
            left->key[i] = right->key[j];
            left->child[i] = right->child[j];

            if (left->child[i] != nullptr) // 把孩子更改父亲
                left->child[i]->parent = left;
            ++left->keynum;
        }

        delete right;
        right = left;
    }

    void Adjust(BTnode *&p) // 调整树型
    {
        BTnode *parent = p->parent, *brother;
        int i;

        while (p->keynum < key_min && parent != nullptr) // 如果节点个数比较少,同时不为根节点
        {
            i = findpos(parent, p->key[1]); // 父亲的编号 当前在 child[i-1] 中

            if (i > 1 && (brother = parent->child[i - 2]) != nullptr) // 左兄弟不为空, 当前在 child[i-1] 那么 child[i-2]就是左兄弟了
            {
                if (brother->keynum > key_min) // 左兄弟富裕
                {
                    InsertBTnode(p, parent->key[i-1], brother->child[brother->keynum]); // 把父亲的节点要下来,注意把左兄弟最右端的指针也拿下来
                    swap(p->child[0],p->child[1]);
                    brother->child[brother->keynum]=nullptr;
                    // 这里不需要像有兄弟一样把 child[brother->keynum] 置空,因为减1了,画个图就明白了
                    parent->key[i-1] = brother->key[brother->keynum]; // 然后父亲替换成左兄弟的最大值
                    Remove(brother, brother->key[brother->keynum]); // 左兄弟删除这个节点,
                    break;                                          // 调整完成,break
                }
                else // 左兄弟穷
                {
                    InsertBTnode(brother, parent->key[i-1], p->child[0]); // 把父亲要下来,这个时候父亲那个节点要被删除,把兄弟最右端指针拿下来
                    p->child[0]=nullptr;
                    Remove(parent, parent->key[i-1]);           // 删除父亲
                    Merge(brother, p);                        // 合并兄弟

                    p = p->parent; // 父亲可能节点变少,继续循环
                    parent = p->parent;
                }
            }
            else if (i <= parent->keynum && (brother = parent->child[i]) != nullptr) // 有右兄弟
            {
                if (brother->keynum > key_min) // 右兄弟富裕
                {
                    InsertBTnode(p, parent->key[i], brother->child[0]);
                    brother->child[0] = brother->child[1]; // 这一句别忘了多加,因为右兄弟最左端节点被我拿走了,所以附带的他也为空
                    parent->key[i] = brother->key[1];
                    Remove(brother, brother->key[1]);
                    break;
                }
                else // 右兄弟穷
                {
                    InsertBTnode(p, parent->key[i], brother->child[0]);
                    Remove(parent, parent->key[i]);
                    Merge(p, brother); // 注意这个时候p在左边,brother在右边

                    p = p->parent;
                    parent = p->parent;
                }
            }
            else // 如果没有直接相邻的左右兄弟,啥也不管了,直接把父亲拽下来,然后迭代父亲
            {
                InsertBTnode(p, parent->key[i], nullptr);//没有左右兄弟,所以为nullptr
                Remove(parent, parent->key[i]); // 这样是合理的,因为 parent->child[i] 为空

                p = p->parent;
                parent = p->parent;
            }

            //TraverseBTree();
        }

        // 如果变成了根节点,根据定义,根至少有两个孩子,但可以keynum可以少于key_min

        if (parent == nullptr && p->keynum == 0) // 如果根节点keynum为零了,找一个孩子替代它
        {
            root=(root->child[0]==nullptr)?root->child[1]:root->child[0];
            root->parent=nullptr;
            delete p;
        }
    }

    void Remove(BTnode *&p, node &x) // 删除函数,给出节点指针和数值,别管这个数值在哪里,不然很麻烦,交给这个函数处理
    {
        int i = findpos(p, x);
        for (int j = i; j <= p->keynum; ++j)
            p->key[j] = p->key[j + 1],
            p->child[j] = p->child[j + 1];
        --p->keynum;
    }

    void SuccessOr(Result &res) // 找到后继,替代,并且删除本身
    {
        // 注意是引用
        int &i = res.i;
        BTnode *&p = res.ptr;

        if (p->child[i - 1] != nullptr) // 有直接左孩子
        {
            BTnode *pre = p->child[i - 1];

            while (pre->child[pre->keynum] != nullptr)
                pre = pre->child[pre->keynum];

            p->key[i] = pre->key[p->keynum];
            p = pre;
            i = p->keynum;
            Remove(p, p->key[i]);
        }
        else if (p->child[i] != nullptr) // 有直接右孩子
        {
            BTnode *next = p->child[i];

            while (p->child[0] != nullptr)
                p = p->child[0];

            p->key[i] = next->key[1];
            p = next;
            i = 1;
            Remove(p, p->key[i]);
        }
        else if (i > 1) // 有直接左兄弟
        {
            p->child[i - 1] = p->child[i];
            Remove(p, p->key[i]);
        }
        else if (i < p->keynum) // 有直接右兄弟 为什么这里少一句话,可以思考一下。
        {
            Remove(p, p->key[i]);
        }
        // else
        // //在本层及以下没有前驱后继,也就是说这个节点只剩下一个了,
        // //删除这个节点之后应该直接释放这个节点,但实际上这个情况不可能出现(m大于4时)所以不管它了
        // {
        //     Remove(p, p->key[i]);
        // }
    }
    void erase(BTnode *&root, node key, Result &res)
    {
        SuccessOr(res);

       // BTnode *p = res.ptr;
         
        Adjust(res.ptr);

       // if (p->parent != nullptr && p->keynum < key_min)
       // {
       //     Adjust(p); // 调整
       // }
        // else if (p->parent == nullptr && p->keynum == 0)//如果出现了这种情况,不应该这样处理,应该想Adjust那样处理
        // {
        //     delete root;
        //     root = nullptr;
        // }
    }

其中,第一个erase函数为public,其他全部为private。

1.看最后的erase函数,它实现了什么功能:调用SuccessOr函数,然后Adjust函数,(其实参数中的key压根没有用到,因为result已经给了所需要的信息。)

2.来看SuccessOr函数,它的功能:在以删除节点为根的树下,找删除节点的直接前驱和直接后继。再来看实现,先明确i是什么,i是删除节点在 key数组的位置。

策略:先找直接前驱,没有直接前驱,找直接后继,先看有没有直接左孩子,有的话就和AVL树一样找叶子哪里的直接前驱,没有的话有左兄弟就直接删除本身,随便把节点改一下(至于为什么只有这里需要改,可以考虑一下。)右边是一样的策略。

也就是说,这个删除就是删除了节点同时移动指针,不管之后会发生什么,有问题就让Adjust来调整树型就好了。

3.全篇最难:调整树型(我挣扎了很长时间。)

循环条件:不为根且节点个数足够。(注意,如果是根的话,即使个数小于key_min也是可以的。)

策略:

1.有左兄弟,且左兄弟富裕。则把父亲插入当前节点,同时把父亲节点赋值成brother的最右节点,最后删除brother最右节点,注意:插入节点的时候要把brother最右儿子给带过去,之后直接break已经不再需要迭代。 思考一下:为什么要有swap,但是在另一种情况下却不需要。

2.有左兄弟,且左兄弟穷。把父亲给拿下来(必须,原因是,等会左右孩子要合并,这是父亲中孩子节点少一个,为了保持B-树的性质,必须要把父亲给拿走一个,不然child没法填。)之后是删除节点和合并兄弟,最后迭代父亲。

3.有右兄弟,且右兄弟富裕。方法与1.相同,但是不需要swap(思考为什么)但多了一句brother->child[0]=brother->child[1],其实这两个不一样的两句话干的事情是类似的,然后直接break。

4.有右兄弟,且右兄弟穷。与2.相同,但注意合并是p在左。

5.没有左右兄弟(这是可能的!),直接找父亲要(注意一定要的是parent->key[i],而不是相邻的另一个parent->key[i-1],这是为了方便Remove函数,可以少写一句parent->child[i-1]=parent->child[i]),然后删除父亲中的对应节点。

循环结束之后,判断是否是根节点,如果是根节点并且根节点被删空,则选择他的一个儿子来替代它本人,最后删除这个舍弃的根节点。

这就是Adjust全过程。然后来看各个辅助函数:

1.Remove 给出节点指针和要删除的值,然后删除同时移动child指针,不要在函数接口有i这个东西。!!!同时注意双亲的双向绑定。

2.Merge给出两个节点指针,把right归并到left中,同时释放right,最后把right置成left,方便后面操作。一个问题:为什么right->child[0]没有被归并代码却是正确的?答案:这一点在函数外保证。

代码结束!

其实洛谷的P1177由于排序的缘故,是从前向后进行erase的,不能完美的测试erase,可以自己造几个数据来判断是否erase合理,当然也可以用这里的代码去写洛谷的P3369普通平衡树来测试,但是我太懒了,就没写P3369,只写了P1177来测试再加上自己的一些手写数据。

完整代码和AC记录
#

完整代码(整整500行代码,虽然有部分不是代码。):

CPP 代码 · 共 499 行
#include <iostream>
#include <cmath>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <fstream>

#define ll long long

using namespace std;

// B-树给我的启示:
// 1.不要在脑子模糊的情况下写算法
// 2.函数的接口要尽可能简单,越方便调用越好
// 3.打持久战要有耐心
// 4.调试函数的存在可能比一个有用的函数更重要
// 5.不要超过两个小时持续思考
// 6.别人的代码可以看,可以借鉴,但是不能全信,
//我看的别人的代码想法很美妙,但是不能细想,一细想发现漏洞百出
// 7.有想法了就去实现
// 8.对拍或者利用oj是个不错的想法
// 9.与AVL树相同,对自己的数据结构制定一个规则,然后通过优雅的方式实现,代码的效率就会极大地提升

//吐槽:B-树真的不仅仅是把分块思想用到了排序树吗?
#define m 35

struct node
{
    int key, value;
    bool operator<(const node &it) const
    {
        return key < it.key;
    }
    bool operator>(const node &it) const
    {
        return key > it.key;
    }
    bool operator==(const node &it) const
    {
        return key == it.key;
    }
};

struct BTnode
{
    BTnode *parent = nullptr;
    int keynum = 0;
    BTnode *child[m + 1] = {0};
    node key[m + 1];
    int Size[m + 1] = {0};
};

struct Result
{
    BTnode *ptr;
    bool tag;
    int i;
};

class B_Tree
{
private:
    int Size;
    BTnode *root;
    const int key_min = (m - 1) / 2;
    const int mid = (m + 1) / 2;
    const int key_max = m - 1;

public:
    B_Tree()
    {
        Size = 0;
        root = nullptr;
    }

    bool insert(int x)
    {
        Result res = search(x);

        if (res.tag) // 找到了,就个数+1
        {
            ++res.ptr->key[res.i].value;
            return false;
        }
        else
        {
            insert(root, {x, 1}, res);
            ++Size;
            return true;
        }
    }

    Result search(int x) // 找位置,函数很简单,没啥好说的
    {
        BTnode *p = root, *q = nullptr;
        int i;
        bool flag = 0;
        node key = {x, 1};

        while (p != nullptr && !flag)
        {
            i = findpos(p, key);

            if (i <= p->keynum && p->key[i] == key)
                flag = 1;
            else
            {
                q = p;
                p = p->child[i - 1];
            }
        }

        if (flag)
            return {p, flag, i};
        else
            return {q, flag, i};
    }

    bool erase(int x)
    {
        Result res = search(x);

        if (res.tag)
        {
            if (--res.ptr->key[res.i].value == 0)
                erase(root, {x, 1}, res);
            return true;
        }
        else
            return false;
    }

    /* 计算出整棵树上记录条数的总和 */
    int CountKeyNum(BTnode *tree)
    {
        // 空树则为 0
        if (tree == NULL)
            return 0;

        // 计算所有子树记录条数总和
        int childTotal = 0;
        for (int i = 0; i <= tree->keynum; i++)
        {
            childTotal += CountKeyNum(tree->child[i]);
        }

        // 子树加上自身的记录条数,即为总记录条数
        return tree->keynum + childTotal;
    }

    /* 打印 B 树 */
    void TraverseBTree(BTnode *tree = nullptr)
    { // 看的别人代码直接复制粘贴的调试函数

        // 动态创建一个数组,用于存放当前结点是否为最后结点的标识
        tree = this->root;
        int nowNum = 0;
        int *isLast = (int *)malloc(sizeof(int) * (CountKeyNum(tree) + 10));

        // 打印树形
        printf("\n");
        PrintBTree(tree, isLast, nowNum);
    }

    /* 递归打印树形 */
    void PrintBTree(BTnode *tree, int isLast[], int nowNum)
    {
        // 空树直接跳过
        if (tree == nullptr)
            return;

        // 打印新节点先打印一下平台
        printf("-|\n");
        for (int i = 0; i <= tree->keynum; i++)
        {
            if (tree->child[i] != NULL)
            {
                printBranch(isLast, nowNum);
                printf("|----");
                isLast[nowNum++] = (i == tree->keynum);
                PrintBTree(tree->child[i], isLast, nowNum);
                nowNum--;
            }
            if (i != tree->keynum)
            {
                printBranch(isLast, nowNum);
                printf("|- %d\n", tree->key[i + 1].key);
            }
        }
    }

    /* 打印树枝 */
    void printBranch(int isLast[], int nowNum)
    {
        for (int i = 0; i < nowNum; i++)
        {
            if (isLast[i])
                printf(" ");
            else
                printf("|");
            printf("      ");
        }
    }

private:
    
    void Merge(BTnode *&left, BTnode *&right) // 把右边归并到左边,并删除右边的节点
    {
        for (int i = left->keynum + 1, j = 1; j <= right->keynum; ++j, ++i)
        {
            left->key[i] = right->key[j];
            left->child[i] = right->child[j];

            if (left->child[i] != nullptr) // 把孩子更改父亲
                left->child[i]->parent = left;
            ++left->keynum;
        }

        delete right;
        right = left;
    }

    void Adjust(BTnode *&p) // 调整树型
    {
        BTnode *parent = p->parent, *brother;
        int i;

        while (p->keynum < key_min && parent != nullptr) // 如果节点个数比较少,同时不为根节点
        {
            i = findpos(parent, p->key[1]); // 父亲的编号 当前在 child[i-1] 中

            if (i > 1 && (brother = parent->child[i - 2]) != nullptr) // 左兄弟不为空, 当前在 child[i-1] 那么 child[i-2]就是左兄弟了
            {
                if (brother->keynum > key_min) // 左兄弟富裕
                {
                    InsertBTnode(p, parent->key[i - 1], brother->child[brother->keynum]); // 把父亲的节点要下来,注意把左兄弟最右端的指针也拿下来
                    swap(p->child[0], p->child[1]);
                    brother->child[brother->keynum] = nullptr;
                    // 这里不需要像有兄弟一样把 child[brother->keynum] 置空,因为减1了,画个图就明白了
                    parent->key[i - 1] = brother->key[brother->keynum]; // 然后父亲替换成左兄弟的最大值
                    Remove(brother, brother->key[brother->keynum]);     // 左兄弟删除这个节点,
                    break;                                              // 调整完成,break
                }
                else // 左兄弟穷
                {
                    InsertBTnode(brother, parent->key[i - 1], p->child[0]); // 把父亲要下来,这个时候父亲那个节点要被删除,把兄弟最右端指针拿下来
                    p->child[0] = nullptr;
                    Remove(parent, parent->key[i - 1]); // 删除父亲
                    Merge(brother, p);                  // 合并兄弟

                    p = p->parent; // 父亲可能节点变少,继续循环
                    parent = p->parent;
                }
            }
            else if (i <= parent->keynum && (brother = parent->child[i]) != nullptr) // 有右兄弟
            {
                if (brother->keynum > key_min) // 右兄弟富裕
                {
                    InsertBTnode(p, parent->key[i], brother->child[0]);
                    brother->child[0] = brother->child[1]; // 这一句别忘了多加,因为右兄弟最左端节点被我拿走了,所以附带的他也为空
                    parent->key[i] = brother->key[1];
                    Remove(brother, brother->key[1]);
                    break;
                }
                else // 右兄弟穷
                {
                    InsertBTnode(p, parent->key[i], brother->child[0]);
                    Remove(parent, parent->key[i]);
                    Merge(p, brother); // 注意这个时候p在左边,brother在右边

                    p = p->parent;
                    parent = p->parent;
                }
            }
            else // 如果没有直接相邻的左右兄弟,啥也不管了,直接把父亲拽下来,然后迭代父亲
            {
                InsertBTnode(p, parent->key[i], nullptr); // 没有左右兄弟,所以为nullptr
                Remove(parent, parent->key[i]);           // 这样是合理的,因为 parent->child[i] 为空

                p = p->parent;
                parent = p->parent;
            }

            // TraverseBTree();
        }

        // 如果变成了根节点,根据定义,根至少有两个孩子,但可以keynum可以少于key_min

        if (parent == nullptr && p->keynum == 0) // 如果根节点keynum为零了,找一个孩子替代它
        {
            root = (root->child[0] == nullptr) ? root->child[1] : root->child[0];
            root->parent = nullptr;
            delete p;
        }
    }

    void Remove(BTnode *&p, node &x) // 删除函数,给出节点指针和数值,别管这个数值在哪里,不然很麻烦,交给这个函数处理
    {
        int i = findpos(p, x);
        for (int j = i; j <= p->keynum; ++j)
            p->key[j] = p->key[j + 1],
            p->child[j] = p->child[j + 1];
        --p->keynum;
    }

    void SuccessOr(Result &res) // 找到后继,替代,并且删除本身
    {
        // 注意是引用
        int &i = res.i;
        BTnode *&p = res.ptr;

        if (p->child[i - 1] != nullptr) // 有直接左孩子
        {
            BTnode *pre = p->child[i - 1];

            while (pre->child[pre->keynum] != nullptr)
                pre = pre->child[pre->keynum];

            p->key[i] = pre->key[p->keynum];
            p = pre;
            i = p->keynum;
            Remove(p, p->key[i]);
        }
        else if (p->child[i] != nullptr) // 有直接右孩子
        {
            BTnode *next = p->child[i];

            while (p->child[0] != nullptr)
                p = p->child[0];

            p->key[i] = next->key[1];
            p = next;
            i = 1;
            Remove(p, p->key[i]);
        }
        else if (i > 1) // 有直接左兄弟
        {
            p->child[i - 1] = p->child[i];
            Remove(p, p->key[i]);
        }
        else if (i < p->keynum) // 有直接右兄弟
        {
            Remove(p, p->key[i]);
        }
        // else
        // //在本层及以下没有前驱后继,也就是说这个节点只剩下一个了,
        // //删除这个节点之后应该直接释放这个节点,但实际上这个情况不可能出现(m大于4时)所以不管它了
        // {
        //     Remove(p, p->key[i]);
        // }
    }

    void erase(BTnode *&root, node key, Result &res)
    {
        SuccessOr(res);

        // BTnode *p = res.ptr;

        Adjust(res.ptr);

        // if (p->parent != nullptr && p->keynum < key_min)
        // {
        //     Adjust(p); // 调整
        // }
        // else if (p->parent == nullptr && p->keynum == 0)//如果出现了这种情况,不应该这样处理,应该想Adjust那样处理
        // {
        //     delete root;
        //     root = nullptr;
        // }
    }

    void newroot(BTnode *&root, BTnode *&child1, BTnode *&child2, node &key) // 如果根节点出现了分裂或者初始为空,需要新建一个节点
    {
        root = new BTnode;
        root->keynum = 1;
        root->key[1] = key;
        root->child[0] = child1;
        root->child[1] = child2;
        root->parent = nullptr;

        if (child1 != nullptr)
            child1->parent = root;
        if (child2 != nullptr)
            child2->parent = root;
        return;
    }

    void split(BTnode *&p, BTnode *&ap) // p分裂,并分给ap
    {
        ap = new BTnode;
        ap->child[0] = p->child[mid]; // 别忘了

        for (int i = mid + 1, j = 1; i <= p->keynum; ++i, ++j)
            ap->key[j] = p->key[i],
            ap->child[j] = p->child[i];

        ap->parent = p->parent;
        ap->keynum = p->keynum - mid;
        p->keynum = mid - 1;

        // 双亲和孩子的双向绑定
        for (int i = 0; i <= ap->keynum; ++i)
            if (ap->child[i] != nullptr)
                ap->child[i]->parent = ap;

        return;
    }

    void InsertBTnode(BTnode *&p, node &key, BTnode *ap) // 给出节点,值和指针,不要给出i!!!不然会变得不幸!
    {
        int i = findpos(p, key); // i在这里现场获得!!!

        for (int j = p->keynum; j >= i; --j)
            p->key[j + 1] = p->key[j],
                       p->child[j + 1] = p->child[j];

        p->child[i] = ap;
        p->key[i] = key;
        p->keynum++;
        if (ap != nullptr)
            ap->parent = p;
        return;
    }

    void insert(BTnode *&root, node key, Result &res) // 插入
    {
        BTnode *ap = nullptr, *p = res.ptr;
        bool finished = false;
        // int i = res.i;//这个i其实屁用没有,因为我坚持i一定要现场获得,不然会变得不幸!

        while (p != nullptr && !finished)
        {
            InsertBTnode(p, key, ap);

            if (p->keynum <= key_max)
                finished = 1;
            else
            {
                split(p, ap);
                key = p->key[mid];

                if (p->parent != nullptr)
                {
                    p = p->parent;
                    // i = findpos(p, key);//不要管i
                }
                else
                    break;
            }
        }

        if (!finished) // 如果p是根,说明要么根分裂了,要么树为空
            newroot(root, p, ap, key);
    }

    int findpos(BTnode *&q, node &x) // 作用:找这个节点中第一个大于等于x的下标,PS;永远不可能为0
    {
        return lower_bound(q->key + 1, q->key + 1 + q->keynum, x) - q->key;
    }
};

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);

    // fstream p;
    // p.open("E:\\DOWNLOAD\\P1177_4.in");

    B_Tree it;
    int n;
    cin >> n;

    int a[n + 1];

    for (int i = 1; i <= n; ++i)
    {
        cin >> a[i];
        it.insert(a[i]);
    }

    sort(a + 1, a + 1 + n);
    // reverse(a+1,a+1+n);

    for (int i = 1; i <= n; ++i)
    {
        Result res = it.search(a[i]);

        if (res.tag)
            cout << res.ptr->key[res.i].key << ' ';

        // it.erase(a[i]);
    }

    cout << endl;
    system("pause");
    return 0;
}

给大伙看一下痛苦的时候的代码:

CPP 代码 · 共 590 行
#include <iostream>
#include <algorithm>
#include <cmath>
#include <fstream>

#define ll long long
#define m 35

using namespace std;

//只实现了insert函数,erase函数没有成功实现。

/*
为什么B-树文件索引(磁盘)更优?
1.对磁盘进行访问等同于指针的移动
2.磁盘具有局部访问优化,页访问
3.节点与节点之间,数据是不连续的,可能在不同的页之间
4.B-树比较矮,指针操作较少

在把磁盘里的数据加载到内存中的时候,是以页为单位来加载的,
而我们也知道,节点与节点之间的数据是不连续的,所以不同的节点,很有可能分布在不同的磁盘页中
*/

struct node{
    int key,value;
    bool operator<(const node&it)const{
        return key<it.key;
    }
    bool operator>(const node&it)const{
        return key>it.key;
    }
    bool operator==(const node&it)const{
        return key==it.key;
    }
};

struct BTNode{
    int keynum;
    BTNode*parent;
    node key[m+1]={0};//0号元素未用
    BTNode*ptr[m+1]={0};//子树元素指针,0号元素要用!
};  

struct result{//tag表示是否找到对于节点
    int i;//i表示位置
    bool tag;
    BTNode*ptr;//指向位置
};

//在调整的过程中,牢记一个问题:双亲和孩子的双向绑定(这是在踩了很多坑后总结出来的)

class B_Tree{
private:
    int Size;
    const int s=(m+1)/2;//中间分裂的位置
    const int min_key=(m-1)/2;//最小的keynum
    BTNode*root;
public:
    B_Tree()
    {
        Size=0;
        root=nullptr;
    }

    bool erase(int x)
    {
        result res=search(x);

        if(res.tag)
        {
            if(--res.ptr->key[res.i].value==0)
                erase(root,{x,0},res);
            return true;
        }
        else
            return false;
    }

    result search(int x)
    {
        node key={x,0};

        BTNode*p=root,*q=nullptr;
        bool flag=0;
        int i=0;

        while(p!=nullptr&&!flag)
        {
            //i最小是1,找的是第一个大于等于key的位置,如果没有,则应该指向p->keynum+1,
            //即尾指针才对,永远不会在查找时出现0
            i=lower_bound(p->key+1,p->key+p->keynum+1,key)-p->key;
            
            if(i<=p->keynum&&p->key[i]==key)flag=1;
            else
            {
                q=p;
                p=p->ptr[i-1];
                //要减去1,这才是应该下一步寻找的位置,书上的太不对啊
                //因为i指向的是第一个大于等于key的值,但是已知不相等,
                //所以应该减去1才不会漏过
            }
        }

        if(flag)//返回查找位置
        {
            return {i,flag,p};
        }
        else//返回插入位置,应该注意的是,如果查找失败,一定返回的是最底层的位置
        {
            return {i,flag,q};//返回的是插入节点
        }
    }
    
    bool insert(int x)//B-tree的插入总是插入到底端节点
    {
        result res=search(x);

        if(res.tag)
        {
            ++res.ptr->key[res.i].value;
            return false;
        }
        else
            return _insert(root,{x,1},res.ptr,res.i);//最底层的地方,并且i不等于0
    }

/* 计算出整棵树上记录条数的总和 */
int CountKeyNum(BTNode* tree) {
	// 空树则为 0
	if (tree == NULL) return 0;

	// 计算所有子树记录条数总和
	int childTotal = 0;
	for (int i = 0; i <= tree->keynum; i++) {
		childTotal += CountKeyNum(tree->ptr[i]);
	}

	// 子树加上自身的记录条数,即为总记录条数
	return tree->keynum + childTotal;
}

/* 打印 B 树 */
void TraverseBTree(BTNode *tree=nullptr) {//看的别人代码直接复制粘贴的调试函数

	// 动态创建一个数组,用于存放当前结点是否为最后结点的标识
    tree=this->root;
	int nowNum = 0;
	int* isLast = (int*)malloc(sizeof(int) * (CountKeyNum(tree) + 10));
	
	// 打印树形
	printf("\n");
	PrintBTree(tree, isLast, nowNum);
}

/* 递归打印树形 */
void PrintBTree(BTNode *tree, int isLast[], int nowNum) {
	// 空树直接跳过
	if (tree == nullptr) return;

	// 打印新节点先打印一下平台
	printf("-|\n");
	for (int i = 0; i <= tree->keynum; i++) {
		if (tree->ptr[i] != NULL) {
			printBranch(isLast, nowNum);
			printf("|----");
			isLast[nowNum++] = (i == tree->keynum);
			PrintBTree(tree->ptr[i], isLast, nowNum);
			nowNum--;
		}
		if (i != tree->keynum) {
			printBranch(isLast, nowNum);
			printf("|- %d\n", tree->key[i+1].key);
		}
	}
}

/* 打印树枝 */
void printBranch(int isLast[], int nowNum) {
	for (int i = 0; i < nowNum; i++) {
		if (isLast[i]) printf(" ");
		else printf("|");
		printf("      ");
	}
}

private:

    void deleteroot(BTNode*&root)//空根只有一个孩子
    {
        //  this->root=root->ptr[0];
        //  this->root->parent=nullptr;
        //  delete root;
        
        // return ;

        if(root->ptr[0]!=nullptr)
        { BTNode*child=root->ptr[0];
         root->ptr[0]=child->ptr[0];

         if(child->ptr[0]!=nullptr)
             child->ptr[0]->parent=root;
        
         for(int i=1;i<=child->keynum;++i)
             _Insert(root,child->key[i],i,child->ptr[i]);
        
        delete child;}
        else
        {
            BTNode*child=root->ptr[1];
            root->ptr[1]=child->ptr[0];

            if(child->ptr[0]!=nullptr)
                child->ptr[0]->parent=root;
            
            int t=root->keynum;
            for(int i=1;i<=child->keynum;++i)
                _Insert(root,child->key[i],i+t,child->ptr[i]);
            
            delete child;
        }
    }

    //把右节点合并到左节点中
    void Combine(BTNode*&left,BTNode*&right)
    {
        if(left==nullptr)return;

        //把右节点的信息依次插入左边的最右边
        for(int i=1;i<=right->keynum;++i)
        {
            _Insert(left,right->key[i],left->keynum+1,right->ptr[i]);
        }

        delete right;
        right=nullptr;
    }

    void Restore(BTNode*&p,int pi)
    {
        BTNode*parent,*brother;
        parent=p->parent;

        while(1)
        {
            if(pi>0&&(brother=parent->ptr[pi-1])!=nullptr&&brother->keynum>min_key)
            {
                _Insert(p,parent->key[pi],1,p->ptr[0]);//把左兄弟的父节点给它
                p->ptr[0]=brother->ptr[brother->keynum];//把最左边的孩子节点改为左兄弟最右边孩子

                if(brother->ptr[brother->keynum]!=nullptr)//修改左兄弟的最右孩子的父亲
                    brother->ptr[brother->keynum]->parent=p;
                
                /*下面的这三步其实是复杂化了,只需要移动几次指针其实就好了*/

                Remove(parent,pi);//把父亲节点删去,但是不修改指针
                Insert(parent,pi,brother->key[brother->keynum]);//把左兄弟的最右节点给它,
                Remove(brother,brother->keynum);//删除最右边的
                break;
            }
            else if(pi<parent->keynum&&(brother=parent->ptr[pi+1])!=nullptr&&brother->keynum>min_key)
            {
                //和左兄弟同理,也有些复杂化了
                _Insert(p,parent->key[pi+1],p->keynum+1,brother->ptr[0]);
                Remove(parent,pi+1);
                Insert(parent,pi+1,brother->key[1]);
                Remove(brother,1);

                for(int i=0;i<=brother->keynum;++i)
                    brother->ptr[i]=brother->ptr[i+1];
                break;
            }
            else if(pi>0&&(brother=parent->ptr[pi-1])!=nullptr)//左兄弟也比较穷即 min_key = (m-1)/2 ,这是两个节点相加不超过m,所以合并!
            {  
                _Insert(p,parent->key[pi],1,p->ptr[0]);//把父亲节点拿到当前节点上
                Remove(parent,pi);//父亲这里删除
                Combine(brother,p);//兄弟合并
                p=brother;

                for(int i=pi;i<=parent->keynum;++i)//父亲这里修改指针
                    parent->ptr[i]=parent->ptr[i+1];
                
                if(parent->keynum<min_key)//如果父亲也变穷了
                {
                    if(parent->parent==nullptr)
                    {
                        deleteroot(parent);
                        break;
                    }
                    else
                    {
                        int i=lower_bound(parent->parent->key+1,parent->parent->key+parent->parent->keynum+1,p->key[1])-parent->parent->key-1;
                        p=parent;
                        pi=i;
                        parent=p->parent;//别忘了修改父亲
                    }
                }
                else //节点个数正常,直接break;
                    break;
            }
            else if(pi<p->keynum&&(brother=parent->ptr[pi+1])!=nullptr)
            {
                _Insert(p,parent->key[pi+1],p->keynum+1,brother->ptr[0]);
                Remove(parent,pi+1);
                Combine(p,brother);

                for(int i=pi+1;i<=parent->keynum;++i)
                    parent->ptr[i]=parent->ptr[i+1];
                
                if(parent->keynum<min_key)
                {
                    if(parent->parent==nullptr)
                    {
                        deleteroot(parent);
                        break;
                    }
                    else
                    {
                        int i=lower_bound(parent->parent->key+1,parent->parent->key+parent->parent->keynum+1,p->key[p->keynum+1])-parent->parent->key-1;
                        pi=i;
                        p=parent;
                        parent=p->parent;//parent别忘了修改
                    }
                }
                else    //如果父亲节点个数正常,直接break
                    break;  
            }
            else
            {
                _Insert(p,parent->key[pi],1,p->ptr[0]);
                Remove(parent,pi);
                for(int i=pi+1;i<=parent->keynum;++i)
                    parent->ptr[i]=parent->ptr[i+1];
                
                if(parent->keynum<min_key)
                {
                    if(parent->parent==nullptr)
                    {
                        deleteroot(parent);
                        break;
                    }
                    else
                    {
                        int i=lower_bound(parent->parent->key+1,parent->parent->key+parent->parent->keynum+1,p->key[p->keynum+1])-parent->parent->key-1;
                        pi=i;
                        p=parent;
                        parent=p->parent;//parent别忘了修改
                    }
                }
                else    //如果父亲节点个数正常,直接break
                    break;  
            }
        }
        
    }

    void Insert(BTNode*&p,int i,node&x)//把x插入p的第i个位置
    {
        if(p==nullptr)return;

        for(int j=p->keynum;j>=i;--j)
            p->key[j+1]=p->key[j];
        
        p->key[i]=x;
        ++p->keynum;
    }

    void Remove(BTNode*&p,int i)//删除这个节点
    {
        /*删除记录这一部分,我省略了删除孩子指针的步骤,为什么呢?
        这里是为了方便后续的树形调整函数
        如果只删除记录的话,会导致孩子指针比原本多出一个,但好处也很明显,
        到后边有一个双亲记录替换的需求,我提供了只删除记录和只插入记录的操作
        ,可以在不影响孩子指针的情况下完成记录值的替换*/
        if(p==nullptr)return;

        for(;i<p->keynum;++i)//左移
            p->key[i]=p->key[i+1];
        
        p->keynum--;
        return;
    }

    void Successor(BTNode*&p,int &i)//获得前驱节点,并替换i和p
    {
        if(p==nullptr)return;//如果是空(比如本来就是最底层)
        if(p->ptr[i-1]==nullptr)return;

        BTNode*res=p->ptr[i-1];

        while(res->ptr[res->keynum]!=nullptr)
            res=res->ptr[res->keynum];
        
        p->key[i]=res->key[res->keynum];

        p=res;
        i=res->keynum;
    }

    void Next(BTNode*&p,int &i)
    {
        if(p==nullptr)return;
        if(p->ptr[i]==nullptr)return;

        BTNode*res=p->ptr[i];

        while(res->ptr[0]!=nullptr)
            res=res->ptr[0];
        
        p->key[i]=res->key[1];
        p=res;
        i=1;
    }

    //问题在于没有前驱怎么办
    void erase(BTNode*&root,node x,result&res)
    {
        int flag=0;

        if(res.ptr->ptr[res.i-1]!=nullptr)//判断当前节点是不是最下层非终端节点
            Successor(res.ptr,res.i);//不是,则替换
        else if(res.ptr->ptr[res.i]!=nullptr)
        {    Next(res.ptr,res.i);flag=1;}

        x=res.ptr->key[res.i];//保存要删除的那个节点,

        BTNode*parent=res.ptr->parent;

        //如果是根节点,就不再调整,因为既然删除操作放生在了根节点,说明已经没有了左右兄弟和双亲了
        //如果不是根节点,并且节点个数比较少,
        if(parent!=nullptr&&res.ptr->keynum-1<min_key)
        {
            Remove(res.ptr,res.i);//删除这个节点
            //在父节点找它的位置,因为已经被remove了,可以预测肯定是找不到这个节点的
            //这时得到最后一个小于本身的进行restore
            int pi=lower_bound(parent->key+1,parent->key+parent->keynum+1,x)-parent->key-1+flag;
            Restore(res.ptr,pi);
            /*这里的 RestoreBTree 函数,我和其他地方的不一样,参数 pi 并不是关键字在目标结点中的位置,
            而是整个目标节点在双亲节点中的位置,这样做的好处上边也提到了,可以很方便地找到左右双亲和左右兄弟
            也可以注意到 pi = Search -1; 这一句,和上边的 child[i-1] 是一样的道理,都是找到双亲中孩子的位置*/
        }
        else
        {
            for(int i=res.i;i<res.ptr->keynum;++i)
            {
                res.ptr->key[i]=res.ptr->key[i+1];
                res.ptr->ptr[i]=res.ptr->ptr[i+1];
            }
            --res.ptr->keynum;
        }
        return;
    }

    void split(BTNode*&q,const int &mid,BTNode*&ap)//节点q的mid之后全部分给ap,但是mid之前全部保留
    {
        int i,j;
        ap=new BTNode;
        ap->ptr[0]=q->ptr[mid];//节点零是这个时候会去填写的

        for(i=mid+1,j=1;i<=m;++i,++j)//优化的话,可以考虑这里改成move函数
        {
            ap->key[j]=q->key[i];
            ap->ptr[j]=q->ptr[i];
        }

        ap->keynum=m-mid;
        ap->parent=q->parent;
        //置于同一层,保证最底层在同一层次上,就这么一句话保证了在插入时所有最下端节点都在同一层上

        for(i=0;i<=m-mid;++i)
            if(ap->ptr[i]!=nullptr)
                ap->ptr[i]->parent=ap;
        
        q->keynum=mid-1;//后面的全部舍弃
    }

    void _Insert(BTNode*&q,node&x,int i,BTNode*&ap)
    {
        for(int j=q->keynum;j>=i;--j)
        {
            q->key[j+1]=q->key[j];
            q->ptr[j+1]=q->ptr[j];
        }

        q->key[i]=x;
        q->ptr[i]=ap;

        if(ap!=nullptr)
        {
            ap->parent=q;
        }

        ++q->keynum;
    }

    void newroot(BTNode*&T,BTNode*&p,node&x,BTNode*&ap)//这个函数只对根节点使用!
    {
        T=new BTNode;
        T->keynum=1;
        T->ptr[0]=p;
        T->ptr[1]=ap;
        T->key[1]=x;

        if(p!=nullptr)
            p->parent=T;
        if(ap!=nullptr)
            ap->parent=T;

        T->parent=nullptr;
    }

    bool _insert(BTNode*&T,node x,BTNode*q,int i)
    {
        bool finished=false;
        BTNode*ap=nullptr;
        //ap表示x所在的节点的指针,初始时为空,毕竟是节点
    
        while(q!=nullptr&&!finished)
        {
            _Insert(q,x,i,ap);//事实上,i不可能为0

            if(q->keynum<m)finished=1;
            else
            {
                split(q,s,ap);
                x=q->key[s];

                //q=q->parent;
                //问题出现在了这里,如果q变成了空,那么下面判断出根节点出现了分裂,
                //那应该是把q放在新的根节点的最左子树上,但是q却变成了空,那还放个屁,直接没有了
                //就是这里的问题导致的时不时会出错而且不容易发现,超你妈的费我老半天劲找不出来为什么。
                //这样insert函数就十分完善了,

                if(q->parent!=nullptr)
                {
                    q=q->parent;
                    i=lower_bound(q->key+1,q->key+q->keynum+1,x)-q->key;
                }
                else 
                    break;
            }
        }

        //如果根节点为空,或者根节点出现分裂,
        //因为finished为false,只能是根节点为空或者根节点出现了分裂
        if(!finished)newroot(T,q,x,ap);

        return true;
    }
};

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);

     fstream p;
     p.open("E:\\DOWNLOAD\\P1177_1.in");

    B_Tree it;
    int n;
    cin>>n;
    int a[n+1];

    for(int i=1;i<=n;++i)
    {
        cin>>a[i];
        it.insert(a[i]);
       // it.TraverseBTree();
    }

    
    sort(a+1,a+1+n);
    //int tot=unique(a+1,a+1+n)-a-1;

    for(int i=1;i<=n;++i)
    {
        it.TraverseBTree();
        result p=it.search(a[i]);
        
        if(p.tag)cout<<a[i]<<' ';

        it.erase(a[i]);
    }

    //it.TraverseBTree();

    cout<<endl;
    system("pause");
    return 0;
}