B树之C语言实现(包括查找、删除、插入)

我在大二上学期的数据结构实验设计中选择了B树这个题目,该B树的数据结构实现采用了C语言。趁现在寒假整理完写一篇博文记录我的学习。文末提供了项目源代码的地址。

B树的定义

一棵m阶B树(Balanced Tree of order m),或为空树,或为满足下列特性对的m叉树。

  1. 树中每个结点最多含有m棵子树。
  2. 若根结点不是叶子结点,则至少有2个子树。
  3. 除根结点之外的所有非终端结点至少有 m/2 ⌈ m / 2 ⌉ 棵子树。
  4. 每个非终端结点中包含信息:( nA0K1A1K2A2KnAn n , A 0 , K 1 , A 1 , K 2 , A 2 , … , K n , A n )。其中:
    • Ki1in K i ( 1 ≤ i ≤ n ) 为关键字,且关键字按升序排序。
    • 指针 Ai0in A i ( 0 ≤ i ≤ n ) 指向子树的根结点, Ai1 A i − 1 指向子树中所有结点的关键字均小于 Ki K i ,且大于 Ki1 K i − 1
    • 关键字的个数n必须满足: m/21nm1 ⌈ m / 2 ⌉ − 1 ≤ n ≤ m − 1
  5. 所有的叶子节点都在同一层,子叶结点不包含任何信息。

B树的存储结构

#define m 3 // B树的阶,此设为3
typedef int KeyType;

typedef struct {
  KeyType  key;
  char     data;
} Record;

typedef struct BTNode {
  int             keynum;        // 结点中关键字个数,即结点的大小
  struct BTNode  *parent;        // 指向双亲结点
  KeyType         key[m+1];      // 关键字向量,0号单元未用
  struct BTNode  *ptr[m+1];      // 子树指针向量
  Record         *recptr[m+1];   // 记录指针向量,0号单元未用
} BTNode, *BTree;                // B树结点和B树的类型

typedef struct {
  BTree    pt;      // 指向找到的结点
  int      i;       // 1..m,在结点中的关键字序号
  int      tag;     // 1:查找成功,0:查找失败
} Result;           // 在B树的查找结果类型

B树的查找

B树的查找从根结点开始,然后重复以下过程:

  1. 若给定关键字等于结点中某个关键字 Ki K i ,则查找成功。
  2. 若给定关键字比结点中的 K1 K 1 小,则进入指针 A0 A 0 指向的下一层结点继续查找。
  3. 若比该结点所有关键字大,则在其最后一个指针 An A n 指向的下一层结点继续寻找;
  4. 若查找到叶子结点,则说明给定值对应的数据记录不存在,查找失败。

实现代码

/** * 在B-树t查找关键字key,用r返回(pt, i, tag) * 若查找成功,则tag=1,指针pt所指结点中第i个关键字等于key * 否则tag=0,若要插入关键字key,应位于pt结点中第i-1和第i个关键字之间 * * @param t B-树 * @param key 待查找的关键字 * @param r B-树的查找结果类型 */
void SearchBTree(BTree t, KeyType key, Result &r) {
    int i = 0;
    int found = 0;
    BTree p = t; // 一开始指向根结点,之后指向待查结点
    BTree q = NULL; // 指向待查结点的双亲
    while (p != NULL && found == 0) {
        i = Search(p, key);
        if (i <= p->keynum && p->key[i] == key) {
            found = 1;
        } else {
            q = p;
            p = p->ptr[i - 1]; // 指针下移
        }
    }
    if (1 == found) { // 查找成功,返回key的位置p和i
        r.pt = p;
        r.i = i;
        r.tag = 1;
    } else { // 查找失败,返回key的插入位置q和i
        r.pt = q;
        r.i = i;
        r.tag = 0;
    }
}

/** * 在p->key[1 .. p->keynum]找key,并返回位序 * * @param p B-树的结点p * @param key 关键字 * @return key在p结点的位序 */
int Search(BTree p, KeyType key) {
    int i = 1;
    while (i <= p->keynum && key > p->key[i]) {
        i++;
    }
    return i;
}

B树的插入

