🏆个人主页:企鹅不叫的博客
🌈专栏
- C语言初阶和进阶
- C项目
- Leetcode刷题
- 初阶数据结构与算法
- C++初阶和进阶
- 《深入理解计算机操作系统》
- 《高质量C/C++编程》
- Linux
⭐️ 博主码云gitee链接:代码仓库地址
⚡若有帮助可以【关注+点赞+收藏】,大家一起进步!
💙系列文章💙
【C++高阶数据结构】并查集
【C++高阶数据结构】图
【C++高阶数据结构】LRU
文章目录
- 💙系列文章💙
- 💎一、概念
- 🏆1.优点
- 🏆2.B树规则
- 💎二、B树插入
- 🏆1.插入过程
- 🏆2.代码实现
- 💎三、B+树
- 🏆1.概念
- 🏆2.分裂过程
- 💎四、B*树
- 🏆1.概念
- 💎五、B树系类应用
- 🏆1.MyISAM
- 🏆2.InnoDB
💎一、概念
🏆1.优点
B树适合外查找,当数据量很大,无法一次全部都放进内存的话,那就只能存在磁盘上,B树本质是一个多叉搜索树。
从树的根开始读取的话,我们需要读取树的高度次磁盘IO,多次进行磁盘读取,就会非常缓慢。每次要读取新的数据,要去定位这个过程是非常缓慢。
在平衡搜索树的基础上寻找优化方法
1.压缩高度,二叉变多插
2.一个结点里面存多行的值,也就是一个结点里面有多个关键字以及映射的值
🏆2.B树规则
一棵m阶(m>2)的B树,是一棵平衡的M路平衡搜索树,可以是空树或者满足一下性质:
- 根节点至少有两个孩子
- 每个分支节点都包含k-1个关键字和k个孩子,其中ceil(m/2) ≤ k ≤ m ceil是向上取整函数
- 每个叶子节点都包含k-1个关键字,其中 ceil(m/2) ≤ k ≤ m
- 所有的叶子节点都在同一层
- 每个节点中的关键字从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域划分
- 每个结点的结构为:(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。
假设现在我们的m是10
按照上面的规则,也就是说我们
最少需要4个关键字和5个孩子
最多需要9个关键字和10个孩子这个节点中的关键字按照从小到大的顺序进行排列
(n,A0,K1,A1,K2,A2,… ,Kn,An)
K1<K2<K3<…<Kn
A0节点中的值<K1<A1节点中的值<K2<……也就是说,如果比K1小,我们就在A0节点中继续寻找,如果比K2小比K1大,我们就在A1节点中进行查找。B树本质是一个多叉搜索树。
💎二、B树插入
🏆1.插入过程
假设M=3。
也就是最少存1个关键字,最多两个关键字,最少2个孩子,最多3个孩子
这里是我们的数据:{53, 139, 75, 49, 145, 36, 101}首先我们将53,139和75插入
(我们这里多开一个空间,便于我们的插入。否则我们的第m个元素插入的时候,也就是我们刚刚好越界的时候,我们就不知道插入在哪里,我们可能还要分情况进行讨论,这样就非常麻烦。多开辟一个空间的话,我们就可以先将这第M个元素先插进去,然后再进行分裂操作,就省去了分类讨论)关键字的数量等于M,那就是满了,满了就分裂,分裂出一个兄弟(兄弟里面最初始没有值),然后分一半的值给兄弟
插入49,145
再插入36的时候,我们左边的结点1就满了,我们又需要进行分裂
49是我们的中位数。
所以我们将49放入我们的父节点中
(我们的关键字要比我们孩子的数量少一个。现在我们有三个孩子和两个关键字)最右边的子树满了,进行持续分裂
B树天然平衡
因为它是向右和向上生长的
新插入的结点一定是在叶子插入的。叶子没有孩子,所以不会影响孩子和关键字的关系(孩子比关键字多一个)
叶子结点满了,就分裂出一个兄弟,提取中位数,向父亲插入一个值和一个孩子。
根节点分裂才会增加一层。
假设M=1024,那么一个4层的M路的B树可以存多少个值呢?
如果这棵树是全满的情况下
第一层1023个关键字,1024个孩子
第二层10241023个关键字(上一层的每一个孩子也就是这一层的每一个结点都有1023个关键字),10241024个孩子
第三层102410241023个关键字(上一层的每一个孩子也就是这一层的每一个结点都有1023个关键字),102410241024个孩子
第三层1024102410241023个关键字(上一层的每一个孩子也就是这一层的每一个结点都有1023个关键字),1024102410241024个孩子最差的情况,最空的情况:
第一层只有1个关键字,2个孩子
第二层有2 * 512个关键字,大概1000个关键字,1000个孩子
第三层大概1000 * 512个关键字,1000 * 512个孩子
第四层大概50w * 512个关键字,约等于2.5亿个关键字
🏆2.代码实现
template<class K, size_t M> struct BTreeNode { // 原本key是M-1个大小空间 // 原本孩子是M个大小空间 // 为了方便插入以后再分裂,多给一个空间 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; _parent = nullptr; _n = 0; } }; // 数据是存在磁盘,K是磁盘地址,是M路的搜索树,我们的M是不确定的 template<class K, size_t M> class BTree { typedef BTreeNode<K, M> Node; public: //返回这个节点和下标 pair<Node*, int> Find(const K& key) { Node* parent = nullptr; Node* cur = _root; while (cur) { // 在一个节点查找 size_t i = 0; while (i < cur->_n) { if (key < cur->_keys[i]) { break; } else if (key > cur->_keys[i]) { ++i; } //找到了就返回这个节点 else { return make_pair(cur, i); } } // 往孩子去跳 // 在往下一层跳之前先将当前的结点给parent parent = cur; cur = cur->_subs[i]; } //找不到 return make_pair(parent, -1); } void InsertKey(Node* node, const K& key, Node* child) { int end = node->_n - 1; while (end >= 0) { if (key < node->_keys[end]) { // 挪动key和他的右孩子 node->_keys[end + 1] = node->_keys[end]; node->_subs[end + 2] = node->_subs[end + 1]; --end; } else { break; } } node->_keys[end + 1] = key; node->_subs[end + 2] = child; if (child) { child->_parent = node; } node->_n++; } //插入 bool Insert(const K& key) { //第一次插入 if (_root == nullptr) { //如果我们整颗树一个结点都没有 _root = new Node; //将我们的第一个关键字传入 _root->_keys[0] = key; _root->_n++; return true; } // key已经存在,不允许插入 pair<Node*, int> ret = Find(key); if (ret.second >= 0) { return false; } // 如果没有找到,find顺便带回了要插入的那个叶子节点 // 循环每次往cur插入 newkey和child Node* parent = ret.first; K newKey = key; Node* child = nullptr; while (1) { InsertKey(parent, newKey, child); // 满了就要分裂 // 没有满,插入就结束 if (parent->_n < M) { return true; } else { size_t mid = M / 2; // 分裂一半[mid+1, M-1]给兄弟 Node* brother = new Node; size_t j = 0; size_t i = mid + 1; for (; i <= M - 1; ++i) { // 分裂拷贝key和key的左孩子 brother->_keys[j] = parent->_keys[i]; brother->_subs[j] = parent->_subs[i]; if (parent->_subs[i]) { parent->_subs[i]->_parent = brother; } ++j; // 拷走重置一下方便观察 parent->_keys[i] = K(); parent->_subs[i] = nullptr; } // 还有最后一个右孩子拷给 brother->_subs[j] = parent->_subs[i]; if (parent->_subs[i]) { parent->_subs[i]->_parent = brother; } parent->_subs[i] = nullptr; brother->_n = j; parent->_n -= (brother->_n + 1); K midKey = parent->_keys[mid]; parent->_keys[mid] = K(); // 说明刚刚分裂是根节点 if (parent->_parent == nullptr) { //创建一个新的父节点 _root = new Node; _root->_keys[0] = midKey; _root->_subs[0] = parent; _root->_subs[1] = brother; _root->_n = 1; parent->_parent = _root; brother->_parent = _root; break; } else { // 转换成往parent->parent 去插入parent->[mid] 和 brother newKey = midKey; child = brother; parent = parent->_parent; } } } return true; } 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); } private: Node* _root = nullptr; }; void TestBtree() { int a[] = { 53, 139, 75, 49, 145, 36, 101 }; BTree<int, 3> t; for (auto e : a) { t.Insert(e); } t.InOrder(); }
时间复杂度:
第一层:M
第二层:M * M
第三层:M * M * M
第四层:M * M * M * MN=M+M2 +M3 +M4 +……+Mh
h约等于
l o g M N log{M}^{N} logMN
💎三、B+树
🏆1.概念
1.分支节点的子树指针与关键字个数相同。(就相当于是取消掉了原先B树每个结点的最左边的那个孩子)
2.分支节点的子树指针p[i]指向关键字值大小在[k[i],k[i+1])区间之间
3.所有叶子节点增加一个链接指针链接在一起
4.所有关键字及其映射数据都在叶子节点出现】5.节点的关键字数量是[1, M],非根节点关键字数量为[M/2,M]
6.分支节点跟叶子结点有重复的值,分支节点存的是叶子结点的索引
7.父亲中存的是孩子结点中的最小值做索引
8.分支节点可以只存key,叶子结点存key/value
🏆2.分裂过程
假设这是一棵M == 3的B+树,然后我们B+树要插入的数据是
{53,139,75,49,145,36,101,150,155};1.依次插入53 139 75
2.插入19时发生裂变
3.插入146和36
4.插入101的时候发生第二次分裂
5.插入150,插入155的时候发生连续的两次分裂
B+树的插入过程根B树是基本类似的,区别在于第一次插入的时候需要插入两层节点,一层做分支,一层做根,后面一样往叶子去插入,插入满了以后,分一半给兄弟,转换成往父亲插入一个key和一个孩子,孩子就是兄弟,key为兄弟结点的第一个最小值的key
总结:
1.简化孩子比关键字多一个的规则,变成相等。
2.所有值都在叶子上,方便便利查找所有值。
💎四、B*树
🏆1.概念
- B树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3(代替B+树的1/2);
- B*树的分裂:当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了)。
- 如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针。 所以,B * 树分配新结点的概率比B+树要低,空间使用率更高。
- B*树主要就是节省空间。
💎五、B树系类应用
B树系列优点:
在内存中做内查找的话和哈希、平衡搜索树对比,单纯论树的高度,搜索效率而言,B树更好
B树系列缺点:
1.空间利用率低,消耗高
2.插入和删除数据、分裂和合并节点,那么必然挪动数据。
3.虽然高度更低,但是在内存中而言,跟哈希和平衡搜索树还是在一个量级的
结论:实质上B树系列在内存中体现不出优势。B树系列的应用:数据库中的引擎MyISAM或者InnoDB
🏆1.MyISAM
MyISAM引擎是MySQL5.5.8版本之前默认的存储引擎,不支持事务,支持全文检索,使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址
yISAM中索引检索的算法为首先按 照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为 地址,读取相应数据记录。
🏆2.InnoDB
InnoDB存储引擎支持事务,InnoDB支持B+树索引、全文索引、哈希索引。InnoDB索引,表数据文件本身就是按B+Tree组织的一个索 引结构,这棵树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。
先用name,name对应主键id,再用主键id再去搜索一次,也就是说他用索引查找需要查找两次
B树节点数据都在磁盘文件中。访问节点都是IO行为,只是他们会热数据缓存到Cache中