一:背景
B – 树(或者 B 树,英语:B-Tree)是一种自平衡的树,能够保持数据有序。这种数据结构能够让查找数据、顺序访问、插入数据及删除的动作,都在对数时间 O ( l o g n )” role=”presentation”> O(logn) 内完成。与其它一般化的二叉查找树不同,它可以拥有多余 2 个子结点。
B – 树为系统大块数据的读写操作做了优化,减少定位记录时所经历的中间过程,从而加快存取速度。这种数据结构可以用来描述外部存储,因此常被应用在数据库和文件系统的实现上。
一棵 B – 树具有如下性质:
- 所有的叶子结点在同一层;
- 每棵 B – 树有一个 Minimum Degree,称其为 t(t 的值取决于磁盘块的大小);
- 除了根结点,其余每个结点至少包含 t-1 个 keys,根结点可以只包含 1 个 key;
- 每个结点(包括根结点)最多包含 2t-1 个 keys;
- 一个结点的孩子指针数等于这个结点的 keys 数 + 1;
- 每个结点的 keys 都按升序排列;
- 对于每个 key,其左边孩子结点的所有 keys 都小于它,右边孩子结点的所有 keys 都大于它。
如下图,是一棵 Minimum Degree 为 3(即 t=3)的 B – 树:
二:算法过程与分析
首先大致看下程序的轮廓:
struct Node
{
bool leaf; // 是否是叶子结点
int n; // keys 的数目
int * keys; // 保存 keys
Node ** siblings; // 保存孩子指针
Node(int t, bool leaf)
{
this->leaf = leaf;
this->n = 0;
this->keys = new int[2 * t - 1];
this->siblings = new Node*[2 * t]{ 0 };
}
};
class BTree
{
private:
int t; // Minimum Degree
Node * root;
private:
void destroy(Node * node);
void split_child(Node * parent, int i, Node * child);
void insert_non_full(Node * node, int key);
bool find_real(Node * node, int key);
void merge(Node * node, int i);
void erase_real(Node * node, int key);
public:
BTree(int t);
~BTree();
void insert(int key);
bool find(int key);
void erase(int key);
void print();
};
注意,以下讲解,默认 Minimum Degree = 3。
2.1 插入操作
我们举个例子来具体说明插入是如何完成的。
(1)
对于一棵空树,直接插入即可。
(2)
再插入 20,30,40 和 50,此时根结点 keys 数正好达到上限 5(由 2t – 1 = 6 – 1 = 5 得)。
(3)
再插入 60 的话,根结点的 keys 就会超过上限,那么我们的做法就是先把它一分为二,具体做法是:把中间的那个 key 移到上面,再申请一个新的结点,把最后 t-1 个 keys 移到新结点里。参见图四的左部分;
接着再插入 60。参见图四的右部分。
(4)
再插入 70 和 80,此时根结点最右边孩子结点的 keys 数已达到上限。
(5)
再插入 90 的话,按照之前一分为二的思想,把中间的 key,即 60 移到上面,再申请一个新结点来存储最后 t-1 个 keys,最后插入 90。
插入操作的代码如下:
/* 一分为二 */
void BTree::split_child(Node * parent, int i, Node * child)
{
Node * z = new Node(t, child->leaf);
z->n = t - 1;
for (int j = 0; j < t - 1; j++)
z->keys[j] = child->keys[j + t];
if (!child->leaf)
{
for (int j = 0; j < t; j++)
z->siblings[j] = child->siblings[j + t];
}
child->n = t - 1;
for (int j = parent->n; j >= i + 1; j--)
parent->siblings[j + 1] = parent->siblings[j];
parent->siblings[i + 1] = z;
for (int j = parent->n - 1; j >= i; j--)
parent->keys[j + 1] = parent->keys[j];
parent->keys[i] = child->keys[t - 1];
parent->n++;
}
void BTree::insert_non_full(Node * node, int key)
{
int i = node->n - 1;
if (node->leaf)
{
while (i >= 0 && node->keys[i] > key)
{
node->keys[i + 1] = node->keys[i];
i--;
}
node->keys[i + 1] = key;
node->n++;
}
else
{
while (i >= 0 && node->keys[i] > key)
i--;
if (node->siblings[i + 1]->n == 2 * t - 1)
{
split_child(node, i + 1, node->siblings[i + 1]);
if (node->keys[i + 1] < key)
i++;
}
insert_non_full(node->siblings[i + 1], key);
}
}
void BTree::insert(int key)
{
if (!root)
{
root = new Node(t, true);
root->keys[0] = key;
root->n = 1;
}
else
{
if (root->n == 2 * t - 1)
{
Node * s = new Node(t, false);
s->siblings[0] = root;
split_child(s, 0, root);
int i = 0;
if (s->keys[0] < key)
i++;
insert_non_full(s->siblings[i], key);
root = s;
}
else
insert_non_full(root, key);
}
}
2.2 查找操作
查找很简单,直接看代码即可。
bool BTree::find_real(Node * node, int key)
{
int i = 0;
while (i < node->n && node->keys[i] < key)
i++;
if (node->keys[i] == key)
return true;
if (node->leaf)
return false;
return find_real(node->siblings[i], key);
}
bool BTree::find(int key)
{
if (root)
return find_real(root, key);
return false;
}
2.3 删除操作
假设我们要删除的值是 key,在当前这棵 B – 树中它存在且位于结点 t 里。
- t 是叶子结点。
- 这种情况很简单,直接删除即可。
- t 不是叶子结点。分三种情况,我们约定 key 的左边的孩子结点为 left,右边的孩子结点为 right,
- left 的 keys 数大于 t-1,那就在 left 中找到 key 的 “前驱”,用 “前驱” 替换 key,转而继续删除 “前驱”;
- right 的 keys 数大于 t-1,那就在 right 中找到 key 的 “后继”,用 “后继” 替换 key,转而继续删除 “后继”;
- left 和 right 的 keys 数都等于 t-1,那就把 left,key 和 right 合并为一个结点,继续在这个结点中删除 key。
但是上述做法存在一个问题,考虑:若 t 是叶子结点(不是根结点),且 keys 数恰好等于 t-1,那么我们直接删除 key 的话,势必会使这棵 B – 树违反 ” 性质 3:除了根结点,其余每个结点至少包含 t-1 个 keys,根结点可以只包含 1 个 key “。那如何解决这个问题呢?
删除伴随着查找,从根结点开始,向右或向下移动指针查找 key 的位置所在。我们可以提前判断要下降的那个结点(不妨设为 cur),如果 cur 的 keys 数等于 t-1,我们就给它填充,使其 keys 数大于 t-1。具体做法是:如果 cur 的左右兄弟结点中存在 keys 数大于 t-1 的,就从那里 “拿一个” key;如果都等于 t-1,就把左右兄弟结点和夹在兄弟中间的那个 key 进行合并。
好,下面以几幅图来具体说明上述的步骤和做法:
(1)图 1 中,删除 2。
从根节点 a 开始,要下降的结点为 b,发现 b 的 keys 数为 t-1,而它的右兄弟结点 c 的 keys 数也只有 t-1,进行合并。合并后如下图 7。
接着在结点 b 中找,要下降的结点为 d,发现 d 的 keys 为 t-1,而它的右兄弟 e 的 keys 是大于 t-1 的,那我们就从 e 里拿一个 key:把 3 放进 d 中,4 放进 b 中。此时 b 结点 keys 数不变,d 变成 3 个,e 少了一个,但依旧满足大于等于 t-1。删除 2 后,如下图 8。
如果要下降的那个结点的 keys 数等于 t-1,我们就对其进行填充(或从兄弟里拿一个,或直接合并),使其 keys 数大于 t-1。这样的做法很有用处,且也必须这么做。
为什么 “必须” 呢?
请看图 1,假设我们不在下降的过程中进行填充操作了,现在如果要删除结点 g 中的 13,问题出现了,g 的 keys 数就会小于 t-1,此时违反 B – 树的性质 3。那怎么解决这个问题呢?从兄弟结点 h 中拿一个?不可以,h 的 keys 数也是 t-1。合并?所谓的合并就是把 g,15 和 h 合并为一个结点,可是 c 的 keys 数也是 t-1,也行不通。因此,对要下降的那个 keys 数等于 t-1 的结点进行填充是必要的。
那又如何 “很有用处” 了呢?
观察 “删除 2” 后,图 1 到图 8 的变化,整棵 B – 树的高度降低,高度降低意味着查找效率的提高。分析发现,它之所以会降低,是因为进行了合并操作。读者可以再仔细分析下会发现,这是降低 B – 树高度的唯一方式:根结点一个 key,左右两个孩子结点的 keys 数都是 t-1,合并成一个具有 2t-1 个 keys 的结点。
(2)图 8 中,删除 4。
4 存在于结点 b 中,且 b 不是叶子结点。观察 4 的左右孩子结点(d 和 e)的 keys 数,发现右孩子结点 e 的 keys 数是大于 t-1 的,因此就在 e 中找到 4 的后继(就是 5),用 5 替换 4,接着再在 e 中删除 5。删除后,如下图 9。
(3)图 9 中,删除 5。
5 存在于结点 b 中,且 b 不是叶子结点。观察 5 的左右孩子结点(d 和 e)的 keys 数,都是恰好等于 t-1,那么进行合并操作:把结点 d,5 和结点 e 合并为一个结点,再在这个结点中删除 5 即可。删除后,如下图 10。
最后看下删除操作的代码实现:
void BTree::merge(Node * node, int i)
{
Node * cur = node->siblings[i];
Node * next = node->siblings[i + 1];
cur->keys[t - 1] = node->keys[i];
for (int j = 0; j < next->n; j++)
cur->keys[j + t] = next->keys[j];
if (!cur->leaf)
{
for (int j = 0; j <= next->n; j++)
cur->siblings[j + t] = next->siblings[j];
}
for (int j = i + 1; j < node->n; j++)
node->keys[j - 1] = node->keys[j];
for (int j = i + 2; j <= node->n; j++)
node->siblings[j - 1] = node->siblings[j];
cur->n += next->n + 1;
node->n--;
delete next;
}
void BTree::erase_real(Node * node, int key)
{
int i = 0;
while (i < node->n && node->keys[i] < key) // 找到第一个大于等于 key 的位置
i++;
if (i < node->n && node->keys[i] == key) // 如果在当前结点找到 key
{
if (node->leaf) // 当前结点是叶子的话,直接删除
{
for (int j = i + 1; j < node->n; j++)
node->keys[j - 1] = node->keys[j];
node->n--;
}
else // 当前结点不是叶子的话,分为三种情况
{
if (node->siblings[i]->n > t - 1) // 如果 key 的左边那个孩子结点 keys 数大于 t-1 ,就用 key 的前驱替换 key,问题转化为删除前驱
{
Node * left = node->siblings[i];
while (!left->leaf)
left = left->siblings[left->n - 1];
int precursor_key = left->keys[left->n - 1];
node->keys[i] = precursor_key;
erase_real(node->siblings[i], precursor_key);
}
else if (node->siblings[i + 1]->n > t - 1) // 如果 key 的右边那个孩子结点 keys 数大于 t-1 ,就用 key 的后继替换 key,问题转化为删除后继
{
Node * right = node->siblings[i + 1];
while (!right->leaf)
right = right->siblings[0];
int successor_key = right->keys[0];
node->keys[i] = successor_key;
erase_real(node->siblings[i + 1], successor_key);
}
else // 如果左右两个孩子结点 keys 都等于 t-1,那就进行合并操作,再删除
{
merge(node, i);
erase_real(node->siblings[i], key);
}
}
}
else // 如果未在当前结点找到 key
{
if (node->leaf) // 是叶子的话,说明根本不存在该 key
return;
bool flag = (i == node->n) ? true : false;
if (node->siblings[i]->n == t - 1) // 每次下降的时候,都要检查下降的那个结点的 keys 数是不是等于下限 t-1,如果是就要做填充处理,分为三种情况
{
if (i != 0 && node->siblings[i - 1]->n > t - 1) // 左兄弟结点 keys 数大于 t-1
{
Node * cur = node->siblings[i];
Node * pre = node->siblings[i - 1];
for (int j = cur->n - 1; j >= 0; j--)
cur->keys[j + 1] = cur->keys[j];
if (!cur->leaf)
{
for (int j = cur->n; j >= 0; j--)
cur->siblings[j + 1] = cur->siblings[j];
cur->siblings[0] = pre->siblings[pre->n];
}
cur->keys[0] = node->keys[i - 1];
node->keys[i - 1] = pre->keys[pre->n - 1];
cur->n++;
pre->n--;
}
else if (i != node->n && node->siblings[i + 1]->n > t - 1) // 右兄弟结点 keys 数大于 t-1
{
Node * cur = node->siblings[i];
Node * next = node->siblings[i + 1];
for (int j = 1; j < next->n; j++)
next->keys[j - 1] = next->keys[j];
if (!next->leaf)
{
for (int j = 1; j < next->n; j++)
next->siblings[j - 1] = next->siblings[j];
cur->siblings[cur->n + 1] = next->siblings[0];
}
cur->keys[cur->n] = node->keys[i];
node->keys[i] = next->keys[0];
cur->n++;
next->n--;
}
else // 如果左右兄弟结点 keys 数都等于 t-1,则对它们进行合并
{
if (i != node->n)
merge(node, i);
else
merge(node, i - 1);
}
}
if (flag && i > node->n)
erase_real(node->siblings[i - 1], key);
else
erase_real(node->siblings[i], key);
}
}
void BTree::erase(int key)
{
if (!root)
return;
erase_real(root, key);
if (root->n == 0)
{
Node * t = root;
if (root->leaf)
root = nullptr;
else
root = root->siblings[0];
delete t;
}
}
2.4 打印操作
以下实现为层次遍历的代码。
void BTree::print()
{
if (root)
{
queue<Node*> Q;
Q.push(root);
while (!Q.empty())
{
Node * t = Q.front();
Q.pop();
for (int i = 0; i < t->n; i++)
cout << t->keys[i] << " ";
cout << endl;
if (!t->leaf)
{
for (int i = 0; i < t->n + 1; i++)
Q.push(t->siblings[i]);
}
}
cout << endl;
}
}
三:完整代码
/**
*
* author : 刘毅(Limer)
* date : 2017-09-28
* mode : C++
*/
#include <iostream>
#include <queue>
using namespace std;
struct Node
{
bool leaf; // 是否是叶子结点
int n; // keys 的数目
int * keys; // 保存 keys
Node ** siblings; // 保存孩子指针
Node(int t, bool leaf)
{
this->leaf = leaf;
this->n = 0;
this->keys = new int[2 * t - 1];
this->siblings = new Node*[2 * t]{ 0 };
}
};
class BTree
{
private:
int t; // Minimum Degree
Node * root;
private:
void destroy(Node * node);
void split_child(Node * parent, int i, Node * child);
void insert_non_full(Node * node, int key);
bool find_real(Node * node, int key);
void merge(Node * node, int i);
void erase_real(Node * node, int key);
public:
BTree(int t);
~BTree();
void insert(int key);
bool find(int key);
void erase(int key);
void print();
};
int main()
{
BTree btree(3);
// test "insert"
btree.insert(1);
btree.insert(3);
btree.insert(7);
btree.insert(10);
btree.insert(11);
btree.insert(13);
btree.insert(14);
btree.insert(15);
btree.insert(18);
btree.insert(16);
btree.insert(19);
btree.insert(24);
btree.insert(25);
btree.insert(26);
btree.insert(21);
btree.insert(4);
btree.insert(5);
btree.insert(20);
btree.insert(22);
btree.insert(2);
btree.insert(17);
btree.insert(12);
btree.insert(6);
btree.print();
// test "find"
cout << ((btree.find(0) == true) ? 1 : -1) << endl;
cout << ((btree.find(1) == true) ? 1 : -1) << endl;
cout << ((btree.find(20) == true) ? 1 : -1) << endl << endl;
// test "erase"
btree.erase(6);
btree.print();
btree.erase(21);
btree.print();
btree.erase(20);
btree.print();
btree.erase(19);
btree.print();
btree.erase(22);
btree.print();
return 0;
}
void BTree::destroy(Node * node)
{
if (node->siblings[0])
{
for (int i = 0; i <= node->n; i++)
destroy(node->siblings[i]);
}
delete node;
}
/* 一分为二 */
void BTree::split_child(Node * parent, int i, Node * child)
{
Node * z = new Node(t, child->leaf);
z->n = t - 1;
for (int j = 0; j < t - 1; j++)
z->keys[j] = child->keys[j + t];
if (!child->leaf)
{
for (int j = 0; j < t; j++)
z->siblings[j] = child->siblings[j + t];
}
child->n = t - 1;
for (int j = parent->n; j >= i + 1; j--)
parent->siblings[j + 1] = parent->siblings[j];
parent->siblings[i + 1] = z;
for (int j = parent->n - 1; j >= i; j--)
parent->keys[j + 1] = parent->keys[j];
parent->keys[i] = child->keys[t - 1];
parent->n++;
}
void BTree::insert_non_full(Node * node, int key)
{
int i = node->n - 1;
if (node->leaf)
{
while (i >= 0 && node->keys[i] > key)
{
node->keys[i + 1] = node->keys[i];
i--;
}
node->keys[i + 1] = key;
node->n++;
}
else
{
while (i >= 0 && node->keys[i] > key)
i--;
if (node->siblings[i + 1]->n == 2 * t - 1)
{
split_child(node, i + 1, node->siblings[i + 1]);
if (node->keys[i + 1] < key)
i++;
}
insert_non_full(node->siblings[i + 1], key);
}
}
bool BTree::find_real(Node * node, int key)
{
int i = 0;
while (i < node->n && node->keys[i] < key)
i++;
if (node->keys[i] == key)
return true;
if (node->leaf)
return false;
return find_real(node->siblings[i], key);
}
void BTree::merge(Node * node, int i)
{
Node * cur = node->siblings[i];
Node * next = node->siblings[i + 1];
cur->keys[t - 1] = node->keys[i];
for (int j = 0; j < next->n; j++)
cur->keys[j + t] = next->keys[j];
if (!cur->leaf)
{
for (int j = 0; j <= next->n; j++)
cur->siblings[j + t] = next->siblings[j];
}
for (int j = i + 1; j < node->n; j++)
node->keys[j - 1] = node->keys[j];
for (int j = i + 2; j <= node->n; j++)
node->siblings[j - 1] = node->siblings[j];
cur->n += next->n + 1;
node->n--;
delete next;
}
void BTree::erase_real(Node * node, int key)
{
int i = 0;
while (i < node->n && node->keys[i] < key) // 找到第一个大于等于 key 的位置
i++;
if (i < node->n && node->keys[i] == key) // 如果在当前结点找到 key
{
if (node->leaf) // 当前结点是叶子的话,直接删除
{
for (int j = i + 1; j < node->n; j++)
node->keys[j - 1] = node->keys[j];
node->n--;
}
else // 当前结点不是叶子的话,分为三种情况
{
if (node->siblings[i]->n > t - 1) // 如果 key 的左边那个孩子结点 keys 数大于 t-1 ,就用 key 的前驱替换 key,问题转化为删除前驱
{
Node * left = node->siblings[i];
while (!left->leaf)
left = left->siblings[left->n - 1];
int precursor_key = left->keys[left->n - 1];
node->keys[i] = precursor_key;
erase_real(node->siblings[i], precursor_key);
}
else if (node->siblings[i + 1]->n > t - 1) // 如果 key 的右边那个孩子结点 keys 数大于 t-1 ,就用 key 的后继替换 key,问题转化为删除后继
{
Node * right = node->siblings[i + 1];
while (!right->leaf)
right = right->siblings[0];
int successor_key = right->keys[0];
node->keys[i] = successor_key;
erase_real(node->siblings[i + 1], successor_key);
}
else // 如果左右两个孩子结点 keys 都等于 t-1,那就进行合并操作,再删除
{
merge(node, i);
erase_real(node->siblings[i], key);
}
}
}
else // 如果未在当前结点找到 key
{
if (node->leaf) // 是叶子的话,说明根本不存在该 key
return;
bool flag = (i == node->n) ? true : false;
if (node->siblings[i]->n == t - 1) // 每次下降的时候,都要检查下降的那个结点的 keys 数是不是等于下限 t-1,如果是就要做填充处理,分为三种情况
{
if (i != 0 && node->siblings[i - 1]->n > t - 1) // 左兄弟结点 keys 数大于 t-1
{
Node * cur = node->siblings[i];
Node * pre = node->siblings[i - 1];
for (int j = cur->n - 1; j >= 0; j--)
cur->keys[j + 1] = cur->keys[j];
if (!cur->leaf)
{
for (int j = cur->n; j >= 0; j--)
cur->siblings[j + 1] = cur->siblings[j];
cur->siblings[0] = pre->siblings[pre->n];
}
cur->keys[0] = node->keys[i - 1];
node->keys[i - 1] = pre->keys[pre->n - 1];
cur->n++;
pre->n--;
}
else if (i != node->n && node->siblings[i + 1]->n > t - 1) // 右兄弟结点 keys 数大于 t-1
{
Node * cur = node->siblings[i];
Node * next = node->siblings[i + 1];
for (int j = 1; j < next->n; j++)
next->keys[j - 1] = next->keys[j];
if (!next->leaf)
{
for (int j = 1; j < next->n; j++)
next->siblings[j - 1] = next->siblings[j];
cur->siblings[cur->n + 1] = next->siblings[0];
}
cur->keys[cur->n] = node->keys[i];
node->keys[i] = next->keys[0];
cur->n++;
next->n--;
}
else // 如果左右兄弟结点 keys 数都等于 t-1,则对它们进行合并
{
if (i != node->n)
merge(node, i);
else
merge(node, i - 1);
}
}
if (flag && i > node->n)
erase_real(node->siblings[i - 1], key);
else
erase_real(node->siblings[i], key);
}
}
BTree::BTree(int t)
{
this->t = t;
this->root = nullptr;
}
BTree::~BTree()
{
if (root)
destroy(root);
}
void BTree::insert(int key)
{
if (!root)
{
root = new Node(t, true);
root->keys[0] = key;
root->n = 1;
}
else
{
if (root->n == 2 * t - 1)
{
Node * s = new Node(t, false);
s->siblings[0] = root;
split_child(s, 0, root);
int i = 0;
if (s->keys[0] < key)
i++;
insert_non_full(s->siblings[i], key);
root = s;
}
else
insert_non_full(root, key);
}
}
bool BTree::find(int key)
{
if (root)
return find_real(root, key);
return false;
}
void BTree::erase(int key)
{
if (!root)
return;
erase_real(root, key);
if (root->n == 0)
{
Node * t = root;
if (root->leaf)
root = nullptr;
else
root = root->siblings[0];
delete t;
}
}
void BTree::print()
{
if (root)
{
queue<Node*> Q;
Q.push(root);
while (!Q.empty())
{
Node * t = Q.front();
Q.pop();
for (int i = 0; i < t->n; i++)
cout << t->keys[i] << " ";
cout << endl;
if (!t->leaf)
{
for (int i = 0; i < t->n + 1; i++)
Q.push(t->siblings[i]);
}
}
cout << endl;
}
}
运行截图如下:
四:参考文献
- 维基百科. B 树.
- GeeksforGeeks. B-Tree | Set 1 (Introduction).
- GeeksforGeeks. B-Tree | Set 2 (Insert).
- GeeksforGeeks. B-Tree | Set 3 (Delete).
今天的文章B – 树分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/22718.html