定义
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树在内存中体现不出优势 。