B-树用于减少树的长度和减少插入节点操作时的OI次数。以下主要讨论其具体的代码实现。抽象过程和图示则移步至CS61B相关内容。
点这看原文
多出的解释或有不准确之处(因为我并不是很清楚)。
定义
1 2 3 4 5
| int *keys; int t; BTreeNode **C; int n; bool leaf;
|
其中leaf
可省略,可通过判断**C
是否为空指针实现。
B-树中一个节点的定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class BTreeNode { int *keys; int t; BTreeNode **C; int n; bool leaf; public: BTreeNode(int _t, bool _leaf);
void traverse();
BTreeNode *search(int k);
friend class BTree; };
|
友元friend
是C++中的一个关键字,友元函数可以访问类中的private
成员和protected
成员,但由于友元函数可定义在类的外侧,因此友元函数不是成员函数。
友元类即指定某类,可访问自己的private
成员和protected
成员。
友元能节省类中部分getter()
和setter()
函数的使用,但会导致类的继承关系较为混乱。
B树的完整定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class BTree { BTreeNode *root; int t; public: BTree(int _t) { root = NULL; t = _t; }
void traverse() { if (root != NULL) root->traverse(); }
BTreeNode* search(int k) { return (root == NULL) ? NULL : root->search(k); } };
|
查找
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| BTreeNode *BTreeNode::search(int k) { int i = 0; while (i < n && k > keys[i]) i++;
if (keys[i] == k) return this;
if (leaf == true) return NULL;
return C[i]->search(k); }
|
寻找到k所在的位置或范围(找到则是具体位置,输出结果,没找到则是两个数之间的范围)。若找到的是范围,则寻找其子树(如果有)。而子树数组和值数组间的关系如下图
key: | 0 | 1 | 2 | 3 |
child: | 0 | 1 | 2 | 3 | 4 |
而i
只是一个索引,key数组的索引和child数组的索引是等效的。
此处是先循环,找到索引再判断情况。如果是创造一个大循环体,又会是怎么样的状况?
中序遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| void BTreeNode::traverse() { int i; for (i = 0; i < n; i++) { if (leaf == false) C[i]->traverse(); cout << " " << keys[i]; }
if (leaf == false) C[i]->traverse(); }
|
本节点的右子树即下一节点的左子树,因此只需要循环打印左子树-根节点即可。最后再打印右子树。
插入
插入的操作较为复杂。需要涉及指针的重新分配。以下是伪代码
- 初始化x作为根结点
- 当x不是叶子结点,执行如下操作:
- 找到x的下一个要被访问的孩子结点 y
- 如果y没有满,则将结点y作为新的 x
- 如果y已经满了,拆分y,结点x的指针指向结点y的两部分。 如果 k 比y中间的关键字小, 则将y的第一部分作为新的x,否则将y的第二部分作为新的x,当将y拆分后,将y中的一个关键字移动到它的父结点x当中。
以下是插入主函数的操作
- 若树为空树,则在关键字数组中添加一员。
- 若树的结点已满,则进行生长操作
- 生成新的根节点。
- 旧的根节点分成两个,均作为新的根节点的孩子。
- 随后更新结点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| void BTree::insert(int k) { if (root == NULL) { root = new BTreeNode(t, true); root->keys[0] = k; root->n = 1; } else { if (root->n == 2*t-1) { BTreeNode *s = new BTreeNode(t, false);
s->C[0] = root;
s->splitChild(0, root);
int i = 0; if (s->keys[0] < k) i++; s->C[i]->insertNonFull(k);
root = s; } else root->insertNonFull(k); } }
|
将关键字 k 插入到一个未满的结点中,步骤如下
- 如果是叶子节点,则直接寻找位置插入。
- 如果不是叶子结点
- 找到符合条件的孩子结点。
- 若孩子结点已满,则进行分裂操作
C[i]
中第二个关键字上移到父节点,其余分裂为两个孩子结点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| void BTreeNode::insertNonFull(int k) { int i = n-1;
if (leaf == true) { while (i >= 0 && keys[i] > k) { keys[i+1] = keys[i]; i--; }
keys[i+1] = k; n = n+1; } else { while (i >= 0 && keys[i] > k) i--;
if (C[i+1]->n == 2*t-1) { splitChild(i+1, C[i+1]);
if (keys[i+1] < k) i++; } C[i+1]->insertNonFull(k); } }
|
结点y已满,则分裂结点 y
- 创建一个新的结点,空间更小
- y的后面结点都到新结点中。若y不是叶子结点,则将孩子结点全部拷贝到新结点中。
- 随后就是常规的数组移动数据,腾位置给指定结点的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| void BTreeNode::splitChild(int i, BTreeNode *y) { BTreeNode *z = new BTreeNode(y->t, y->leaf); z->n = t - 1;
for (int j = 0; j < t-1; j++) z->keys[j] = y->keys[j+t];
if (y->leaf == false) { for (int j = 0; j < t; j++) z->C[j] = y->C[j+t]; }
y->n = t - 1;
for (int j = n; j >= i+1; j--) C[j+1] = C[j];
C[i+1] = z;
for (int j = n-1; j >= i; j--) keys[j+1] = keys[j];
keys[i] = y->keys[t-1];
n = n + 1; }
|