一个 m 阶的B树是一个有以下属性的树:
每一个内部节点的键将节点的子树分开。例如,如果一个内部节点有3个子节点(子树),那么它就必须有两个键: a1 和 a2 。左边子树的所有值都必须小于 a1 ,中间子树的所有值都必须在 a1 和a2 之间,右边子树的所有值都必须大于 a2 。
注:由于m=2时B树可以退化成二叉搜索树(BST),且m=2时B树的删除与m>=3时的B树的删除不兼容,因此本文实现的代码仅支持m>=3的B树,同时也会介绍为什么m=2时B树的删除不兼容。
template>
class BTree{
private:
struct Node{
......
};
Compare compare;
int size;
Node *root;
std::stack findNodeByKey(Key key);
void adjustNodeForInsert(Node *node,Node* parent);
void adjustNodeForRemove(Node *node,Node* parent);
void maintainAfterInsert(std::stack& nodePathStack);
void maintainAfterRemove(std::stack& nodePathStack);
public:
BTree(){
assert(order>=3);
this->size = 0;
this->root = nullptr;
this->compare = Compare();
}
int insert(Key key,Value value);
int remove(Key key);
void levelOrderTraversal();
};
成员名 | 类型 | 描述 |
---|---|---|
n | int | 节点数量 |
keys | Key[] | 节点键值的数组,具有唯一性和可排序性 |
values | Value[] | 节点保存的值的数组 |
children | Node*[] | 节点指向孩子们的数组 对keys[i]而言,children[i]是其左子树,children[i+1]是其右子树 |
struct Node{
// 对根节点 n满足 1 <= n <= order-1
// 除根节点外 n满足 ((order-1)>>1) <= n <= order-1
int n;
// 大小设置为order,保证上溢出时也可插入
Key keys[order];
Value values[order];
// 大小设置为order+1,保证上溢出时也可插入
Node* children[order+1];
// 构造函数
Node():n(0){
for(auto &p:children) p = nullptr;
}
// 二分查找,返回第一个大于等于该节点key的下标
inline int search(Key key,const Compare& compare) const noexcept{
// 避免因为极端情况导致的查询效果低下
if(!this->n || !compare(this->keys[0],key)) return 0;
if(this->n && compare(this->keys[this->n-1],key)) return this->n;
int i=1,j=this->n-1;
while(i<=j){
int mid = i+((j-i)>>1);
if(this->keys[mid] == key) return mid;
if(compare(this->keys[mid],key)) i = mid + 1;
else j = mid - 1;
}
return i;
}
// 判断节点是否为叶子节点
inline bool isLeaf() const noexcept{
return this->children[0] == nullptr;
}
// 判断节点是否含有key
inline bool hasKey(Key key,const Compare& compare) const noexcept{
int arg = this->search(key,compare);
return this->keys[arg] == key;
}
// 更新节点
inline void update(Key key,Value value,const Compare& compare){
int arg = this->search(key,compare);
this->values[arg] = value;
}
// 插入节点及其右子树
inline void insert(Key key,Value value,Node* rightChild,const Compare& compare){
int arg = this->search(key,compare);
for(int i=this->n;i>arg;i--){
this->keys[i] = this->keys[i-1];
this->values[i] = this->values[i-1];
this->children[i+1] = this->children[i];
}
this->keys[arg] = key;
this->values[arg] = value;
this->children[arg+1] = rightChild;
this->n++;
}
// 删除节点及其右子树
inline void remove(Key key,const Compare& compare){
int arg = this->search(key,compare);
for(int i=arg;in-1;i++){
this->keys[i] = this->keys[i+1];
this->values[i] = this->values[i+1];
this->children[i+1] = this->children[i+2];
}
this->children[this->n] = nullptr;
this->n--;
}
// 上溢出(n >= order)的时候调用,分裂成左右子树,自身变成左子树,返回右子树
inline Node* split(){
Node* newNode = new Node();
int mid = ((order-1)>>1);
newNode->children[0] = this->children[mid+1];
this->children[mid+1] = nullptr;
for(int i=0,j=mid+1;jn;i++,j++){
newNode->keys[i] = this->keys[j];
newNode->values[i] = this->values[j];
newNode->children[i+1] = this->children[j+1];
newNode->n++;
this->children[j+1] = nullptr;
}
this->n = mid;
return newNode;
}
// 下溢出(n < ((order-1)>>1))且兄弟无法借出节点时调用,和右兄弟合并
inline void merge(Key key,Value value,Node *rightSibling){
this->keys[this->n] = key;
this->values[this->n] = value;
this->children[this->n+1] = rightSibling->children[0];
this->n++;
for(int i=0;in;i++){
this->keys[this->n] = rightSibling->keys[i];
this->values[this->n] = rightSibling->values[i];
this->children[this->n+1] = rightSibling->children[i+1];
this->n++;
}
delete rightSibling;
}
};
B树本身也是搜索树,可以直接按照搜索树的搜索流程进行,由于查找路径会在插入和删除时用到,因此可以直接返回保存了查找路径的栈,则栈顶即为含有key的节点
注:调用此函数需要保证根节点存在
/**
* @brief 查找含有key的节点(需保证根节点存在)
* @param key 键值
* @return 保存了查找路径的栈,栈顶即为含有key的节点
*/
template
std::stack::Node*> BTree::findNodeByKey(Key key){
std::stack nodePathStack;
Node* node = this->root;
nodePathStack.push(node);
while(node->children[0]){
int arg = node->search(key,this->compare);
if(argn && node->keys[arg]==key) break;
node = node->children[arg];
nodePathStack.push(node);
}
return nodePathStack;
}
/**
* @brief 插入键值对
* @param key 新的键
* @param value 新的值
* @return int 0表示插入成功,1表示节点已存在,更新value
*/
template
int BTree::insert(Key key,Value value){
if(this->root==nullptr){
this->root = new Node();
this->root->insert(key,value,nullptr,this->compare);
return 0;
}
std::stack nodePathStack = this->findNodeByKey(key);
Node *node = nodePathStack.top();
if(node->hasKey(key,this->compare)){
node->update(key,value,this->compare);
return 1;
}
node->insert(key,value,nullptr,this->compare);
this->maintainAfterInsert(nodePathStack);
this->size++;
return 0;
}
B树插入后的维护即是检查被维护的节点是否上溢出(保存数据的数量n==order),若未溢出,则无需维护当前和后续节点,反之若上溢出,则当前节点需要结合其父节点进行调整。注意若出现根节点上溢出,则需要创建一个新节点作为根节点,并让当前节点作为其第一个孩子进行调整父子间的调整
/**
* @brief 插入新数据后的维护
* @param nodePathStack 保存了因为插入而受到影响的节点的栈
* @return void
*/
template
void BTree::maintainAfterInsert(std::stack& nodePathStack){
Node *node,*parent;
node = nodePathStack.top();nodePathStack.pop();
while(!nodePathStack.empty()){
parent = nodePathStack.top();nodePathStack.pop();
if(node->n < order) return ;
this->adjustNodeForInsert(node,parent);
node = parent;
}
if(node->n < order) return ;
this->root = new Node();
parent = this->root;
parent->children[0] = node;
this->adjustNodeForInsert(node, parent);
}
将上溢出的子节点根据中间数分成不包括中间数的左右两个部分,该过程可以调用Node结构体函数split函数实现,其可以将自身分成左右两个节点,自身变为左节点,并返回右节点,最后将中间数和右节点插入父节点即可
/**
* @brief 调整上溢出节点
* @param node 上溢出节点
* @param parent 上溢出节点父亲
* @return void
*/
template
void BTree::adjustNodeForInsert(Node *node,Node* parent){
// parent: ... ... ... mid ...
// / =====> / |
// node: [left] mid [right] [left] [right]
int mid = ((order-1)>>1);
Key key = node->keys[mid];
Value value = node->values[mid];
Node *rightChild = node->split();
parent->insert(key,value,rightChild,this->compare);
}
/**
* @brief 根据键删除数据
* @param key 要被删除的数据的键
* @return int 0表示删除成功,1表示键不存在,删除失败
*/
template
int BTree::remove(Key key){
if(this->root == nullptr) return 1;
std::stack nodePathStack = this->findNodeByKey(key);
Node *node = nodePathStack.top();
if(!node->hasKey(key,this->compare)) return 1;
// 检查节点是否为叶子节点,若不是,则找到其直接后继进行替换,将要删除的数据变成直接后继,并相应的更新查询路径
if(!node->isLeaf()){
Node* temp = node;
int arg = node->search(key,this->compare);
node = node->children[arg+1];
nodePathStack.push(node);
while(!node->isLeaf()){
node = node->children[0];
nodePathStack.push(node);
}
temp->keys[arg] = node->keys[0];
temp->values[arg] = node->values[0];
key = node->keys[0];
}
node->remove(key,this->compare);
this->maintainAfterRemove(nodePathStack);
this->size--;
return 0;
}
B树删除后的维护即是检查被维护的节点是否下溢出(保存数据的数量n<(order+1)/2-1),若未溢出,则无需维护当前和后续节点,反之若下溢出,则当前节点需要结合其父节点进行调整。注意若当前节点是根节点,且其因为删除或删除后维护而造成数据量归0,则需要更新根节点为其第一个孩子(具体为什么则这样可以对照插入的流程)
/**
* @brief 删除数据后的维护
* @param nodePathStack 保存了因为删除而受到影响的节点的栈
* @return void
*/
template
void BTree::maintainAfterRemove(std::stack& nodePathStack){
int mid = (order-1)>>1;
Node *node,*parent;
node = nodePathStack.top();nodePathStack.pop();
while(!nodePathStack.empty()){
parent = nodePathStack.top();nodePathStack.pop();
if(node->n >= mid) return ;
this->adjustNodeForRemove(node,parent);
node = parent;
}
if(this->root->n) return ;
this->root = node->children[0];
delete node;
}
case 1:左兄弟或右兄弟可以借出一个数据
左右兄弟借出的数据不能直接给到子节点,不然会破坏搜索树的性质,此时可以将父节点中,左右子树分别为子节点和兄弟的数据下放到子节点,同时将兄弟借出的数据替换父节点下放的数据
case 2:左兄弟或右兄弟都不能借出一个数据
此时子节点可以选择与其一个存在的兄弟进行合并,根据其左右顺序,可以分为合并的左节点和右节点。合并的流程是,将父节点中,左右子树分别为子节点和兄弟的数据下放到合并的左节点,之后与合并的右节点进行合并,合并的过程可以调用Node结构体函数merge函数,该函数可以接受父亲的数据和合并的右节点进行合并,自身变成合并后的结果
/**
* @brief 调整下溢出节点
* @param node 下溢出节点
* @param parent 下溢出节点的父亲
* @return void
*/
template
void BTree::adjustNodeForRemove(Node *node,Node* parent){
int mid = ((order-1)>>1);
int arg=-1;
if(node->n) arg = parent->search(node->keys[0],this->compare);
else while(parent->children[++arg]!=node);
Node* left = arg > 0 ? parent->children[arg-1] : nullptr;
Node* right = arg < parent->n ? parent->children[arg+1] : nullptr;
// case 1: 左兄弟或右兄弟可以借出一个数据
if((left && left->n > mid) || (right && right->n > mid)){
// 左兄弟借出一个数据
// parent: ... key ... ... last ...
// / \ =====> / \
// [......]:last [node] [......] key:[node]
if(left && left->n > mid){
Key key = parent->keys[arg-1];
Value value = parent->values[arg-1];
node->insert(key,value,node->children[0],this->compare);
node->children[0] = left->children[left->n];
parent->keys[arg-1] = left->keys[left->n-1];
parent->values[arg-1] = left->values[left->n-1];
left->remove(left->keys[left->n-1],this->compare);
}
// 右兄弟借出一个数据
// parent: ... key ... ... first ...
// / \ =====> / \
// [node] first:[......] [node]:key [......]
else if(right && right->n > mid){
Key key = parent->keys[arg];
Value value = parent->values[arg];
node->insert(key,value,right->children[0],this->compare);
right->children[0] = right->children[1];
parent->keys[arg] = right->keys[0];
parent->values[arg] = right->values[0];
right->remove(right->keys[0],this->compare);
}
return ;
}
// case 2: 左兄弟或右兄弟都不能借出一个数据
if(left){
// 和左兄弟合并
// parent: ... key ... ... ...
// / \ =====> /
// [left] [node] [left]:key:[node]
Key key = parent->keys[arg-1];
Value value = parent->values[arg-1];
left->merge(key,value,node);
parent->remove(key,this->compare);
}
else if(right){
// 和右兄弟合并
// parent: ... key ... ... ...
// / \ =====> /
// [node] [right] [node]:key:[right]
Key key = parent->keys[arg];
Value value = parent->values[arg];
node->merge(key,value,right);
parent->remove(key,this->compare);
}
}
m=2时,根据定义,此时每个节点至少需要有0个节点,也就是说没有数据的空节点是允许存在的,而对于非叶子节点的内的数据删除,其需要找到后继节点中的第1个数据进行替换删除,而由于空节点是允许存在的,此时找到的后继节点可能没有数据,也就无法进行数据的替换。
个人认为解决该问题的一个思路是引入“第0个数据”的概念,但碍于2阶B树的实际意义并不大,因此并没有继续深入研究
OIwiki B树 https://oi-wiki.org/ds/b-tree/
B站 B树(B-树) - 来由, 定义, 插入, 构建 https://www.bilibili.com/video/BV1tJ4m1w7yR
B站 B树(B-树) - 删除 https://www.bilibili.com/video/BV1JU411d7iY
#include
#include
#include
#include
#include
#include
template>
class BTree{
private:
struct Node{
// 对根节点 n满足 1 <= n <= order-1
// 除根节点外 n满足 ((order-1)>>1) <= n <= order-1
int n;
// 大小设置为order,保证上溢出时也可插入
Key keys[order];
Value values[order];
// 大小设置为order+1,保证上溢出时也可插入
Node* children[order+1];
// 构造函数
Node():n(0){
for(auto &p:children) p = nullptr;
}
// 二分查找,返回第一个大于等于该节点key的下标
inline int search(Key key,const Compare& compare) const noexcept{
// 避免因为极端情况导致的查询效果低下
if(!this->n || !compare(this->keys[0],key)) return 0;
if(this->n && compare(this->keys[this->n-1],key)) return this->n;
int i=1,j=this->n-1;
while(i<=j){
int mid = i+((j-i)>>1);
if(this->keys[mid] == key) return mid;
if(compare(this->keys[mid],key)) i = mid + 1;
else j = mid - 1;
}
return i;
}
// 判断节点是否为叶子节点
inline bool isLeaf() const noexcept{
return this->children[0] == nullptr;
}
// 判断节点是否含有key
inline bool hasKey(Key key,const Compare& compare) const noexcept{
int arg = this->search(key,compare);
return this->keys[arg] == key;
}
// 更新节点
inline void update(Key key,Value value,const Compare& compare){
int arg = this->search(key,compare);
this->values[arg] = value;
}
// 插入节点及其右子树
inline void insert(Key key,Value value,Node* rightChild,const Compare& compare){
int arg = this->search(key,compare);
for(int i=this->n;i>arg;i--){
this->keys[i] = this->keys[i-1];
this->values[i] = this->values[i-1];
this->children[i+1] = this->children[i];
}
this->keys[arg] = key;
this->values[arg] = value;
this->children[arg+1] = rightChild;
this->n++;
}
// 删除节点及其右子树
inline void remove(Key key,const Compare& compare){
int arg = this->search(key,compare);
for(int i=arg;in-1;i++){
this->keys[i] = this->keys[i+1];
this->values[i] = this->values[i+1];
this->children[i+1] = this->children[i+2];
}
this->children[this->n] = nullptr;
this->n--;
}
// 上溢出(n >= order)的时候调用,分裂成左右子树,自身变成左子树,返回右子树
inline Node* split(){
Node* newNode = new Node();
int mid = ((order-1)>>1);
newNode->children[0] = this->children[mid+1];
this->children[mid+1] = nullptr;
for(int i=0,j=mid+1;jn;i++,j++){
newNode->keys[i] = this->keys[j];
newNode->values[i] = this->values[j];
newNode->children[i+1] = this->children[j+1];
newNode->n++;
this->children[j+1] = nullptr;
}
this->n = mid;
return newNode;
}
// 下溢出(n < ((order-1)>>1))且兄弟无法借出节点时调用,和右兄弟合并
inline void merge(Key key,Value value,Node *rightSibling){
this->keys[this->n] = key;
this->values[this->n] = value;
this->children[this->n+1] = rightSibling->children[0];
this->n++;
for(int i=0;in;i++){
this->keys[this->n] = rightSibling->keys[i];
this->values[this->n] = rightSibling->values[i];
this->children[this->n+1] = rightSibling->children[i+1];
this->n++;
}
delete rightSibling;
}
};
Compare compare;
int size;
Node *root;
std::stack findNodeByKey(Key key);
void adjustNodeForInsert(Node *node,Node* parent);
void adjustNodeForRemove(Node *node,Node* parent);
void maintainAfterInsert(std::stack& nodePathStack);
void maintainAfterRemove(std::stack& nodePathStack);
public:
BTree(){
assert(order>=3);
this->size = 0;
this->root = nullptr;
this->compare = Compare();
}
int insert(Key key,Value value);
int remove(Key key);
void levelOrderTraversal();
};
/**
* @brief 查找含有key的节点(需保证根节点存在)
* @param key 键值
* @return 保存了查找路径的栈,栈顶即为含有key的节点
*/
template
std::stack::Node*> BTree::findNodeByKey(Key key){
std::stack nodePathStack;
Node* node = this->root;
nodePathStack.push(node);
while(node->children[0]){
int arg = node->search(key,this->compare);
if(argn && node->keys[arg]==key) break;
node = node->children[arg];
nodePathStack.push(node);
}
return nodePathStack;
}
/**
* @brief 插入键值对
* @param key 新的键
* @param value 新的值
* @return int 0表示插入成功,1表示节点已存在,更新value
*/
template
int BTree::insert(Key key,Value value){
if(this->root==nullptr){
this->root = new Node();
this->root->insert(key,value,nullptr,this->compare);
return 0;
}
std::stack nodePathStack = this->findNodeByKey(key);
Node *node = nodePathStack.top();
if(node->hasKey(key,this->compare)){
node->update(key,value,this->compare);
return 1;
}
node->insert(key,value,nullptr,this->compare);
this->maintainAfterInsert(nodePathStack);
this->size++;
return 0;
}
/**
* @brief 插入新数据后的维护
* @param nodePathStack 保存了因为插入而受到影响的节点的栈
* @return void
*/
template
void BTree::maintainAfterInsert(std::stack& nodePathStack){
Node *node,*parent;
node = nodePathStack.top();nodePathStack.pop();
while(!nodePathStack.empty()){
parent = nodePathStack.top();nodePathStack.pop();
if(node->n < order) return ;
this->adjustNodeForInsert(node,parent);
node = parent;
}
if(node->n < order) return ;
this->root = new Node();
parent = this->root;
parent->children[0] = node;
this->adjustNodeForInsert(node, parent);
}
/**
* @brief 调整上溢出节点
* @param node 上溢出节点
* @param parent 上溢出节点的父亲
* @return void
*/
template
void BTree::adjustNodeForInsert(Node *node,Node* parent){
// parent: ... ... ... mid ...
// / =====> / |
// node: [left] mid [right] [left] [right]
int mid = ((order-1)>>1);
Key key = node->keys[mid];
Value value = node->values[mid];
Node *rightChild = node->split();
parent->insert(key,value,rightChild,this->compare);
}
/**
* @brief 根据键删除数据
* @param key 要被删除的数据的键
* @return int 0表示删除成功,1表示键不存在,删除失败
*/
template
int BTree::remove(Key key){
if(this->root == nullptr) return 1;
std::stack nodePathStack = this->findNodeByKey(key);
Node *node = nodePathStack.top();
if(!node->hasKey(key,this->compare)) return 1;
// 检查节点是否为叶子节点,若不是,则找到其直接后继进行替换,将要删除的数据变成直接后继,并相应的更新查询路径
if(!node->isLeaf()){
Node* temp = node;
int arg = node->search(key,this->compare);
node = node->children[arg+1];
nodePathStack.push(node);
while(!node->isLeaf()){
node = node->children[0];
nodePathStack.push(node);
}
temp->keys[arg] = node->keys[0];
temp->values[arg] = node->values[0];
key = node->keys[0];
}
node->remove(key,this->compare);
this->maintainAfterRemove(nodePathStack);
this->size--;
return 0;
}
/**
* @brief 删除数据后的维护
* @param nodePathStack 保存了因为删除而受到影响的节点的栈
* @return void
*/
template
void BTree::maintainAfterRemove(std::stack& nodePathStack){
int mid = (order-1)>>1;
Node *node,*parent;
node = nodePathStack.top();nodePathStack.pop();
while(!nodePathStack.empty()){
parent = nodePathStack.top();nodePathStack.pop();
if(node->n >= mid) return ;
this->adjustNodeForRemove(node,parent);
node = parent;
}
if(this->root->n) return ;
this->root = node->children[0];
delete node;
}
/**
* @brief 调整下溢出节点
* @param node 下溢出节点
* @param parent 下溢出节点的父亲
* @return void
*/
template
void BTree::adjustNodeForRemove(Node *node,Node* parent){
int mid = ((order-1)>>1);
int arg=-1;
if(node->n) arg = parent->search(node->keys[0],this->compare);
else while(parent->children[++arg]!=node);
Node* left = arg > 0 ? parent->children[arg-1] : nullptr;
Node* right = arg < parent->n ? parent->children[arg+1] : nullptr;
// case 1: 左兄弟或右兄弟可以借出一个数据
if((left && left->n > mid) || (right && right->n > mid)){
// 左兄弟借出一个数据
// parent: ... key ... ... last ...
// / \ =====> / \
// [......]:last [node] [......] key:[node]
if(left && left->n > mid){
Key key = parent->keys[arg-1];
Value value = parent->values[arg-1];
node->insert(key,value,node->children[0],this->compare);
node->children[0] = left->children[left->n];
parent->keys[arg-1] = left->keys[left->n-1];
parent->values[arg-1] = left->values[left->n-1];
left->remove(left->keys[left->n-1],this->compare);
}
// 右兄弟借出一个数据
// parent: ... key ... ... first ...
// / \ =====> / \
// [node] first:[......] [node]:key [......]
else if(right && right->n > mid){
Key key = parent->keys[arg];
Value value = parent->values[arg];
node->insert(key,value,right->children[0],this->compare);
right->children[0] = right->children[1];
parent->keys[arg] = right->keys[0];
parent->values[arg] = right->values[0];
right->remove(right->keys[0],this->compare);
}
return ;
}
// case 2: 左兄弟或右兄弟都不能借出一个数据
if(left){
// 和左兄弟合并
// parent: ... key ... ... ...
// / \ =====> /
// [left] [node] [left]:key:[node]
Key key = parent->keys[arg-1];
Value value = parent->values[arg-1];
left->merge(key,value,node);
parent->remove(key,this->compare);
}
else if(right){
// 和右兄弟合并
// parent: ... key ... ... ...
// / \ =====> /
// [node] [right] [node]:key:[right]
Key key = parent->keys[arg];
Value value = parent->values[arg];
node->merge(key,value,right);
parent->remove(key,this->compare);
}
}
/**
* @brief B树的中序遍历
* @return void
*/
template
void BTree::levelOrderTraversal(){
std::queue q;
q.push(this->root);
while(!q.empty()){
int layerSize = q.size();
while(layerSize--){
Node *node = q.front();q.pop();
if(!node) continue;
int i;
for(i=0;in;i++){
std::cout << node->keys[i] << " ";
q.push(node->children[i]);
}
std::cout << " | ";
q.push(node->children[i]);
}
std::cout << std::endl;
}
}