"一条路踏过太多日落,一封信写下太多如果。"
一、初识B树
(1)B树系列为何而来?
常见的搜索结构:
种类 | 数据格式 | 时间复杂度O() |
顺序查找(遍历) | 无要求 | O(N) |
二分查找 | 有序 | O(log_2 N) |
二叉搜索树 | 无要求 | O(N) |
二叉平衡搜索树(AVL \ 红黑树) | 无要求 | O(log_2 N) |
哈希 | 无要求 | O(1) |
很多时候,数据量比较小时,我们可以直接 把数据量一次性放入 内存中,并进行的数据的查找。然而,如果是数据量很大(100G)的情况下呢? 很显然此刻,再也无法一次性将数据放入 读取到内存中,以供查找,而是只能放到磁盘上了。
如何解决呢?
我们可以通过 建立存放的关键字 与 磁盘地址的映射关系。从而通过关键字拿到 存放数据的磁盘地址,访问在磁盘中的数据。
如何理解磁盘的IO流?
然而事实上,磁盘(外存)具有比主存(内存)更大的容量,它的代价就在于IO流效率的损失。
因此,对于“海量数据”的查找,最根本上的在于 “减少磁盘的IO交互”。
为什么别的搜索结构不行?
从根本上,B树系列是继承了 平衡搜索二叉树部分 高查找效率的精华。
可以形象地理解为,将整颗树压扁,而不是像二叉树那样变高。 B树系列更多的感觉就是一种“矮(高度)、胖(多叉)” 。
①压缩高度、二叉变多叉
②一个节点里,存放多个关键字的映射关系。
-----------------前言
二、认识B树
R.Bayer和E.mccreight提出了一种适合外查找的树,它是一种平衡的多叉树,称为B树.
它的优化版本有 B+树 \ B*树B+树各位肯定不会陌生,因为这与我们数据库的索引联系密切。
因此从根本上,B树系列是适合 外查找(磁盘、光盘、U盘)的平衡多叉树。
(1)B树性质
1. 根节点至少有两个孩子。
2. 每个分支节点都包含k-1个关键字和k个孩子,其中 ceil(m/2) ≤ k ≤ m ceil是向上取整函数。
3. 每个叶子节点都包含k-1个关键字,其中 ceil(m/2) ≤ k ≤ m。4. 所有的叶子节点都在同一层。
5. 每个节点中的关键字从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的至于划分
6.每个结点的结构为:(n,A0,K1,A1,K2,A2,… ,Kn,An)其中,Ki(1≤i≤n)为关键
字,且Ki<Ki+1(1≤i≤n-1)。Ai(0≤i≤n)为指向子树根结点的指针。且Ai所指子树所有结点中的
关键字均小于Ki+1。
n为结点中关键字的个数,满足ceil(m/2)-1≤n≤m-1。
这什么鬼?看得懂?
答案是仅仅这样 读完一遍,恐怕也就只有 仙人才懂得了这其中的奥秘。
①节点关系
②存储位置
(2)B树的插入分析
是否有那么一点点的启发?好像是懂那么一点点了。下面来看看 B树是如何实现插入的。
①巧妙设计
②分裂与提取
分裂+提取:
1.创建一个兄弟节点,并以mid为界限 拷贝一半关键字给兄弟。
2.提取mid关键字 给父亲。 如果没有父亲 就 需要去 创建父亲节点。
(3)B树插入的实现
单从B树设计者的图 画出来,就足以展现其巧妙之处。 恐怕也对 B树插入有一点点的理解了。
①基本结构;
template<class K,size_t M>
struct BTreeNode
{
K _keys[M];
BTreeNode<K, M>* _subs[M +1];
BTreeNode<K, M>* _parent;
size_t _n;
BTreeNode()
{
for (size_t i = 0;i < M;++i)
{
_keys[i] = K();
_subs[i] = nullptr;
}
_subs[M] = nullptr;
_parenet = nullptr;
_n = 0;
}
};
template<class K,size_t M>
class BTree
{
typedef BTreeNode<K, M> BTree;
private:
BTree* _root = nullptr;
};
②查找
查找是一棵搜索树核心的步骤。
pair<Node*,int> FindRoot(const K& key)
{
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
size_t i = 0;
while (i < cur->_n)
{
if (key > cur->_keys[i])
{
i++;
}
else if (key < cur->_keys[i])
{
break;
}
else
{
return make_pair(cur, i);
}
}
parent = cur;
cur = cur->_subs[i];
}
return make_pair(parent, -1);
}
③插入
有了以上的铺垫我们 进行B树实现的核心。
第一步判定是否root 没有节点:
第二步 判定关键字是否存在:
第三步:插入函数
void _insert(Node* parent, const K& key, Node* child)
{
int end = parent->_n - 1;
while (end >= 0)
{
if (key < parent[end])
{
parent->_keys[end + 1] = parent->_keys[end];
parent->_subs[end + 2] = parent->_subs[end + 1];
--end;
}
else
{
break;
}
}
parent->_keys[end + 1] = key;
parent->_subs[end + 2] = child;
if (child)
{
child->_parent = parent;
}
parent->_n++;
}
第四步:插入 满时 如何进行 分裂?
Node* parent = ret.first;
K newkey = key;
Node* child = nullptr;
while (1)
{
_insert(parent, newkey, child);
if (parent->_n < M)
{
return true;
}
else
{
size_t mid = M / 2;
Node* brother = new Node;
size_t p_i = mid + 1;
size_t b_j = 0;
for (;p_i <= M - 1;++p_i)
{
brother->_keys[b_j] = parent->_keys[p_i];
brother->_subs[b_j] = parent->_subs[p_i];
if (parent->_subs[p_i])
{
parent->_subs[p_i]->parent = brother;
}
b_j++;
parent->_keys[p_i] = K();
parent->_subs[p_i] = nullptr;
}
brother->_subs[b_j] = parent->_subs[p_i];
if (parent->_keys[p_i])
{
parent->_keys[p_i]->parent = brother;
}
parent->_subs[p_i] = nullptr;
brother->_n = b_j;
parent->_n -= b_j + 1;
K midkey = parent->_keys[mid];
parent->_keys[mid] = K();
if (parent->_parent == nullptr)
{
_root = new Node;
_root->_keys[0] = midkey;
_root->_subs[1] = parent;
_root->_subs[2] = brother;
_root->_n = 1;
parent->_parent = _root;
brother->_parent = _root;
break;
}
else
{
newkey = midkey;
child = brother;
parent = parent->_parent;
}
}
}
测试:
void _InOrder(Node* cur)
{
if (cur == nullptr)
return;
size_t i = 0;
for (; i < cur->_n; ++i)
{
_InOrder(cur->_subs[i]); // 左子树
cout << cur->_keys[i] << " "; // 根
}
_InOrder(cur->_subs[i]); // 最后的那个右子树
}
void InOrder()
{
_InOrder(_root);
}
三、浅谈B树系列
在后来,为了让B树的理解变得简单,对B树的规则 进行了一些的修改。从而衍生出了,其他B树的优化版本。
(1)B树其他系列
①B+树
B+树是B树的变形,是在B树基础上优化的多路平衡搜索树,B+树的规则跟B树基本类似:
B树规则的以下变动:
1.分支节点的子树指针与关键字个数相同.
2.分支节点的子树指针p[i]指向关键字值大小在[k[i],k[i+1])区间之间.
3.所有叶子节点增加一个链接指针链接在一起.4.所有关键字及其映射数据都在叶子节点出现.
B+树的特性:
1. 所有关键字都出现在叶子节点的链表中,且链表中的节点都是有序的。
2. 不可能在分支节点中命中。
3. 分支节点相当于是叶子节点的索引,叶子节点才是存储数据的数据层。
②B*树
B*树是B+树的变形,在B+树的非根和非叶子节点再增加指向兄弟节点的指针。
③分裂
B树系列的精华就在于 分裂。
B+树的分裂:
当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增加新结点的指针;B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针。
单次分裂:
多次分裂:
B*树的分裂:
当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针。
不管是B+ 还是B树,对待满节点的状态下,都是拷贝1/2 给兄弟节点,一定程度上,第一次分裂的情况下,空间的利用率是较低的。
反观B*树,B*树虽然分配新结点的概率比B+树要低,但空间使用率更高。
④B树系列的优缺点
B树:有序数组+平衡多叉树;
B+树:有序数组链表+平衡多叉树;
B*树:一棵更丰满的,空间利用率更高的B+树。
(2)B树系列的应用
索引:
B树系列常见的用法就是 用来做索引。
比如:书籍目录可以让读者快速找到相关信息,hao123网页导航网站,为了让用户能够快速的找到有价值的分类网站,本质上就是互联网页面中的索引结构。
MySQL官方对索引的定义为:索引(index)是帮助MySQL高效获取数据的数据结构,简单来说:索引就是数据结构。
mysql引擎 与 索引
mysql有两个常用的 存储引擎:MyiSam \ innodb;
它们的性质上的区别在于: 聚簇索引 、 非聚簇索引。
一个是数据、索引存在一起(聚簇索引)
一个是数据、索引 分开存储(非聚簇索引)
总结:
①B树系列适用的检索 场景。
②B树系列的 性质 决定 为什么是平衡树。
③B树系列的应用 innodb 、 myisam
本小篇也就到此结束了;感谢您的阅读
祝你好运~