B树
首先本文参考了其他一些博客,在此列出,表示感谢:
http://blog.csdn.net/v_july_v/article/details/6530142
http://blog.csdn.net/mishifangxiangdefeng/article/details/7798672
(一)m叉搜索树
在讨论B树之前,首先我们来讨论m叉搜索树,所谓m叉搜索树是这样定义的:
对于内部节点(非叶子节点),它含有n个键值k1, k2, ... , kn 和 n+1个儿子节点s0, s1, ... , sn (其中1 <= k <= m -1)
且满足:
(i) k1 < k2 < ... < kn, 键值k是递增序列
(ii) 对于任意一个儿子节点 si ,以si为根节点的子树中的所有键值都要大于ki小于ki+1
对于外部节点(叶子节点),不含任何键值和儿子节点指针
其实上面的定义虽然拗口晦涩,但是实际上确很简单,如果m=2,那么就是一颗简单的BST了,所以可以借助BST来思考m
叉搜索树的样子,这里为了帮助理解,画出一颗4叉搜索树如下:
图中蓝色节点为内部节点,白色节点为外部节点,内部节点中的数值为键值
(二)B-Tree
(1)定义
介绍完m叉搜索树之后,现在我们来看一下m阶B树的定义:
1)m阶B树是一颗m叉搜索树
2)除了根节点以外,所有内部节点都至少含有UP(m/2)个孩子, UP是取上界函数
3)所有外部节点都在同一层次
所以可以看到, B树其实就是一种特殊的m叉搜索树,它的特殊意义在于一是限定内部节点的孩子不能太少,而是限定所有外部节点位于同一层
换句话说就是保证树是平衡的
5阶B树(最底层为叶子节点,这里省略)
(2)性质
根据B树的定义,我们可以得到相关性质如下:
1)如果一颗m阶B树存储的总键值数目为n, 设根节点高度为0, d = UP(m/2)
那么B树的高度最差情况下(值最大)是 logd [(n + 1) / 2] + 1
B树高度最优情况下(值最小)是 logm (n + 1) + 1
这里证明一下最坏情况下的高度:
最坏情况就是跟节点只有两个儿子,而其他内部节点只有d = UP(m/2)个儿子,那么设树高度为h,总键值数目有n个
则,0层键值有1个
1层有2*(d-1)个
2层有2*(d-1)^2个
...
h-1层有2*(d-1)^(h-1)个
h层都是外部节点所以为0个
那么 1 + 2(d-1) + 2(d-1)^2 + ... + 2(d-1)^(h-1) = 1 + 2[(d-1) + (d-1)^2 + ... + (d-1)^(h-1)]
= 1 + 2(d-1) * (1 - dh-1) / ( 1 - d) ,
= 1 + 2*(dh-1 - 1)
= 2*dh-1 - 1 = n
=> h = logd [(n + 1) / 2] + 1
(3)结构
根据上面的定义,可以清楚的得到B树的节点结构形式:
1 // B-Tree节点,按照算法导论中的定义 2 struct BTreeNode{ 3 BTreeNode* parent; //父亲节点 4 int rank; // 节点在父亲节点中childs的位置 ,这个值一般来说没什么用,可以不用管它,只是在下面的删除操作时候会用到 5 int nums; // keys个数 6 list<int> keys; 7 list<BTreeNode*> childs; 8 BTreeNode(int nums):nums(nums){ } 9 };
(4) 树的基本操作
i)查找:
给定一个key查找出包含这个key的节点,如果不存在返回叶子节点
//find函数内部实现函数 BTreeNode* BTree::_find(int key, BTreeNode *node){ if(!node->nums) return node; list<int>::iterator iter_k = node->keys.begin(); list<BTreeNode*>::iterator iter_c = node->childs.begin(); while(iter_k != node->keys.end() && key >= *iter_k){ if(key == *iter_k) return node; // 找到key值,返回节点 iter_k++; iter_c++; } BTreeNode *ret = _find(key, *iter_c); // 递归搜索子树 return ret; } //查找时间为O(h*m) + h次访问节点时间(在磁盘模型中就是访问磁块的时间) BTreeNode* BTree::find(int key){ BTreeNode* ret = _find(key, root); return ret; }
从查找函数的时间分析,可以看出在m阶B树查找一个key值,在内存中操作时间花费为O(h*m), 访问节点数目为h个,如果这数据量特别大,
整个树的节点内容只能保存到磁盘中,那么每次访问一个节点就是读取一个磁块,假设访问一次磁盘读取一个磁块所花时间为tm,那么整个find函数
磁盘访问花费时间为 h*tm
所以总时间 T = O(h*m) + h * tm
我们知道访问磁盘时候,寻址时间非常的漫长,所以每一次读取一个磁块的时间都是远远大于在内存中对这块磁块中数据操作所花的时间,
即tm >> O(m),所以可以看到T的大小主要受树的高度h影响,这也就是为什么在那些磁盘检索内容的场景中(比如数据库)非常喜欢使用B树来组织了,
因为树的高度要比红黑树等低很多,所以磁盘花费的时间就少很多.
ii)插入
在B树中插入一个key
对于插入操作,首先是find整个树看一下是否已经存在key,如果存在那么不做任何操作,如果不存在,那么find函数会返回一个外部节点L,
那么将这个key插入到该外部节点的父亲节点P中,这是其父亲节点P的key数目会增加1,有可能会达到m,违反节点树存储键值最大不超过m-1个原则,
那么就需要对它进行拆分,此时我们知道该节点P有m个key值,所以可以把第m/2个key放入P的父亲节点Q中,然后前m/2个key值组成一个节点,
后m/2个key值组成一个节点,这样Q的key值数目也可能超标,需要递归的调整.
相关伪代码如下:
1 //插入函数 2 void BTree::insert(int key){ 3 if(root->nums == 0){ // 树现在为空 4 root->keys.push_back(key); 5 root->childs.push_back(new BTreeNode(0, root)); 6 root->childs.push_back(new BTreeNode(0, root)); 7 root->nums++; 8 return; 9 } 10 BTreeNode *node = find(key); 11 if(node->nums == 0){ // key 确实不存在树中 12 BTreeNode *parent = node->parent; // parent节点是要插入的节点 13 list<int>::iterator iter = parent->keys.begin(); 14 list<BTreeNode*>::iterator iter_c = parent->childs.begin(); 15 while(iter != parent->keys.end() && key > *iter){ // 寻找插入位置 16 iter++; 17 iter_c++; 18 } 19 parent->keys.insert(iter, key); // 插入进去 20 iter_c++; 21 parent->childs.insert(iter_c, new BTreeNode(0, parent)); 22 parent->nums++; 23 if(parent->nums == m){ // 插入之后key值爆满,需要调整 24 adjust(parent); 25 } 26 } 27 }
下面是调整函数
1 //调整函数 2 void BTree::adjust(BTreeNode *node){ 3 BTreeNode *parent = node->parent; 4 if(!parent){// node节点是根节点 5 int mid = (node->nums)/2; 6 list<int>::iterator iter_k = node->keys.begin(); 7 list<BTreeNode*>::iterator iter_c = node->childs.begin(); 8 BTreeNode *left = new BTreeNode(), *right = new BTreeNode(); // 拆分后的两个新节点 9 for(int i = 0; i < mid; iter_k++, iter_c++, i++){//拆分, 前一半弄成坐便节点 10 left->nums++; 11 left->keys.push_back(*iter_k); 12 left->childs.push_back(*iter_c); 13 (*iter_c)->parent = left; 14 } 15 left->childs.push_back(*iter_c); 16 (*iter_c)->parent = left; 17 iter_c++; 18 BTreeNode *newroot = new BTreeNode();//由于node是根节点,需要重新生成新跟节点 19 newroot->keys.push_back(*iter_k); 20 newroot->childs.push_back(left); 21 newroot->childs.push_back(right); 22 newroot->nums = 1; 23 left->parent = newroot; 24 right->parent = newroot; 25 iter_k++; 26 for(; iter_k != node->keys.end(); iter_k++, iter_c++){//后一半弄成右边节点 27 right->nums++; 28 right->keys.push_back(*iter_k); 29 right->childs.push_back(*iter_c); 30 (*iter_c)->parent = right; 31 } 32 right->childs.push_back(*iter_c); 33 (*iter_c)->parent = right; 34 35 36 //delete node; 37 root = newroot; 38 return; 39 }else{// node 并非是根节点 40 int mid = (node->nums)/2; 41 list<int>::iterator iter_k = node->keys.begin(); 42 list<BTreeNode*>::iterator iter_c = node->childs.begin(); 43 BTreeNode *left = new BTreeNode(), *right = new BTreeNode(); 44 for(int i = 0; i < mid; iter_k++, iter_c++, i++){// 拆分 45 left->nums++; 46 left->keys.push_back(*iter_k); 47 left->childs.push_back(*iter_c); 48 (*iter_c)->parent = left; 49 } 50 left->childs.push_back(*iter_c); 51 (*iter_c)->parent = left; 52 iter_c++; 53 list<int>::iterator iter_pk = parent->keys.begin(); 54 list<BTreeNode*>::iterator iter_pc = parent->childs.begin(); 55 while(iter_pk != parent->keys.end() && *iter_k > *iter_pk){ 56 iter_pk++; 57 iter_pc++; 58 } 59 parent->keys.insert(iter_pk, *iter_k); 60 *iter_pc = right; 61 parent->childs.insert(iter_pc, left); 62 parent->nums++; 63 left->parent = parent; 64 right->parent = parent; 65 iter_k++; 66 for(; iter_k != node->keys.end(); iter_k++, iter_c++){ 67 right->nums++; 68 right->keys.push_back(*iter_k); 69 right->childs.push_back(*iter_c); 70 (*iter_c)->parent = right; 71 } 72 right->childs.push_back(*iter_c); 73 (*iter_c)->parent = right; 74 75 if(parent->nums == m)// 继续向上调整 76 return adjust(parent); 77 } 78 }
iii)删除
B树的删除节点十分的复杂,这里简单介绍一下具体方法:
* 情况1: 要删除的key所在B树的节点其子节点不是外部节点,即不是最低层的内部节点.这种情况就可以寻在此key的前驱或者后继,然后与器交换值,
再删除其前驱或者后继,则转化为情况2.
* 情况2:要删除的key所在B树的节点是最低层的内部节点.
* * 情况2a: 此节点所含key的数量大于B树的节点key数量下限,这种情况下由于直接删除此key,对该节点的key数量造成-1后还是满足大于等于key数量
最低数量的要求,所以可以直接删掉次key值.
* * 情况2b:此节点所含key数量正好为下限,但是其节点的兄弟节点(左兄弟或者右兄弟)所含key数量大于下限,则可以通过转移的手段,把兄弟节点的某个
key移到其父亲节点,然后父亲节点的某个key移到该节点.
* * 情况2c: 此节点所含key数量正好为下限且节点左右兄弟所含key数量也是下限,这时候需要跟左/右节点合并,然后再递归调整父亲节点.
相关代码如下:
1 void BTree::adjustLessRoot(){//调整根节点 2 root = root->childs.front(); 3 root->parent = NULL; 4 } 5 6 void BTree::_findIterator(BTreeNode *node, Key_Iter &iter_k, Childs_Iter &iter_c){// 寻找node节点所在父亲节点的位置 7 if(node == root) return; 8 if(node->nums == 0){ 9 BTreeNode *parent = node->parent; 10 iter_k = parent->keys.begin(); 11 iter_c = parent->childs.begin(); 12 while(node->rank-- > 0){ 13 iter_k++; 14 iter_c++; 15 } 16 return; 17 } 18 BTreeNode *parent = node->parent; 19 iter_k = parent->keys.begin(); 20 iter_c = parent->childs.begin(); 21 int key = node->keys.front(); 22 while(iter_k != parent->keys.end() && key > *iter_k){ 23 iter_k++; 24 iter_c++; 25 } 26 } 27 28 bool BTree::adjustLessBrother(BTreeNode *node){// 情况2具体调整 29 BTreeNode *parent = node->parent; 30 Key_Iter iter_pk; 31 Childs_Iter iter_pc; 32 _findIterator(node, iter_pk, iter_pc); 33 if(iter_pk != parent->keys.begin()){ // 有左兄弟 34 iter_pc--; 35 BTreeNode *left = *iter_pc; 36 iter_pc++; 37 if(left->nums > up - 1){ // 情况2b 38 iter_pk--; 39 node->keys.push_front(*iter_pk); 40 node->childs.push_front(left->childs.back()); 41 node->nums++; 42 *iter_pk = left->keys.back(); 43 left->childs.pop_back(); 44 left->keys.pop_back(); 45 left->nums--; 46 node->childs.front()->parent = node; 47 iter_pk++; 48 return true; 49 } 50 } 51 if(iter_pk != parent->keys.end()){ // 有右兄弟 52 iter_pc++; 53 BTreeNode *right = *iter_pc; 54 iter_pc--; 55 if(right->nums > up - 1){ // 情况2b 56 node->keys.push_back(*iter_pk); 57 node->childs.push_back(right->childs.front()); 58 node->nums++; 59 *iter_pk = right->keys.front(); 60 right->childs.pop_front(); 61 right->keys.pop_front(); 62 right->nums--; 63 node->childs.back()->parent = node; 64 return true; 65 } 66 } 67 if(parent->nums == 1 && parent->parent){ 68 Key_Iter iiter = parent->parent->keys.begin(); 69 int key = parent->keys.front(); 70 parent->rank = 0; 71 while(iiter != parent->parent->keys.end() && key > *iiter){ 72 iiter++; 73 parent->rank++; 74 } 75 } 76 // 情况2c 77 if(iter_pk != parent->keys.begin()){ // 有左兄弟 78 iter_pc--; 79 BTreeNode *left = *iter_pc; 80 iter_pc++; 81 iter_pk--; 82 left->keys.push_back(*iter_pk); 83 left->keys.insert(left->keys.end(), node->keys.begin(), node->keys.end()); 84 left->childs.insert(left->childs.end(), node->childs.begin(), node->childs.end()); 85 left->nums = left->nums + 1 + node->nums; 86 iter_pk++; 87 parent->keys.erase(iter_pk); 88 parent->childs.erase(iter_pc); 89 parent->nums--; 90 for(Childs_Iter iter_c = node->childs.begin(); iter_c != node->childs.end(); iter_c++){ 91 (*iter_c)->parent = left; 92 } 93 }else{ // 有右兄弟 94 iter_pc++; 95 BTreeNode *right = *iter_pc; 96 node->keys.push_back(*iter_pk); 97 node->keys.insert(node->keys.end(), right->keys.begin(), right->keys.end()); 98 node->childs.insert(node->childs.end(), right->childs.begin(), right->childs.end()); 99 node->nums = node->nums + 1 + right->nums; 100 parent->keys.erase(iter_pk); 101 parent->childs.erase(iter_pc); 102 iter_pc--; 103 parent->nums--; 104 for(Childs_Iter iter_c = right->childs.begin(); iter_c != right->childs.end(); iter_c++){ 105 (*iter_c)->parent = node; 106 } 107 } 108 if(parent->nums < up - 1) 109 adjustLess(parent); 110 } 111 112 void BTree::adjustLess(BTreeNode *node){//调整 113 if(node == root){ // root为空 114 if(node->nums) return; 115 adjustLessRoot(); 116 return; 117 } 118 adjustLessBrother(node); // 情况2 119 } 120 121 void BTree::_removeExternal(int key, BTreeNode *node){// 情况2 122 if(node == root && root->nums == 1){ 123 node->keys.clear(); 124 node->childs.clear(); 125 node->nums = 0; 126 return; 127 } 128 list<int>::iterator iter_k = node->keys.begin(); 129 list<BTreeNode*>::iterator iter_c = node->childs.begin(); 130 while(*iter_k != key && iter_k != node->keys.end()){ 131 iter_k++; 132 iter_c++; 133 } 134 node->keys.erase(iter_k); // 默认情况2a 135 node->childs.erase(iter_c); 136 node->nums--; 137 if(node->nums < up - 1){// 转至情况2b,2c 138 adjustLess(node); 139 } 140 } 141 142 BTreeNode* BTree::_getMax(BTreeNode *node){ 143 if(node->childs.front()->nums == 0) return node; 144 return _getMax(node->childs.back()); 145 } 146 void BTree::remove(int key){// remove函数 147 BTreeNode *node = find(key); 148 if(!isLeaf(node)){ 149 list<int>::iterator iter_k = node->keys.begin(); 150 list<BTreeNode*>::iterator iter_c = node->childs.begin(); 151 while(*iter_k != key && iter_k != node->keys.end()){ 152 iter_k++; 153 iter_c++; 154 } 155 if(isLeaf(*iter_c)){ 156 _removeExternal(key, node); // 情况2 157 }else{ 158 BTreeNode* max; 159 if(iter_k == node->keys.begin()){ // 与后继交换 160 iter_c++; 161 max = _getMax(*iter_c); 162 *iter_k = max->keys.front(); 163 _removeExternal(max->keys.front(), max); // 转为情况2 164 } 165 else{ // 与前驱交换 166 max = _getMax(*iter_c); 167 *iter_k = max->keys.back(); 168 _removeExternal(max->keys.back(), max); 169 } 170 } 171 } 172 }
(三)B+Tree
B+树是B树的改进版本,这里介绍一下它与B树的差别:
1. 所有的最底层内部节点结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针,且这些结点本身依关键字的大小自小而大的顺序链接。
2. 所有的非终端结点仅仅可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键字。 (而B 树的非终节点也包含需要查找的有效信息)
note: 为什么说B+-tree比B 树更适合实际应用中操作系统的文件索引和数据库索引?(这里引用July的博客)
1) B+-tree的磁盘读写代价更低
B+-tree的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B 树更小。如果把所有同一内部结点的关键字存放在同一盘块中,
那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。
2) B+-tree的查询效率更加稳定
由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。
所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
3)其他原因
数据库索引采用B+树的主要原因是 B树在提高了磁盘IO性能的同时并没有解决元素遍历的效率低下的问题。正是为了解决这个问题,B+树应运
而生。B+树只要遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作(或者说效率太低)。
(四)B*Tree
B*-tree是B+-tree的变体,在B+树的基础上(所有的最底层结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针),B*树中非根和
非最底层结点再增加指向兄弟的指针;B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3(代替B+树的1/2)