定义
- B树,又称M路平衡搜索树,一个结点上可以有多个key和映射规则

性质
- 根节点至少有两个孩子,所有的叶子节点都在同一层
- 每个分支节点都包含k-1个关键字和k个孩子(孩子比关键字多一个)
- 每个叶子节点只包含k-1个关键字,其中ceil(m/2) ≤ k ≤ m
- 每个节点中的关键字从小到大排列
- 每个结点的结构为(n,A0,K1,A1,K2,A2,… ,Kn,An)
其中,Ki(1≤i≤n)为关键字,且Ki<Ki+1;Ai(0≤i≤n) 为指向子树根结点的指针,且Ai所指子树所有结点中的关键字均小于Ki+1; n为结点中关键字的个数,满足ceil(m/2)-1≤n≤m-1。
B树的模拟实现
定义B树的结点
- 设置数据的结构为<class K,int M>类型,K为磁盘地址(关键字),M为孩子的个数;
- 若结点中关键字的个数定义为M,则其孩子的个数为M+1,但是为了方便插入以后再分裂,此处多给一个空间;
template <class K, int M>
struct BTreeNode{
K _keys[M];
BTreeNode<K, M>* _subs[M + 1];
BTreeNode<K, M>* _parent;
size_t _n;
BTreeNode()
:_parent(nullptr),
_n(0)
{
for (size_t i = 0; i < M; i++){
_keys[i] = K();
_subs[i] = nullptr;
}
}
};
B树的基本框架
class BTree{
public:
typedef BTreeNode<K, M> Node;
private:
Node* _root = nullptr;
};
B树的基本操作
find查找操作
- 小于该关键字或者与最后一个关键字都比较完了向下一层找,大于继续向后找,等于返回key所在结点及其下标;
pair<Node*, int> find(const K& key){
Node* cur = _root;
Node* parent = nullptr;
while (cur != nullptr){
size_t i = 0;
while (i < cur->_n){
if (key < cur->_keys[i]){
break;
}
else if (key > cur->_keys[i]){
i++;
}
else
return pair<Node*, int>(cur, i);
}
parent = cur;
cur = cur->_subs[i];
}
return pair<Node*, int>(parent, -1);
}
insertkey插入操作
- 在结点中插入关键字和对应右孩子:从后向前找待插入的位置,若key小,则当前位置的关键字向后挪动,关键字挪动的同时,其对应的右孩子也要挪动;否则插入key和其对应的右孩子child;
void insertKey(Node* node, const K& key, Node* child){
size_t i = node->_n - 1;
for ( ; i >= 0; i--){
if (key < node->_keys[i]){
node->_keys[i + 1] = node->_keys[i];
node->_subs[i + 2] = node->_subs[i + 1];
}
else{
break;
}
}
node->_keys[i + 1] = key;
node->_subs[i + 2] = child;
if (child != nullptr){
child->_parent = node;
}
node->_n++;
}
insert插入操作
- 空树-----直接插入根节点
- key值已在树中-----不用插入
- key值不在树中-----找待插入结点的位置-----insertKey-----判满-----若为满则分裂
- 分裂-----分裂出自己的一半关键字和孩子给兄弟结点,中位数给父亲
- 连接-----连接自己和父亲,连接兄弟和父亲
- 注意:插入过程中可能会涉及到满的情况,需要向上生长,所以要记录每个结点的父结点。
bool insert(const K& key){
if (_root == nullptr){
_root = new Node;
_root->_keys[0] = key;
_root->_n = 1;
return true;
}
else{
pair<Node*, int> ret = find(key);
if (ret.second != -1){
return false;
}
else{
Node* parent = ret.first;
K newkey = key;
Node* child = nullptr;
while (1){
insertKey(parent, newkey, child);
if (parent->_n < M){
return true;
}
size_t mid = M / 2;
Node* brother = new Node;
size_t j = 0;
size_t i = mid + 1;
for ( ; i < M; i++){
brother->_keys[j] = parent->_keys[i];
brother->_subs[j] = parent->_subs[i];
parent->_keys[i] = K();
parent->_subs[i] = nullptr;
j++;
}
brother->_subs[j] = parent->_subs[i];
parent->_subs[i] = nullptr;
parent->_n -= (j+1);
brother->_n += j;
if (parent->_parent == nullptr){
_root = new Node;
_root->_keys[0] = parent->_keys[mid];
_root->_subs[0] = parent;
_root->_subs[1] = brother;
_root->_n++;
parent->_parent = _root;
brother->_parent = _root;
return true;
}
else{
newkey = parent->_keys[mid];
child = brother;
parent = parent->_parent;
}
}
}
}
return true;
}
B树的验证
void _inorder(Node* root){
if (root == nullptr)
return;
for (size_t i = 0; i < root->_n; i++){
_inorder(root->_subs[i]);
cout << root->_keys[i] << " ";
}
_inorder(root->_subs[root->_n]);
}
B树的性能分析
- 对于度为M的B树,每一个节点的子节点个数为M/2 ~(M-1)之间,因此树的高度应该在要logM-1N和logM/2N之间,在定位到该节点后,再采用二分查找的方式可以很快的定位到该元素,大大减少了读取磁盘的次数。
- 只在内存中查找:单纯论树的高度,搜索效率而言,B树确实不错;
但其有一些隐形的坏处:空间利用率低,消耗高;
插入删除数据时,若分裂或合并结点,必然要挪动数据;虽然高度更低,但是在内存中而言,与哈希和平衡搜索树是一个量级的。 - 所以,实质上,B树在内存中体现不出优势。