📜  B树中的删除操作(1)

📅  最后修改于: 2023-12-03 15:13:44.142000             🧑  作者: Mango

B树中的删除操作

B树是一种常见的自平衡树,常被用于数据库索引等场景。B树支持高效的插入和删除操作,本篇将介绍B树中的删除操作。

B树的结构简介

B树是一种多叉树,每个节点存储多个关键字和指向子节点的指针。B树节点的结构如下:

struct BTreeNode {
    int n; // 节点中关键字的数量
    int keys[MAX_KEYS]; // 节点中存储的关键字
    struct BTreeNode* children[MAX_CHILDREN]; // 指向子节点的指针
    bool leaf; // 节点是否为叶子节点
};

B树的每个节点中最多包含MAX_KEYS个关键字,最多有MAX_CHILDREN个子节点。B树中每个非根节点都至少有MAX_KEYS/2个关键字,最少有MAX_KEYS/2个子节点。B树的根节点可以是任意节点,其关键字数量不小于1。

B树的删除操作

删除B树中的关键字需要分解为两个步骤:

  1. 找到需要删除的关键字所在的节点和位置
  2. 删除关键字并保持B树的平衡
找到关键字

在B树中查找关键字和插入关键字非常类似。首先从根节点开始,遍历节点中的关键字和子节点。如果查找的关键字小于当前节点中的第一个关键字,则遍历左侧子节点;否则如果查找的关键字大于当前节点中的最后一个关键字,则遍历右侧子节点。如果查找的关键字在当前节点中,则返回该节点和关键字在节点中的位置。

如果遍历到叶子节点,但是没有找到关键字,则表示该关键字不存在于B树中。

保持平衡

删除B树中的关键字可能会导致B树不再满足平衡条件。因此需要在删除操作的过程中维护B树的平衡。

删除的操作需要分为两种情况:

  1. 如果要删除的关键字在叶子节点中,则直接删除;
  2. 如果要删除的关键字不在叶子节点中,则需要找到其后继(即在B树中大于该关键字的最小关键字),并将其替换该关键字。

对于第一种情况,如果删除关键字后节点中的关键字数量小于MAX_KEYS/2,则需要作出以下调整:

  1. 如果该节点与相邻节点中有一个节点关键字数量大于MAX_KEYS/2,则将该节点中的一个关键字借给相邻节点,同时从相邻节点中移动一个关键字到该节点中;
  2. 如果该节点与相邻节点中都只包含MAX_KEYS/2个关键字,则合并这两个节点,并将合并后的节点插入其父节点中,以保持平衡;

对于第二种情况,可以将该关键字在其后继节点中进行替换,然后在后继节点中递归删除该关键字。如果删除后节点中的关键字数量小于MAX_KEYS/2,也需要进行平衡调整。

代码实现

下面是C++实现的B树中的删除操作示例代码。

void BTree::remove(int key) {
    if (root == NULL) {
        return;
    }
    removeHelper(root, key);
    if (root->n == 0) {
        BTreeNode* tmp = root;
        if (root->leaf) {
            root = NULL;
        } else {
            root = root->children[0];
        }
        delete tmp;
    }
}

void BTree::removeHelper(BTreeNode* node, int key) {
    int idx = findIndex(node, key);

    if (idx < node->n && node->keys[idx] == key) { // Case 1: Found the key in the leaf node
        if (node->leaf) {
            removeFromLeafNode(node, idx);
        } else { // Case 2: Found the key in the non-leaf node
            removeFromNonLeafNode(node, idx);
        }
    } else {
        if (node->leaf) { // Case 3: The key isn't in the tree
            return;
        }
        bool flag = (idx == node->n);

        if (node->children[idx]->n < MIN_KEYS) {
            fill(idx);
        }

        if (flag && idx > node->n) {
            removeHelper(node->children[idx - 1], key);
        } else {
            removeHelper(node->children[idx], key);
        }
    }
}

void BTree::removeFromLeafNode(BTreeNode* node, int idx) {
    for (int i = idx + 1; i < node->n; ++i) {
        node->keys[i - 1] = node->keys[i];
    }
    node->n--;
}

