B树
数据库的索引大多用B+树实现,要了解B+树,我们必须先了解什么是B-树?
首先要清楚的是,B-树不能叫做B减树,否则可就让人笑掉大牙了,所以,后文中我们直接用作B树。
之前我们讲过,二叉搜索树的效率是O(log2^N),那为何数据库中不用二叉搜索树来作为索引呢?此时我们必须考虑到磁盘IO。数据库索引是存储在磁盘上的,当数据量比加大 的时候,索引的大小可能有几个G甚至更多。当我们利用索引查询的时候,嗯呢该吧整个索引都加载到内存中吗?显然是不可能的,能做的只能是逐一加载每一个磁盘页,这里的磁盘页对应着索引树的节点。举个栗子:
要在上面这棵二叉搜索树中查找10这个节点。
第一次IO:
第二次IO:
第三次IO:
第四次IO:
我们可以发现,在最坏的情况下,磁盘IO的次数等于这棵索引树的高度,为了减少磁盘IO的次数,我们需要让这棵树“降高度”,B树就是让这种“瘦高”的搜索树变成“矮胖”,从而减少磁盘IO的次数,提高搜索效率。
B树的性质
B树是一种用于外查找的多路平衡搜索树。
一棵M阶的B树:
1. 根节点至少有两个孩子,【2,M】
2. 每个非根节点有【M/2,M】个孩子
3. 每个非根节点有【M/2-1,M-1】个关键字,并且以升序排列
4. 每个节点孩子的数量比关键字的数量多一个
5. 所有的叶子节点都在同一层
6. key[i]和key[i+1]之间的孩子节点的值介于key[i]、key[i+1]之间
假如我们要加下面这棵B树中查找5这个节点:
第一次磁盘IO,在内存中定位,和9比较:
第二次磁盘IO,在内存中定位,和2,6比较:
第三次磁盘IO,在内存中定位,和3,5比较:
我们可以看出,B树在查询过程中的比较次数其实不比二叉查找树少,尤其当单一节点中的元素数量很多时。可是,相比于磁盘IO的速度,内存中的比较耗时几乎可以忽略。所以只要树的高度足够低,IO次数足够找,就可以提升性能。相比之下内部元素很多也没有关系,仅仅是多了几次内存交互,只要不超过磁盘页大小即可。
B树的插入:
B树的插入只能在叶子节点,且当节点中的关键字满了,要及逆行分裂操作。用上面的B树举例:
在叶子节点插入:
第一次分裂:
第二次分裂:
B树的删除:
当删除一个导致该树不符合B树的特性时,要进行左旋操作。比如,要删除下面B树的11这个节点,删除后,12只有一个孩子,不符合B树,此时,我们找出12,13,15这三个树的中位数13,取代节点12,经过左旋12成为第一个孩子。
下面给出B树的结构和插入操作:
#include<iostream>
using namespace std;
template<class K, class V, size_t M>
struct BTreeNode
{
//多开一个空间,方便分裂
pair<K, V> _kvs[M];//关键字数组
BTreeNode<K, V, M>* _subs[M+1];//孩子节点
BTreeNode<K, V, M>* _parent;//三叉
size_t size;
BTreeNode()
:_parent(NULL)
, size(0)
{
for (size_t i = 0; i < M+1; ++i)
{
_subs[i] = NULL;
}
}
};
template<class K,class V,size_t M>
class BTree
{
typedef BTreeNode<K, V, M> Node;
public:
BTree()
:_root(NULL)
{}
pair<Node*, int> Find(const K& key)
{
//要返回这个节点和在这个节点中的位置
Node* cur = _root;
Node* parent = NULL;
while (cur)
{
size_t i = 0;
while (i < cur->size)
{
//在当前位置的左树
if (cur->_kvs[i].first > key)
break;
else if (cur->_kvs[i].first < key)
{
++i;
}
else
return make_pair(cur, i);
}
//在左树或是没找到
parent = cur;
cur = cur->_subs[i];
}
return make_pair(parent, -1);
}
void InSertKV(Node* cur, const pair<K, V>& kv, Node* sub)
{
int end = cur->size - 1;
while (end >= 0)
{
if (cur->_kvs[end].first > kv.first)
{
//左子树的下标是与当前节点下标相同,右子树的下标是当前节点坐标+1
cur->_kvs[end + 1] = cur->_kvs[end];
cur->_subs[end + 2] = cur->_subs[end + 1];
--end;
}
else
{
break;
}
}
//end<0或kv.first>cur_kvs[end].first
cur->_kvs[end + 1] = kv;
cur->_subs[end + 2] = sub;
if (sub)
sub->_parent = cur;
cur->size++;
}
Node* Divided(Node* cur)
{
Node* newNode = new Node;
int mid = (cur->size) / 2;
size_t j = 0;
size_t i = mid + 1;
for (; i < cur->size; ++i)
{
newNode->_kvs[j] = cur->_kvs[i];
newNode->_subs[j] = cur->_subs[i];
if (newNode->_subs[j])
newNode->_subs[j]->_parent = newNode;
newNode->size++;
j++;
}
//右孩子还没拷
newNode->_subs[j] = cur->_subs[i];
if (newNode->_subs[j])
newNode->_subs[j]->_parent = newNode;
return newNode;
}
bool InSert(const pair<K, V>& kv)
{
//节点为NULL直接插入
if (_root == NULL)
{
_root = new Node;
_root->_kvs[0] = kv;
_root->size = 1;
return true;
}
//找到相同值返回false,没找到返回true,节点的关键字满了就进行分裂
pair<Node*, int> ret = Find(kv.first);
if (ret.second >= 0)
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;
else
{
//需要分裂
Node* newNode = Divided(cur);
pair<K, V> midKV = cur->_kvs[(cur->size) / 2];
//根节点分裂
cur->size -= (newNode->size + 1);
if (cur == _root)
{
_root = new Node;
_root->_kvs[0] = midKV;
_root->size = 1;
_root->_subs[0] = cur;
_root->_subs[1] = newNode;
cur->_parent = _root;
newNode->_parent = _root;
return true;
}
else
{
sub = newNode;
newKV = midKV;
cur = cur->_parent;
}
}
}
}
void InOrder()
{
_InOrder(_root);
}
protected:
void _InOrder(Node* root)
{
if (root == NULL)
return;
Node* cur = root;
size_t i = 0;
for (; i < cur->size; ++i)
{
_InOrder(root->_subs[i]);
cout << cur->_kvs[i].first << " ";
}
_InOrder(cur->_subs[i]);
}
private:
Node* _root;
};
void Test()
{
int a[] = { 53, 75, 139, 49, 145, 36, 101 };
int sz = sizeof(a) / sizeof(a[0]);
BTree<int, int, 3>bt;
for (size_t i = 0; i < sz; ++i)
{
bt.InSert(make_pair(a[i],i));
}
bt.InOrder();
}
B树主要应用于文件系统以及部分数据库索引,比如著名的非关系型数据库MongoDB,而大部分关系型数据库,比如Mysql,则使用B+树作为索引。
B+树
B+树的大体特征与B树相似,但B+树有自己的特性:
1.有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。
2.所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
3.所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。
在B树中,所有的节点都携带数据,但在B+树中,只有叶子节点中有数据,中间节点仅仅是索引,没有任何数据关联。
由于B+树的中间节点上没有数据,所以,同样大小的磁盘也可以容纳更多的节点元素,这就意味着,数据量相同的情况下,B+树的结构哦比B树更加“矮胖”,因此查询时IO的次数也更少。其次,B+树的查询必须查找到叶子节点,而B树是只要找到匹配元素即可,无论是中间节点还是叶子节点,因此,B树的查找性能并不稳定,最好情况是查找到根节点,最坏情况是查找到叶子节点,而B+树的查询时稳定的,每一次都是查找到叶子节点。B树对节点的遍历只能是繁琐的中序遍历,而B+树的遍历值需要对叶子节点的链表进行遍历即可。
总结一下,B+树相对于B树的优势有三个:
1.单一节点存储更多的元素,使得查询的IO次数更少。
2.所有查询都要查找到叶子节点,查询性能稳定。
3.所有叶子节点形成有序链表,便于范围查询。