B树的插入首先利用了B树的查找操作查找关键字k的插入位置。若该关键字存在于B树中,则不用插入直接返回,否则查找操作必定失败于某个最底层的非终端结点上,也就是要插入的位置。插入分两种情况讨论。

  1. 插入关键字后该结点的关键字数小于等于 m – 1,插入操作结束。
  2. 插入关键字后该结点的关键字数等于m,则应该进行分裂操作,分裂操作如下:
    • 生成一个新结点,从中间位置把结点(不包括中间位置的关键字)分为两部分。
    • 前半部分留在旧结点中,后半部分复制到新结点中。
    • 中间位置的关键字连同新结点的存储位置插入到父结点中,如果插入后父结点的关键字个数也超过了m-1,则继续分裂。

下图是插入的示意图(因为不想画图,而且这个好像也没有那么容易画,所以就很不要脸的拍了课本的照片)
《B树之C语言实现(包括查找、删除、插入)》

下面给出插入代码的实现

/** * 在B-树t中q结点的key[i - 1]和key[i]之间插入关键字key * 若插入后结点关键字个数等于B-树的阶,则沿双亲指针链进行结点分裂 * * @param t B-树 * @param key 待插入的关键字 * @param q 关键字插入的结点 * @param i 插入位序 */
void InsertBTree(BTree &t, KeyType key, BTree q, int i) {
    KeyType x;
    int s;
    int finished = FALSE;
    int needNewRoot = FALSE;
    BTree ap;
    if (NULL == q) {
        newRoot(t, NULL, key, NULL);
    } else {
        x = key;
        ap = NULL;
        while (FALSE == needNewRoot && FALSE == finished) {
            Insert(q, i, x, ap); // x和ap分别插入到q->key[i]和q->ptr[i]
            if (q->keynum < m) {
                finished = TRUE;
            } else {
                // 分裂q结点
                s = (m + 1) / 2; // 得到中间结点位置
                split(q, s, ap);
                x = q->key[s];
                // 在双亲位置插入关键字x
                if (q->parent != NULL) {
                    q = q->parent;
                    i = Search(q, x); // 寻找插入的位置
                } else {
                    needNewRoot = TRUE;
                }
            }
        }
        if (TRUE == needNewRoot) {
            newRoot(t, q, x, ap);
        }
    }
}

/** * 将q结点分裂成两个结点,前一半保留在原结点,后一半移入ap所指新结点 * * @param q B-树结点 * @param s 中间位序 * @param ap 新结点,用来存放原结点的后一半关键字 */
void split(BTree &q, int s, BTree &ap) {
    int i, j;
    int n = q->keynum; // 关键字数量
    ap = (BTree)malloc(sizeof(BTNode));
    ap->ptr[0] = q->ptr[s];
    for (i = s + 1, j = 1; i <= n; i++, j++) {
        ap->key[j] = q->key[i];
        ap->ptr[j] = q->ptr[i];
    }
    ap->keynum = n - s;
    ap->parent = q->parent;
    for (i = 0; i <= n - s; i++) {
        // 修改新结点的子结点的parent域
        if (ap->ptr[i] != NULL) {
            ap->ptr[i]->parent = ap;
        }
    }
    q->keynum = s - 1; // 修改q结点的关键字数量
}

/** * 生成新的根结点 * * @param t B-树 * @param p B-树结点 * @param key 关键字 * @param ap B-树结点 */
void newRoot(BTree &t, BTree p, KeyType key, BTree ap) {
    t = (BTree)malloc(sizeof(BTNode));
    t->keynum = 1;
    t->ptr[0] = p;
    t->ptr[1] = ap;
    t->key[1] = key;
    if (p != NULL) {
        p->parent = t;
    }
    if (ap != NULL) {
        ap->parent = t;
    }
    t->parent = NULL;
}

/** * 关键字key和新结点指针ap分别插入到q->key[i]和q->ptr[i] * * @param q 插入目标结点 * @param i 插入位序 * @param key 待插入的关键字 * @param ap 新结点指针 */
void Insert(BTree &q, int i, KeyType key, BTree ap) {
    int j;
    int n = q->keynum;
    for (j = n; j >= i; j--) {
        // 后移
        q->key[j + 1] = q->key[j];
        q->ptr[j + 1] = q->ptr[j];
    }
    q->key[i] = key;
    q->ptr[i] = ap;
    if (ap != NULL) {
        ap->parent = q;
    }
    q->keynum++;
}