void BTree::removeFromNonLeafNode(BTreeNode* node, int idx) {
    int k = node->keys[idx];
    if (node->children[idx]->n >= MIN_KEYS) {
        int pred = getPredecessor(node, idx);
        node->keys[idx] = pred;
        removeHelper(node->children[idx], pred);
    } else if (node->children[idx + 1]->n >= MIN_KEYS) {
        int succ = getSuccessor(node, idx);
        node->keys[idx] = succ;
        removeHelper(node->children[idx + 1], succ);
    } else {
        mergeNodes(node, idx);
        removeHelper(node->children[idx], k);
    }
}

int BTree::getPredecessor(BTreeNode* node, int idx) {
    BTreeNode* curr = node->children[idx];
    while (!curr->leaf) {
        curr = curr->children[curr->n];
    }
    return curr->keys[curr->n - 1];
}

int BTree::getSuccessor(BTreeNode* node, int idx) {
    BTreeNode* curr = node->children[idx + 1];
    while (!curr->leaf) {
        curr = curr->children[0];
    }
    return curr->keys[0];
}

void BTree::fill(int idx) {
    if (idx != 0 && root->children[idx - 1]->n >= MIN_KEYS) {
        borrowFromPrev(idx);
    } else if (idx != root->n && root->children[idx + 1]->n >= MIN_KEYS) {
        borrowFromNext(idx);
    } else {
        if (idx != root->n) {
            mergeNodes(root, idx);
        } else {
            mergeNodes(root, idx - 1);
        }
    }
}

void BTree::borrowFromPrev(int idx) {
    BTreeNode* child = root->children[idx];
    BTreeNode* sibling = root->children[idx - 1];

    for (int i = child->n - 1; i >= 0; --i) {
        child->keys[i + 1] = child->keys[i];
    }

    if (!child->leaf) {
        for (int i = child->n; i >= 0; --i) {
            child->children[i + 1] = child->children[i];
        }
    }

    child->keys[0] = root->keys[idx - 1];

    if (!child->leaf) {
        child->children[0] = sibling->children[sibling->n];
    }

    root->keys[idx - 1] = sibling->keys[sibling->n - 1];

    child->n++;
    sibling->n--;
}

void BTree::borrowFromNext(int idx) {
    BTreeNode* child = root->children[idx];
    BTreeNode* sibling = root->children[idx + 1];

    child->keys[child->n] = root->keys[idx];

    if (!child->leaf) {
        child->children[child->n + 1] = sibling->children[0];
    }

    root->keys[idx] = sibling->keys[0];

    for (int i = 1; i < sibling->n; ++i) {
        sibling->keys[i - 1] = sibling->keys[i];
    }

    if (!sibling->leaf) {
        for (int i = 1; i <= sibling->n; ++i) {
            sibling->children[i - 1] = sibling->children[i];
        }
    }

    child->n++;
    sibling->n--;
}

void BTree::mergeNodes(BTreeNode* node, int idx) {
    BTreeNode* child = node->children[idx];
    BTreeNode* sibling = node->children[idx + 1];

    child->keys[MIN_KEYS - 1] = node->keys[idx];

    for (int i = 0; i < sibling->n; ++i) {
        child->keys[i + MIN_KEYS] = sibling->keys[i];
    }

    if (!child->leaf) {
        for (int i = 0; i <= sibling->n; ++i) {
            child->children[i + MIN_KEYS] = sibling->children[i];
        }
    }

    for (int i = idx + 1; i < node->n; ++i) {
        node->keys[i - 1] = node->keys[i];
    }

    for (int i = idx + 2; i <= node->n; ++i) {
        node->children[i - 1] = node->children[i];
    }

    child->n = 2 * MIN_KEYS - 1;
    node->n--;
    delete sibling;
}
总结

本篇介绍了B树中的删除操作,其中涉及到的平衡调整方法有借关键字、合并节点等。相比插入操作,删除操作需要更加复杂的调整,但B树仍能够在对数时间复杂度内完成删除操作。