以下是阿鲤对B树的学习总结,希望可以帮助到大家。
一:B树的介绍
1:为什么需要B树
2:B树的规则
二:B树的实现
1:图解
2:富含详细注释的代码
一:B树的概念
1:为什么需要B树
我想大家都知道AVL树,红黑树数据结构,他们都可以高效的对数据进行搜索,那么为什么该需要B树呢?
这是因为上述的数据结构都是在内存中使用的,那么当我们的数据量过大,内存中存储不下的时候应该怎么办呢?
对于这样的数据我们就可以放在硬盘里面了,对于存在硬盘中的数据使用以上的数据结构进行搜索就会需要多次的硬盘读写,我们都知道硬盘的读取速度和内存相比是相当慢的。
那么!这就到了B树闪亮登场了;它就是用来解决其它数据结构不能处理巨量数据和磁盘读写太多的问题的。
2:B树的规则
B树是一种 - 平衡的多叉搜索树,有的地方会写成B-树,但是大家千万不要读成B减树。一颗M阶的B树,是一颗平衡的M路平衡搜索树。可以是空树,它满足下面这些性质。
1:根节点至少有两个孩子:[2, M]
2:每个非根节点至少有M/2 个孩子(向上取整),至多M个孩子:[M/2, M]
3:每个非根节点至少有M/2 - 1 个关键字(向上取整),至多有M - 1 个关键字,并且以升序排列:[M/2 - 1, M - 1]
4:key[i] 和 key[i + 1] 之间的孩子节点的值介于key[i]和key[i+1] 之间
5:所有的叶子节点都在同一层
大家看着上面这个图,我来给大家介绍一下这个数据结构;大家可以看到每一个节点,孩子的数量都比key多一个,这就符合了1,2,3的性质;然后可以看到36 < 49;53 > 49 && 75 > 49 这就符合了4性质,然后5性质是显而易见的平衡树。
B树也就是数据库的一种搜索结构,一般数据库中的B树的第一层有1023个key,那么第二层就有1024*1024个key;第三层就有1024 * 1024 * 1024个数据了;这样对于十亿数量级的数据,我们在访问呢磁盘的时候只需要访问三次就可以。但是对于红黑树,对于十亿个数据就需要32次磁盘读写了;这就大大减少了磁盘读取的数量;加快了搜索速度。
如果你对上述概念还不理解,请看 下面的图解过程。
二:B树的实现
1:图解
我们在这里使用7,9,17,5,4,3,13,6构造一个4路平衡的B树;
假设我们现在有一个4路的B树,我们依次插入上述数据(插入排序的方式进行插入)
ok,重要的地方来了,当一个节点的数据存储慢了之后就需要进行分裂了。
分裂过程:
仔细的同学可以看出,整个分裂过程都是遵循B树的原则的,当一个节点中的元素等于4个的时候,其就违背了规则3,所以需要分裂出兄弟节点。而且你会发现分裂出的父亲节点,也是符合规则的,其只有1个关键字,但是有2个儿子节点,所以其是符合规则1,2的
我们接着插入。
注意:每次的插入都是以插入排序的方式进行插入,按照顺序,遵循规则4;我们在插入的时候4,3,6是小于7的插在左节点;13是大与7的插在右节点;(不能直接往父节点插入,会破坏规则)。
ok,我们的图解已经演示完成了,接下来让我们看看代码的实现
总结:
对于一个M阶的B树而言,其插入的时间复杂度为M*(h-1)*(M-1)(h是B树的高度)。其只会访问h次磁盘,所以相较于红黑树,其访问磁盘的效率会非常高;因为B树在不满的情况下空间利用率是比较低的,而且需要挪到数据,所以在内存中其访问速度就不如红黑树了。
2:富含详细注释的代码
简单的模拟B树的插入实现
#pragma
#include<iostream>
template<class K, int M = 3>//M个孩子
struct BTreeNode{
BTreeNode<k, M> * m_subs[M + 1];//M + 1个孩子,方便分裂
K m_kyes[M];//M个key,未来方便插入之后分裂,所以将其设置为M个,而不是M-1个;
BTree<K, M> *m_parent; //父亲节点
size_t m_key_szie; //记录关键字的数量
BTreeNode() {
for (size_t i = 0; i < M; ++i) {
m_subs[i] = nullptr;
}
m_parent = nullptr;
m_key_szie = 0;
}
};
template<class K, int M>
class BTree {
typedef BTreeNode<K, M> Node;
//若不存在,返回节点和对应的下标,下标为-1表示不存在,否则存在。
std::pair<Node*, int>(Find(const K& key) {
Node *parent = nullptr;
Node *cur = m_root;
while (cur) {
int i = 0;
while (i < cur->m_key_szie) {
//比关键字小,到关键字的左孩子中查找
if (key < cur->m_kyes[i]){
parent = cur;
cur = cur->m_subs[i];//左孩子的下标和关键字的下标相同
continue;
}
//比关键字大
else if (key > cur->m_kyes[i]){
++i;
}
else{
parent = cur;
return std:make_pair(cur, i);
}
}
//到最后一个右孩子去找
cur = cur->m_subs[i + 1];//有孩子下标比关键字下标+1
}
return std::make_pair<parent, -1>;//parent就是要插入的节点
}
//相当于插入排序
void InsertKey(Node *node, K &key, Node *child) {
int end = node->m_key_szie - 1;
while (end >= 0 && node->m_kyes[end] > key) {
node->m_kyes[end + 1] = node->m_kyes[end];
--end;
}
node->m_kyes[end + 1] = key;
node->m_subs[end + 2] = node;
if (nullpr != child) {
child->m_parent = node;
}
node->m_key_szie++;
}
public:
bool Insert(const K& key) {
//如果根节点为空
if (root == nullptr) {
m_root = new Node;
m_root->m_kyes[0] = key;
m_root->m_key_szie = 1;
return true;
}
//如果这个值已经存在
std::pair<Node*, int> ret = Find(key);
if (ret.second != -1) {
return false;
}
//在对应的节点插入数据
Node *node = ret.first;
K k = key;
Node *child = nullptr;
//下面的循环逻辑是往node中插入k和child
while(true){
InsertKey(node, key, child);
if (node->m_key_szie < M) {
return true;
}
//m_key_size == M, 就需要分裂
else {
int mind = M / 2;
//将右半区间分裂拷贝到新节点中(拷贝关键字和孩子)
Node *newnode = new Node;
int j = 0;
int i = mid + 1;
for (; i < M; ) {
newnode->m_kyes[j] = node->keys[i];
newnode->m_subs[i] = node->m_subs[i];
if (node->m_subs[i]) {
node->m_subs[i]->parent = newnode;
}
node->m_kyes[j] = K();
node->m_subs[i] = nullptr;
newnode->m_key_szie++;
++j;
++i;
}
newnode->m_subs[j] = node->m_subs[i];
if (node->m_subs[i] != nullptr) {
node->m_subs[i]->m_parent = newnode;
}
node->m_key_szie -= (newnode->m_key_szie + 1);
//把node的一个关键字分裂(mid)出来,放入parent中
if (node->m_parent == nullptr) {
m_root = new Node;
m_root->m_kyes[0] = node->m_kyes[mid];
m_root->m_subs[0] = node;
m_root->m_subs[1] = newnode;
return true;
}
//迭代转换往node节点的parent中去插入一个node->key[mid]和newnode
else {
key = node->m_kyes[mid];
child = newnode;
node = node->m_parent;
}
}
}
return true;
}
private:
Node *m_root;
};