B树的删除

B树的删除关键字是B树所有操作种最麻烦的,下面一一讲解。

该结点为最下层非终端结点

  1. 如果被删关键字所在结点的原关键字个数 nm/2 n ≥ ⌈ m / 2 ⌉ ,则删去该关键字后结点仍满足B树的定义,如下图

《B树之C语言实现(包括查找、删除、插入)》

  1. 如果被删关键字所在结点的关键字个数n等于 m/21 ⌈ m / 2 ⌉ − 1 ,则删除该关键字后该结点将不满足B树的定义,需要调整:如果其左右兄弟结点中有富余的关键字,即与该结点相邻的右(或左)兄弟结点中的关键字数目大于 m/21 ⌈ m / 2 ⌉ − 1 ,则可将右(或左)兄弟结点中最小(大)关键字上移至双亲结点。而将双亲结点中小(大)于该上移关键字的关键字下移至被删关键字所在结点中。如下图

《B树之C语言实现(包括查找、删除、插入)》

  1. 如果左右兄弟结点都没有多余的关键字,则需要把要删除关键字的结点与其左(或右)兄弟结点以及双亲结点中分割两者的关键字合并成一个结点,即在删除关键字后,该结点中剩余的关键字和指针,加上双亲结点中的关键字 Ki K i 一起,合并到 Ai1 A i − 1 或( Ai A i )结点,即删除该关键字结点的左(右)兄弟结点。如果导致双亲结点中关键字个数小于 m/21 ⌈ m / 2 ⌉ − 1 ,则对此双亲结点做同样处理。如果知道根结点也做了合并,则整棵树减少一层。如下图

《B树之C语言实现(包括查找、删除、插入)》

该结点不是最下层非终端结点

假设被删关键字为该结点中第i个关键字 Ki K i ,则可从指针 Ai A i 所指的子树中找出位于最下层非终端结点的最小关键字替代 Ki K i ,并将其删除,即可转换为上面的情况进行操作。

代码实现如下

/** * 删除B-树上p结点的第i个关键字 * * @param t B-树 * @param p 目标关键字所在结点 * @param i 关键字位序 */
void DeleteBTree(BTree &t, BTree &p, int i) {
    if (p->ptr[i] != NULL) {
        // 不是最下层非终端结点
        Successor(p, i); // 找到后继最下层非终端结点的最小关键字代替它
        DeleteBTree(t, p, 1); // 删除最下层非终端结点中的最小关键字
    } else {
        Remove(p, i); // 从结点p中删除key[i]
        if (p->keynum < (m - 1) / 2) {
            Restore(t, p); // 调整B树
        }
    }
}

/** * 在Ai子树中找出最下层非终端结点的最小关键字代替Ki * * @param p B-树结点 * @param i 关键字位序 */
void Successor(BTree &p, int i) {
    BTree leaf = p;
    if (NULL == p) {
        return;
    }
    leaf = leaf->ptr[i]; // 指向子树
    while (NULL != leaf->ptr[0]) {
        // 找到最下层非终端结点
        leaf = leaf->ptr[0];
    }
    p->key[i] = leaf->key[1];
    p = leaf;
}

/** * 从结点p移除关键字key[i] * * @param p B-树结点 * @param i 关键字位序 */
void Remove(BTree &p, int i) {
    int k;
    // 指针与key都向左移
    for (k = i; k < p->keynum; k++) {
        p->key[k] = p->key[k + 1];
        p->ptr[k] = p->ptr[k + 1];
    }
    p->keynum--;
}

