一、B树
- 1、B树的定义
是一种多路搜索树 2、B树的性质
一颗M阶(M>2)的B树,是一颗平衡的M路平衡搜索树,可以是空树或者满足以下性质:
1)根节点至少有两个孩子
2)每个非根节点有[M/2-1,M-1]个关键字
3)每个非根节点有[M/2,M]个孩子
4)key[i],key[i+1]孩子的值介于key[i]和key[i+1]之间
5)所有的叶子节点都在同一层3、B树如何实现
1)节点的定义
说明:
*B树其实也是搜索树,所以这里我们用k-v结构
template<class K,class V,size_t M = 3>
struct BTreeNode
{
pair<K,V> _kvs[M]; //值的个数
BTreeNode<K,V>* _subs[M+1]; //子树的个数
size_t _size; //kv的大小
BTreeNode<K,V>* _parent;
BTreeNode(const pair<K,V>& kvs = pair<K,V>())
{
_parent = NULL;
_size = 0;
for(int i = 0; i<M+1; i++)
{
_subs[i] = NULL;
}
}
};
2)B树的查找
- 说明:
*如果你查找的这个值比关键字小的话,那么就在孩子中进行查找
*如果你查找的这个值比关键字大的话,那么就向后查找
*否则就找到了
- 代码实现:
pair<Node* ,int>Find(const pair<K,V>& kv)
{
Node* cur = _root;
Node* parent = NULL;
if(_root == NULL)
return make_pair((Node*)NULL,-1);
while(cur)
{
size_t i = 0;
while(i<cur->_size)
{
//如果你寻找的这个值小于当前值的话
if(cur->_kvs[i].first > kv.first)
{
break;
}
else if(cur->_kvs[i].first < kv.first)
{
++i;
}
else
{
return make_pair(cur,1);
}
}
//到这里的话,就说明是要在子树中进行查找的
parent = cur;
cur = cur->_subs[i];
}
return make_pair(parent,-1);
}
3)B树的插入
- 图示说明:
- 代码实现:
bool Insert(const pair<K,V>& kv)
{
//先进行根节点的判断
if(_root == NULL)
{
_root = new Node;
_root->_kvs[0] = kv;
_root->_size++;
return true;
}
//在判断要插入的节点是否存在
pair<Node*,V> ret = Find(kv);
//如果要插入的节点已经存在的话,则直接返回假
if(ret.second != -1)
return false;
Node* cur = ret.first;
pair<K,V> newkv = kv;
Node* sub = NULL;
//否则要进行插入
while(1)
{
_InsertKV(cur,newkv,sub);
//如果插入值之后没有满的话,就直接返回,否则就进行分裂
if(cur->_size < M)
return true;
//到这一步进行分裂
Node* temp = new Node; //先创建一个节点
size_t mid = M/2;
size_t j = 0; //作为新分裂的节点的下标
size_t i = mid+1;
for(; i<cur->_size; i++)
{
temp->_kvs[j] = cur->_kvs[i];
cur->_kvs[i] = pair<K,V>();
temp->_subs[j] = cur->_subs[j];
if(cur->_subs[i])
cur->_subs[i]->_parent = temp;
temp->_size++;
++j;
}
//拷走最后一个右孩子
temp->_subs[j] = cur->_subs[i];
if(cur->_subs[i])
cur->_subs[i]->_parent = temp;
cur->_size = cur->_size-temp->_size-1;
//将分裂的节点kv和temp向父节点插入(向上调整)
//分两种情况:cur的父节点为空的时候和不为空的时候
if(cur->_parent == NULL)
{
_root = new Node;
_root->_kvs[0] = cur->_kvs[mid];
cur->_kvs[mid] = pair<K,V>();
_root->_size = 1;
cur->_size--; //在进行向上调整后要将原来节点的size--
_root->_subs[0] = cur;
_root->_subs[1] = temp;
cur->_parent = _root;
temp->_parent = _root;
return true;
}
else
{
newkv = cur->_kvs[mid];
sub = temp;
cur->_kvs[mid] = pair<K,V>();
cur = cur->_parent;
}
}
}
- 5、B树的中序遍历
- 1)说明:
因为B树也是搜索树的一种,所以它经过中序遍历后的结果就是升序
*我们先递归找到最左边的数,进行打印,重复进行
*最后我们再将最后一个孩子进行遍历 - 2)代码实现:
- 1)说明:
void _InOrder(Node* root)
{
if(root == NULL)
return;
Node* cur = root;
size_t i = 0;
for(; i<cur->_size; i++)
{
_InOrder(cur->_subs[i]);
cout<<cur->_kvs[i].first<<" ";
}
_InOrder(cur->_subs[i]);
}
- 5、B树的应用
用于数据库系统或者是文件系统的索引结构
二、B+树
- 1、定义
B+树是B树的变形
- 2、性质
性质和B树非常相似,除了下面几点:
1)B+树非叶子节点的指针个数和关键字的个数是相等的
2)非叶子节点的子树指针,指向关键字属于key[i],key[i+1]的子树
3)所有的叶子节点都增加一个链指针
4)所有的关键字都在叶子节点中出现
- 3、节点的定义
B+树由于是分非叶子节点和叶子节点的,所以,我们可以这样定义:
1)非叶子节点:
template<class K>
struct BPTreeNonLeafNode
{
K _key[M];
void* _subs[M];
BPTreeNonLeafNode<K>* _parent;
size_t _size;
};
2)叶子节点
template<class K,class V>
struct BPTreeLeafNode
{
pair<K,V> _kvs[M];
BPTreeLeafNode<K,V>* _next;
BPTreeLeafNode<K,V>* _parent;
size_t _size;
};
4、B+树的特性:
区别是B+树只有达到叶子结点才命中(B-树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找。
*.所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的;
*.不可能在非叶子结点命中;
*.非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层
*.更适合文件索引系统;
5、B+树的应用
用于数据库系统和文件系统的索引
它的效率是优于B树的
三、B*树
- 1、定义
是B+树的变形 - 2、性质
1)在B+树的非根和非叶子结点再增加指向兄弟的指针;
2) B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3(代替B+树的1/2); 3、B*树的分裂
B+树的分裂:当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增加新结点的指针;B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针;
B*树的分裂:当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针;
所以,B*树分配新结点的概率比B+树要低,空间使用率更高;
四、B树,B+树,B*树的总结
1)疑问1:为什么要使用B树或者B+树来作为索引结构?
A:对于自身原因:同样的高度,红黑树存储的节点比B树少很多
B:数据库设计者利用磁盘预读原理,将一个节点的大小设为一个页,这样每个节点只需要进行一次I/O就可以
C:每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了一个node只需一次I/O。
B-Tree中一次检索最多需要h-1次I/O(根节点常驻内存),渐进复杂度为O(h)=O(logdN)O(h)=O(logdN)。一般实际应用中,出度d是非常大的数字,通常超过100,因此h非常小(通常不超过3)。
综上所述,用B-Tree作为索引结构效率是非常高的。
而红黑树这种结构,h明显要深的多。由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,所以红黑树的I/O渐进复杂度也为O(h),效率明显比B-Tree差很多。
2)B树和B+树的区别:
B和B+树的区别在于,B+树的非叶子结点只包含导航信息,不包含实际的值,所有的叶子结点和相连的节点使用链表相连,便于区间查找和遍历。
B+ 树的优点在于:
由于B+树在内部节点上不好含数据信息,因此在内存页中能够存放更多的key。 数据存放的更加紧密,具有更好的空间局部性。因此访问叶子几点上关联的数据也具有更好的缓存命中率。
B+树的叶子结点都是相链的,因此对整棵树的便利只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。
但是B树也有优点,其优点在于,由于B树的每一个节点都包含key和value,因此经常访问的元素可能离根节点更近,因此访问也更迅速
五、MySql的两种存储引擎
1、MyISAM:非聚集性索引
1)索引结构
2)说明:
*索引文件和数据文件分离,叶节点的数据域存放的是数据记录的地址
*主索引和辅助索引的区别是主索引的key是唯一的,辅助所以的key是可以重复的
*如何进行检索:
先利用B+树搜索算法进行索引(key存在的时候)— 取出数据域的值(地址),根据这个值进行读取相应的数据记录
2、INNODB:聚集性索引
1)索引结构
2)说明:
*数据文件本身就是索引文件,叶节点保存的是完整的数据记录
* 因为InnoDB的数据文件本身要按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有),如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整形。
* 第二个与MyISAM索引的不同是InnoDB的辅助索引data域存储相应记录主键的值而不是地址,即 InnoDB的所有辅助索引都引用主键作为data域。
4)聚集索引这种实现方式使得按主键的搜索十分高效,但是辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。
了解不同存储引擎的索引实现方式对于正确使用和优化索引都非常有帮助,例如知道了InnoDB的索引实现后,就很容易明白为什么不建议使用过长的字段作为主键,因为所有辅助索引都引用主索引,过长的主索引会令辅助索引变得过大。再例如,用非单调的字段作为主键在InnoDB中不是个好主意,因为InnoDB数据文件本身是一颗B+Tree,非单调的主键会造成在插入新记录时数据文件为了维持B+Tree的特性而频繁的分裂调整,十分低效,而使用自增字段作为主键则是一个很好的选择。
注:请尽量在InnoDB上采用自增字段做主键。
3)那么这两种引擎到底使用哪一个更好?
从历史上来说MyISAM历史更加久远,所以InnoDB性能也就更好了,在这我们需要考虑当我们修改数据库中的表的时候,数据库发生了变化,那么他们的主键的地址也就发生了变化,这样你的MyISAM的主索引和辅助索引就需要进行重新建立索引。而InnoDB只需要改变主索引,因为它的辅助索引是存主键的。所以这样考虑InnoDB更加高效。