B树系列常用作查找使用(外查找)
B树使用场景:
数据量很大,无法直接讲数据放入内存中,这些数据在磁盘上。
B树的结构中,树中的节点保存的是数据在磁盘中的位置。
但是如果B树是类似AVL树,红黑树或哈希表的话,涉及大量的访问磁盘操作,效率太低。
B树采用的是优化AVL树的方式提高效率
一棵m阶(m>2)的B树,是一棵平衡的M路平衡搜索树,可以是空树或者满足一下性质:
eg:三阶B树插入关键字(53, 139, 75, 49, 145, 36, 50, 47, 101)
节点最多保存2个关键字,最少保存1个关键字。根节点单独看
这次分裂会导致两次连续分裂
第一次分裂导致根节点满
继续分裂根节点,产生新的根节点
插入完毕
特点
#pragma once
#include
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;
_n = 0;
_parent = nullptr;
}
};
//数据如果存在磁盘上,K是磁盘地址
template<class K, size_t M>
class BTree {
typedef BTreeNode<K, M> Node;
public:
//查找要插入的叶子节点对应的下标
std::pair<Node*, int>Find(const K& key) {
Node* par = nullptr;
Node* cur = _root;
while (cur) {
//在一个节点中查找
size_t i = 0;
while (i < cur->_n) {
if (key < cur->_keys[i]) {
//_key[size]的左孩子
break;
}
else if (key > cur->_keys[i]) {
i++;
}
else {
return std::make_pair(cur, i);
}
}
par = cur;//记录cur的父节点
cur = cur->_subs[i];
}
return std::make_pair(par, -1);
}
bool Insert(const K& key) {
if (_root == nullptr) {
_root = new Node;
_root->_keys[0] = key;
_root->_n++;
return true;
}
std::pair<Node*,int> ret = Find(key);
if (ret.second >= 0) {
//不允许冗余
return false;
}
//如果没有找到,Find函数返回要插入的叶子节点
Node* cur = ret.first;
K newKey = key;
Node* child = nullptr;
while (true) {
InsertKey(cur, newKey, child);
if (cur->_n == M) {
//这个节点满了,需要分裂
size_t mid = M / 2;
//node中有[mid+1,M-1]的数据
Node* node = new Node;
size_t pos = 0;
//同时还要拷贝孩子节点
for (int i = mid + 1; i < M; i++) {
node->_keys[pos] = cur->_keys[i];
node->_subs[pos] = cur->_subs[i];
if (cur->_subs[i] != nullptr) {
//更新父节点
cur->_subs[i]->_parent = node;
}
pos++;
cur->_keys[i] = K();//方便观察
cur->_subs[i] = nullptr;
}
//最后一个子节点拷贝
node->_subs[pos] = cur->_subs[M];
if (cur->_subs[M] != nullptr) {
//更新父节点
cur->_subs[M]->_parent = node;
}
cur->_subs[M] = nullptr;
node->_n = pos;
cur->_n -= pos + 1;//还要提取一个节点作为这两个节点的父节点
K midKey = cur->_keys[mid];
cur->_keys[mid] = K();//方便调试观察
//向cur->parent插入cur->_keys[mid]和node节点
if (cur->_parent == nullptr) {
//分裂根节点
_root = new Node;
_root->_keys[0] = midKey;
_root->_subs[0] = cur;
_root->_subs[1] = node;
_root->_n = 1;
cur->_parent = _root;
node->_parent = _root;
break;
}
newKey = midKey;
child = node;
cur = cur->_parent;//while循环插入
}
else {
//节点没有满,插入结束
return true;
}
}
return true;
}
//中序遍历
void Inorder() {
_Inorder(_root);
}
private:
void _Inorder(Node* root) {
if (root == nullptr) {
return;
}
for (size_t i = 0; i < root->_n; i++) {
_Inorder(root->_subs[i]);
std::cout << root->_keys[i] << " ";
}
//最后还剩余一个右子树
_Inorder(root->_subs[root->_n]);
}
void InsertKey(Node* cur, const K& key, Node* child) {
int endPos = cur->_n - 1;
while (endPos >= 0) {
if (key < cur->_keys[endPos]) {
//挪动key和右孩子
cur->_keys[endPos + 1] = cur->_keys[endPos];
cur->_subs[endPos + 2] = cur->_subs[endPos + 1];
endPos -= 1;
}
else {
break;
}
}
cur->_keys[endPos + 1] = key;
cur->_subs[endPos + 2] = child;
if (child != nullptr) {
child->_parent = cur;
}
cur->_n += 1;
}
Node* _root = nullptr;
};
测试代码:
#include"BTree.h"
int main() {
int arr[] = { 53, 139, 75, 49, 145, 36, 50, 47, 101 };
BTree<int, 3>bTree;
for (auto& e : arr) {
bTree.Insert(e);
}
bTree.Inorder();
return 0;
}
时间复杂度:
设B树高度为h
第一层B树保存关键字个数大致为m个
第二层B树保存关键字个数大致为m^2
设B树总关键字个数为N
N=m + m ^ 2 + m ^ 3 +……+m ^ h
计算得到
-m+m^(h+1)=N*m-N
最终得到
h+1=logm[(N*m)-N+m]
其中N远大于m所以最终B树的时间复杂度为
O(logm(N))
B+树是B树的变形,是在B树的基础上优化的多路平衡搜索树,B+树的规则与B树基本类似。
B+树优化的规则如下
分支节点和叶子节点又重复的值,分支节点保存叶子节点的索引
父节点保存子节点最小值作为索引
对于Key-Value形,子节点保存key值,叶子节点保存value值
三阶B树插入关键字(53, 139, 75, 49, 145, 36, 101)
插入53,139,75的过程
B*树是B+树的变形,在B+树的非根和非叶子节点再增加指向兄弟节点的指针
B*树想提高空间利用率
所以,分裂后原节点,兄弟节点,新节点各个都有2/3的数据,B*树分配新结点的概率比B+树要低,且新节点与原来节点的数据均分,空间使用率更高
B树系列可以在内存中做内查找。但是相比于哈希和平衡搜索树
B树系列通常在内存中体现不出优势
B树系列通常在磁盘中可以体现出优势
B树系列通常的应用作为索引使用,MySQL数据库索引底层原理是B+树
B+树索引磁盘数据