/** * 调整B-树 * * @param t B-树 * @param p B-树结点 */
void Restore(BTree &t, BTree &p) {
    BTree parent, leftBrother, rightBrother; // 被删结点的父结点、左右兄弟
    parent = p->parent;
    if (parent != NULL) { // 父结点不为空
        // 寻找左右兄弟
        int i;
        for (i = 0; i <= parent->keynum; i++) {
            if (parent->ptr[i] == p) {
                break;
            }
        }
        if (i > 0) {
            leftBrother = parent->ptr[i - 1];
        } else {
            leftBrother = NULL;
        }
        if (i < parent->keynum) {
            rightBrother = parent->ptr[i + 1];
        } else {
            rightBrother = NULL;
        }

        // 左兄弟或右兄弟有富余关键字
        if ((leftBrother != NULL && leftBrother->keynum >= (m + 1) / 2) ||
            (rightBrother != NULL && rightBrother->keynum >= (m + 1) / 2)) {
            BorrowFromBrother(p, leftBrother, rightBrother, parent, i);
        } else {
            // 左右兄弟都没富余关键字,需要合并
            if (leftBrother != NULL) {
                MegerWithLeftBrother(leftBrother, parent, p, t, i); // 与左兄弟合并
            } else if (rightBrother != NULL) {
                MegerWithRightBrother(rightBrother, parent, p, t, i);
            } else  {
                //当左右子树不存在时改变根结点
                for (int j = 0; j <= p->keynum + 1; j++) {
                    if (p->ptr[j] != NULL) {
                        t = p->ptr[j];
                        break;
                    }
                }
                t->parent = NULL;
            }
        }
    } else {
        //根节点,去掉根节点,使树减一层
        BTree a;
        for (int j = 0; j <= p->keynum + 1; j++) {
            if (p->ptr[j] != NULL) {
                a = p;
                p = p->ptr[j];
                a->ptr[j] = NULL;
                free(a);
                break;
            }
        }
        t = p;
        t->parent = NULL;
    }
}

/** * 向兄弟借关键字 * * @param p B-树结点 * @param leftBrother p结点的左兄弟结点 * @param rightBrother p结点的右兄弟结点 * @param parent p结点的父亲结点 * @param i 位序 */
void BorrowFromBrother(BTree &p, BTree &leftBrother, BTree &rightBrother, BTree &parent, int &i) {
    // 左兄弟有富余关键字,向左兄弟借
    if (leftBrother != NULL && leftBrother->keynum >= (m + 1) / 2) {
        for (int j = p->keynum + 1; j > 0; j--) {
            // 关键字与指针后移,腾出第一个位置
            if (j > 1) {
                p->key[j] = p->key[j - 1];
            }
            p->ptr[j] = p->ptr[j - 1];
        }
        p->ptr[0] = leftBrother->ptr[leftBrother->keynum];
        if (p->ptr[0] != NULL) {
            p->ptr[0]->parent = p;
        }
        leftBrother->ptr[leftBrother->keynum] = NULL;
        p->key[1] = parent->key[i]; // 被删结点存父结点关键字
        parent->key[i] = leftBrother->key[leftBrother->keynum]; // 父结点的key变为被删结点左兄弟的最大关键字
        leftBrother->keynum--;
        p->keynum++;
    } else if (rightBrother != NULL && rightBrother->keynum >= (m + 1) / 2) { // 右兄弟有富余关键字
        p->key[p->keynum + 1] = parent->key[i + 1];
        p->ptr[p->keynum + 1] = rightBrother->ptr[0];    // 子树指针指向右兄弟最左边的子树指针
        if (p->ptr[p->keynum + 1] != NULL) {
            p->ptr[p->keynum + 1]->parent = p;
        }
        p->keynum++;
        parent->key[i + 1] = rightBrother->key[1];        // 父结点从右兄弟借关键字
        for (int j = 0; j < rightBrother->keynum; j++) {
            if (j > 0) {
                rightBrother->key[j] = rightBrother->key[j + 1];
            }
            rightBrother->ptr[j] = rightBrother->ptr[j + 1];
        }
        rightBrother->ptr[rightBrother->keynum] = NULL;
        rightBrother->keynum--;
    }
}

/** * 与左兄弟合并 * * @param leftBrother p结点的左兄弟结点 * @param parent p结点的父亲结点 * @param p B-树结点 * @param t B-树 * @param i 位序 */
void MegerWithLeftBrother(BTree &leftBrother, BTree &parent, BTree &p, BTree &t, int &i) {
    // 与左兄弟合并
    leftBrother->key[leftBrother->keynum + 1] = parent->key[i];    // 从父结点拿下分割本节点与左兄弟的关键字
    leftBrother->ptr[leftBrother->keynum + 1] = p->ptr[0];
    if (leftBrother->ptr[leftBrother->keynum + 1] != NULL) {
        leftBrother->ptr[leftBrother->keynum + 1]->parent = leftBrother;    // 给左兄弟的结点,当此结点存在时需要把其父亲指向指向左结点
    }
    leftBrother->keynum++; //左兄弟关键数加1
    for (int j = 1; j <= p->keynum; j++) {
        // 把本结点的关键字和子树指针赋给左兄弟
        leftBrother->key[leftBrother->keynum + j] = p->key[j];
        leftBrother->ptr[leftBrother->keynum + j] = p->ptr[j];
        if (leftBrother->ptr[leftBrother->keynum + j] != NULL) {
            leftBrother->ptr[leftBrother->keynum + j]->parent = leftBrother;
        }
    }
    leftBrother->keynum += p->keynum;
    parent->ptr[i] = NULL;
    free(p);    // 释放p结点
    for (int j = i;j < parent->keynum; j++) {
        // 左移
        parent->key[j] = parent->key[j + 1];
        parent->ptr[j] = parent->ptr[j + 1];
    }
    parent->ptr[parent->keynum] = NULL;
    parent->keynum--;        // 父结点关键字个数减1
    if (t == parent) {
        // 如果此时父结点为根,则当父结点没有关键字时才调整
        if (0 == parent->keynum) {
            for (int j = 0;j <= parent->keynum + 1; j++) {
                if (parent->ptr[j] != NULL) {
                    t = parent->ptr[j];
                    break;
                }
                t->parent = NULL;
            }
        }
    } else {
        // 如果父结点不为根,则需要判断是否需要重新调整
        if (parent->keynum < (m - 1) / 2) {
            Restore(t, parent);
        }
    }
}
/** * 与右兄弟合并 * * @param rightBrother p结点的右兄弟结点 * @param parent p结点的父亲结点 * @param p B-树结点 * @param t B-树 * @param i 位序 */
void MegerWithRightBrother(BTree &rightBrother, BTree &parent, BTree &p, BTree &t, int &i) {
    // 与右兄弟合并
    for (int j = (rightBrother->keynum); j > 0; j--) {
        if (j > 0) {
            rightBrother->key[j + 1 + p->keynum] = rightBrother->key[j];
        }
        rightBrother->ptr[j + 1 + p->keynum] = rightBrother->ptr[j];
    }
    rightBrother->key[p->keynum + 1] = parent->key[i + 1];    // 把父结点的分割两个本兄弟和右兄弟的关键字拿下来使用
    for (int j = 0; j <= p->keynum; j++) {
        // 把本结点的关键字及子树指针移动右兄弟中去
        if (j > 0) {
            rightBrother->key[j] = p->key[j];
        }
        rightBrother->ptr[j] = p->ptr[j];
        if (rightBrother->ptr[j] != NULL) {
            rightBrother->ptr[j]->parent = rightBrother;    // 给右兄弟的结点需要把其父结点指向右兄弟
        }
    }
    rightBrother->keynum += (p->keynum + 1);
    parent->ptr[i] = NULL;
    free(p); // 释放p结点
    for (int j = i;j < parent->keynum;j++) {
        if (j > i) {
            parent->key[j] = parent->key[j + 1];
        }
        parent->ptr[j] = parent->ptr[j + 1];
    }
    if (1 == parent->keynum) {
        // 如果父结点在关键字减少之前只有一个结点,那么需要把父结点的右孩子赋值给左孩子
        parent->ptr[0] = parent->ptr[1];
    }
    parent->ptr[parent->keynum] = NULL;
    parent->keynum--;                    // 父结点关键字数减1
    if (t == parent) {
        //如果此时父结点为根,则当父结点没有关键字时才调整
        if (0 == parent->keynum) {
            for (int j = 0; j <= parent->keynum + 1; j++) {
                if (parent->ptr[j] != NULL) {
                    t = parent->ptr[j];
                    break;
                }
            }
            t->parent = NULL;
        }
    } else {
        //如果父结点不为根,则需要判断是否需要重新调整
        if (parent->keynum < (m - 1) / 2) {
            Restore(t, parent);
        }
    }
}

如需要完整的实现代码以及测试函数,请到https://github.com/zhengjunming/ds/tree/master/btree 中clone。

    原文作者:B树
    原文地址: https://blog.csdn.net/zheng_junming/article/details/79165049
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