数据结构---B树
目录标题
- B-树的由来
- B-树的规则和原理
- B-树的插入分析
- B-树的插入实现
- 准备工作
- find函数
- insert
- 中序遍历
- B-树的性能测试
- B-树的删除
- B+树
- B+树的元素插入
- B*树的介绍
B-树的由来
在前面的学习过程中,我们见过很多搜索结构比比如说顺序查找,二分查找,搜索二叉树,平衡二叉树,哈希表,如果数据量不是很大能够一次性放在内存中的话上面的结构是没有问题的,如果要处理的数据非常多的话就得对上面的结构进行处理,比如说平衡二叉树,如果数据不是很多的话平衡二叉树的每个节点会存储两个数据,一个是用于比较和查找的关键字K,另外一个就是存储的数据V,如果存储的数据非常多并且每个数据占用的内存非常大的话那这里就得做出更改将存储的数据放到磁盘上,而数据V里面存储的就是对应数据在磁盘上的地址,通过K来对比数据找到目标数据之后就可以根据里面的地址对磁盘进行IO找到相应的数据并对其进行读取,那如果数据的个数非常的多并且关键字所占用的空间大小也很大,导致关键字也存不下去的话这里就又得进行修改,将关键字也存储到磁盘上,内存中每个节点就直接存储数据在磁盘上的地址,那么在查找数据并比较数据的时候就是先通过地址将关键字从磁盘中提取出来然后进行比较,但是这里就存在一个问题我们知道cpu是一个很快的东西,而对磁盘进行读写是一个很慢的过程,尽管通过平衡二叉树可以在log(n)的时间复杂度找到想要的数据,可是因为要对磁盘读取这里的速度也会变的很慢,具体有多慢呢?我们来看看《算法导论》里面的描述:
所以要想快速的在大量数据中找到指定数据,就得减少对磁盘的io次数,那么这个时候有小伙伴会想到哈希表好像可以减少io次数它的时间复杂度是o(1),那这里能不能使用哈希表呢?答案是不行,首先哈希表在面对一些特殊数据时可能会发生很严重的哈希冲突,其次哈希表在搜索数据的时候也不是一次就能够找到这里的o(1)指的是常数次,具体是多少次得看具体的实现,所以哈希表也不能胜任这项工作,那么为了解决这个问题就有了一个新的数据结构叫做B树,这里大家别搞混了啊不是这个B数。
B-树的规则和原理
平衡二叉树效率低的原因是因为要多次从磁盘中提取一个节点的数据进行比较,提取的次数最多为树的高度次,那么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的值等于3,那么通过规则二可以知道每个分支节点里面至少存在一个关键字和两个孩子,最多含有2个关键字和3个孩子,那么节点就长下面这样:
那么这里就有个问题当前节点能不能存储一个关键字和三个孩子呢?答案是不能的因为规则而规定了每个节点都包含k-1个关键字和k个孩子,也就是说不管分支节点的内部数据怎么样,该节点内孩子的数目一定是要比关键字的数目多一,那么这一点也恰好反应了规则一的内容我们知道根节点一定存在一个关键字,那么根据规则二便可以知道根节点一定存在两个孩子,这里大家要知道的一点:虽然我们上面画的关键字最多有两个孩子最多有三个,但是在实现的过程中我们为了方便节点的分裂我们会多加一个上去也就是说当M等于3时孩子最多有4个关键字最多有3个:
我们上面描述的是分支节点的内部情况,而对于叶子节点则是包含k-1个关键字,其中 ceil(m/2) ≤ k ≤ m,那么这就对应的是规则三,而规则四就是告诉我们所有的叶子节点都处于同一层,也就是说B树的每条路径的长度都是一样的,规则五和规则六可以放到一起来理解,节点里面的child表示一个指针,它指向的是一个子树,子树里面也存储着很多节点,而规则五和六就是告诉我们子树child1中节点的值一定比关键字data1要小,data1的值又比child2子树中的所有节点都要小,child2子树中的所有值都要比关键字data2,data2的值又比child3子树中的所有值要小,那么这就是每个节点中数据的规律,当我们要查找一个数据的时候就可以利用这个规律,比如说dest数据比data1要大,那么我们就知道它肯定不会在child1子树里面,如果它比data2要小的话那么它一定在child2里面,那么这个时候就可以用相同的规律在child2的根节点里面进行查找,那么这就是B树的规则和原理,接下来我们来B树是如何生成的。
B-树的插入分析
假设M的值等于3,那么这个B树里面就应该存储2个关键字和3个指针,但是为了方便后面节点的分裂这里就各添加一个元素:
那么接下来就要往树里面插入以下数据:52 ,139, 75, 49, 145, 36,50,47,101,插入52时b树里面就只有一个节点所以B树里面也就不存在指向子树的指针,那么这里的图片就是这样:
那么插入数据139时,该数据就会紧接着52的后面:
按照规则来说当前节点存储的关键字已经满了,但是为了方便后面的分裂这里还可以存储一个关键字,因为75比52大比139小,所以75就应该插入到中间位置,那么这里的图片如下:
当一个节点的空间用完时就得对这个节点进行分裂,分裂的方法是创建一个新节点出来,然后将已满的节点拷贝一半的数据到新节点里面这里拷贝的顺序是从右往左进行拷贝,那么这里存有三个节点所以就是将3/2个节点移动到另外一个节点里面,那么这里的图片如下:
但是仅这里做的话无法将两个节点连接起来,所以这个时候再创建一个节点让这个节点作为B树的根,然后将老节点的最右边的数据(这里就是75)移动到根节点(父节点)里面,并且根节点(父节点)的第一个指针指向老节点,第二个指针指向新节点,那么这里的图片就变成下面这样:
然后插入数据49,因为49比75小所以49应该在75左边的子树,又因为49比52要小所以49应该在52的前面:
然后插入数据145,因为145比75要大所以他应该在75右边的子树,又因为145比139要大所以他应该在139的右边:
然后插入数据36,因为36比75要小所以他应该75左边的子树,又因为26比49要小所以他应该插入到49的左边:
那么这个时候又出现一个吃饱了的节点,所以这个时候得对该节点进行分裂,创建一个新的节点将已满节点的一半数据(52)移动进来,然后让父节点的中的一个指针指向自己,但是这里有个问题:我们说每个节点中指针的数目一定比关键字的数目大一,如果按照上面的步骤做的话会导致指针的数目比关键字的数目大二就变成了下面这样:
所以为了解决这个问题我们还得将老节点中最右边的数据移动到父节点里面,所以这里大家可以发现不管是创建根节点还是已经有了父节点,在节点分裂的时候都要将原来节点中的一个元素移动到父节点或根节点里面来保证关键字和指针的平衡,那么这里的图片如下:
然后插入数据50,因为50比49大比75小所以50应该插入到49和75之间的子树:
同样的道理插入47之后图片就变成了下面这样:
然后插入数据101图片就会变成下面这样:
那么这里就又出现了一个吃饱了的节点,所以得再创建一个节点然后将已满节点的一半移动到新节点里面,然后将老节点中最右节点移动到父节点里面,那么这里的图片就变成下面这样:
通过图片我们可以发现子节点进行分裂之后,父节点吃饱了所以在插入节点的过程中很可能会出现连续的分裂情况,那么这个时候还得进行分裂,因为这里是对根节点进行分裂,所以我们得创建两个节点,一个节点做根一个节点做原来根节点的兄弟,那么这里的图片如下:
那么这就是节点插入的过程。这里有个细节大家要注意一下:创建兄弟节点之后要会将一半的关键字拷贝过去,但是拷贝的同时要将对应的指针也拷贝到兄弟节点里面,并且转移n个关键字就得转移n+1个指针,那么这就是节点插入的过程,接下来我们要模拟实现B树。
B-树的插入实现
准备工作
首先每个节点里面会存有很多的数据,那么这里我们就得创建一个类来描述每个节点并且这个类得是一个模板类,模板中存在两个参数一个参数用来表示关键字或者指针的类型,另外一个参数用来表示当前树的阶树
template<class K,size_t M>
struct TreeNode
{};
节点中需要一个K类型的数组来关键字数组的大小为M,然后需要一个TreeNode类型的指针数组用来存储子树的地址数组的大小为M+1,因为节点满了之后需要对父节点插入数据,所以还需要一个指针用来指向当前节点的父节点,为了方便判断节点是否需要分裂这里就再创建一个变量用来记录当前节点的个数,那么构造函数里面就将指针数组全部初始化为nullptr,将K类型的数组全部初始化为K类型的默认构造,将计数变量初始化为0指向父节点的指针初始化为空,那么这里的代码如下:
struct BTreeNode
{BTreeNode(){for (int i = 0; i < M; i++){_keys[i] = K();_subs[i] = nullptr;}_subs[M] = nullptr;_parent = nullptr;_n = 0;}BTreeNode* _parent;//指向父节点K _keys[M];//存储关键字或者指针BTreeNode* _subs[M + 1];//指向子树的指针数组size_t _n;//记录当前节点的个数
};
然后可以创建一个B数类,这个类也是一个模板类并且模板的参数和节点的参数一样,在这个类里面就只有一个指向节点的指针,那么这里代码如下:
template<class K, size_t M>
class BTree
{typedef BTreeNode<K, M> Node;
public:
private:Node* _root=nullptr;
};
那么准备工作到这里就结束了,接下来我们要实现B树的查找函数。
find函数
find函数需要一个K类型的参数并且返回类不能只为bool,因为我们需要通过find函数来找到数据所在的地址并且还能够获取到数据,所以返回值的类型为pair类型并且pair的第一个参数表示节点的地址第二个参数用来表示节点中的第几个元素,那么这里函数声明就长这样:
pair<Node*, size_t> find(const K&)
通过之前的规律我们知道指针数组第i+1号元素指向的所有节点一定是比关键字数组中第i号元素大比i+1号元素小,所以我们就可以使用循环来进行比较,如果K的值比关键字数组第i号元素小的话就跳转到指针数组中的第i号元素那么这里我们就可以创建一个指针变量cur来进行跳转,因为根节点指针数组的每个元素都为空,所以当查找的元素不存在时cur指针一定会走向根节点并且值为空所以判断循环的条件就可以是cur的值
pair<Node*, size_t> find(const K& key){Node* cur = _root;while (cur){}return make_pair(cur, -1);}
因为指针数组的数目比关键字数组多一个所以我们先创建一个变量用来记录找到元素的下标,创建while循环如果i的值比当前节点内部元素数目要少的话就执行循环,如果K的值比当前下标的元素要大的话就对i++,如果小的话就直接break结束循环,如果相等的话就直接返回当前的下标和节点的地址,循环结束之后我们就得到元素的小标然后对cur进行跳转即可,那么这里打代码如下:
pair<Node*, size_t> find(const K& key)
{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);}}cur = cur->_subs[i];}return make_pair(cur, -1);
}
我们知道一个函数是可以在另外一个函数中使用的,我们知道往容器里面插入一个数据的时候一定得先找数据所在的地方,所以按道理来说我们应该可以在insert函数里面使用find函数,可是这里存在一个问题,当前容器是不支持插入相同数据的,而find函数在找不存在的容器时会返回空指针,所以这里就出现问题我们上面的实现导致find函数用不上,那么这里就可以做出一点改进,再创建一个parent指针用来记录查找指针的父节点,这样当节点存在就返回cur指针如果节点不存在就返回parent指针,这样在insert函数里面使用find函数我们就可以知道元素应该插入到哪个位置上,如果是判断节点是否存在的话我们依然可以通过pair的第二个元素进行判断,那么这里的代码如下:
pair<Node*, size_t> find(const K&)
{Node* cur = _root;Node* parent = nullptr;while (cur){size_t i = 0;while (i < cur->_n){if (K < cur->_keys[i]){break;}else if (K > cur->_keys[i]){i++;}else{return make_pair(cur, i);}}parent = cur;cur = cur->_subs[i];}return make_pair(parent, -1);
}
insert
insert函数需要一个K类型的参数,因为插入的数据存在相同值所以该函数的返回类型为bool类型,如果出现相同值就返回false,如果插入成功就返回true:
bool insert(const K& key)
因为插入的树里面可能一个元素都没有所以在函数的开始得用if语句来进行一下判断,如果当前对象的root变量等于空的话就new一个Node然后让root指针指向这个节点,将节点的第一个关键字复制为key,最后将该节点的_n变量加一:
bool insert(const K& key)
{if (_root == nullptr){_root = new Node;_root->_keys[0] = key;_root->_n++;return true;}
}
如果不为空的话就表明当前的树有元素,那么这个时候就可以创建一个pair类型的变量来接收find函数的返回值,如果变量的第二个参数为-1就表示当前的容器中存在相同值然后就直接返回false,如果第二个元素大于等于0就开始执行
bool insert(const K& key)
{pair<Node*, K> ret = find(key);if (ret.second >= 0){return false;}else{//开始执行插入}
}
ret变量里面记录了元素应该插入的节点,那么这个时候就可以创建一个for循环将插入位置往后的元素都向后挪动一个位置,这里大家要注意一下挪动关键字的同时还得挪动关键字对应的孩子指针,因为数据的挪动不仅仅出现在一个节点,在很多节点都会有所以将这个功能放到一个函数里面,在叶子节点里面挪动数据不需要连接孩子,但是在分支节点里面插入数据是因为下面的节点出现了分裂创建出来了新的节点从而往上面插入了数据,那么这个时候我们不仅得插入数据还得连接新创建的子节点和子节点的父节点,那么这个函数就得存在三个参数一个表示插入的关键字,一个表示关键字插入的节点,最后一个就表示关键字对应的子节点,那么这里的函数声明如下:
void InsertKey(Node* node, const K& key, Node* child)
//node表达当前存在的节点,key表示插入的关键字,child表示关键字对应的孩子节点
然后在函数里面就先通过for循环找到节点要插入的位置,在查找的过程中顺便移动元素那么这里我们就从后往先进行查找,首先创建一个创建一个变量n记录当前节点的最后一个元素的位置,然后创建while在循环里面将元素进行挪动循环执行的条件就是n的值大于等于0,在循环里面添加一个if语句进行判断,如果当前位置的元素大于key就对n的值减一,如果当前元素的值小于key就直接使用break来结束循环:
void InsertKey(Node* node, const K& key, Node* child)
//node表达当前存在的节点,key表示插入的关键字,child表示关键字对应的孩子节点
{int n = node->_n-1;while (n>=0){if (key < node->_keys[n]){node->_keys[n + 1] = node->_keys[n];node->_subs[n + 2] = node->_subs[n + 1];//注意这里的元素多一个--n;}else{break;}}
}
循环结束之后我们就知道当前元素应该插入的位置,又因为元素已经挪动好了我们直接插入元素就可以,插入完元素之后我们还得关注一下参数中的孩子节点,因为叶子节点没有孩子所以他传递过来的参数为空指针,所以父子节点连接的时候还得进行判断,如果孩子节点不为空就让孩子节点指向父节点,最后对节点中的n进行++那么这里的代码如下:
void InsertKey(Node* node, const K& key, Node* child)
//node表达当前存在的节点,key表示插入的关键字,child表示关键字对应的孩子节点
{int n = node->_n-1;while (n>=0){if (key < node->_keys[n]){node->_keys[n + 1] = node->_key[n];node->_subs[n + 2] = node->_keys[n + 1];//注意这里的元素多一个--n;}else{break;}}node->_keys[n + 1] = key;node->_subs[n + 2] = child;//父节点找到孩子if (child != nullptr){child->_parent = node; }node->_n++;
}
实现完插入函数之后我们再回到insert函数,因为插入一个节点可能会导致当前节点发生分裂从往父节点里面也插入节点,所以在完成插入之前先创建insertkey函数所需要的三个变量以方便函数的插入,又因为插入节点是一个循环的过程,所以接下来我们还得创建一个循环来完成插入,如果插入元素之后当前节点不是满的话就直接break,如果满了还得做一些操作:
bool insert(const K& key)
{if (_root == nullptr){_root = new Node;_root->_keys[0] = key;_root->_n++;return true;}pair<Node*, K> ret = find(key);if (ret.second >= 0){return false;}else{//开始执行插入Node* parent = ret.first;K newKey = key;Node* child = nullptr;while (1){InsertKey(parent, newKey, child);if (parent->_n != M){//当前节点没有满就直接退出break;}else{//当前的节点已经满了得分裂} }}
}
如果相等的话就表明当前节点已经满了这个时候就得创建一个兄弟节点然后将一半的数据赋值给兄弟节点,这里的数据包括关键字和孩子指针,并且赋值完了之后还得将原始数据进行覆盖:
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++){brother->_keys[j] = parent->_keys[i];brother->_subs[j] = parent->_subs[i];//赋值完之后得对原始数据进行重置parent->_keys[i] = K();parent->_subs[i] = nullptr;j++;}
}
但是这么做存在一个问题关键字和孩子指针都赋值给了兄弟节点但是这些孩子节点的父节点却依然指向原来的节点,所以在复制的同时还得修改一下孩子节点的父节点的指向,并且因为叶子结点的存在孩子节点可能为空,所以在改变指向的时候得进行一下判断判断,循环结束之后我们还得修改再进行一次赋值因为孩子数组的元素个树比关键字数组多一个所以还得进行一次修改,然后修改两个节点内部的计数变量那么这里的代码如下:
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++){brother->_keys[j] = parent->_keys[i];brother->_subs[j] = parent->_subs[i];if (parent->_subs[i] != nullptr){parent->_subs[i]->_parent = brother;}//赋值完之后得对原始数据进行重置parent->_keys[i] = K();parent->_subs[i] = nullptr;j++;}//指针数组多一个brother->_subs[j] = parent->_subs[i];if (parent->_subs[i] != nullptr){parent->_subs[i]->_parent = brother;}parent->_subs[i] = nullptr;brother->_n = j;parent->_n -= (brother->_n + 1);//这里多减一个是因为还要往父亲移动
}
兄弟节点搞定之后就来处理父亲节点,首先创建变量midkey记录要插入父亲节点的关键字然后将原始数据清除,因为往父节点插入数据存在两个情况一个是父节点不存在也就是根节点发生分裂,另外一种就是父节点存在,这两种情况处理起来是不一样的,所以这里得添加一个if语句进行判断,对于父节点不存在的节点这里就再创建一个节点将该节点的关键字数组的第一个元素赋值为midkey,然后将指针数组的第一个元素指向原来节点,将第二个元素指向兄弟节点,并将兄弟节点和原来的根节点的父亲指针指向新创建的节点,最后修改节点内部计数变量,那么这里的代码如下:
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++){brother->_keys[j] = parent->_keys[i];brother->_subs[j] = parent->_subs[i];if (parent->_subs[i] != nullptr){parent->_subs[i]->_parent = brother;}//赋值完之后得对原始数据进行重置parent->_keys[i] = K();parent->_subs[i] = nullptr;j++;}//指针数组多一个brother->_subs[j] = parent->_subs[i];if (parent->_subs[i] != nullptr){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;}else{}}
如果不是根节点这里就要进行转换,转换成为在父节点里面插入变量midkey,那么这里就只用改变parent和child和newKey的值就行,那么这里的代码如下:
bool insert(const K& key)
{if (_root == nullptr){_root = new Node;_root->_keys[0] = key;_root->_n++;return true;}pair<Node*, K> ret = find(key);if (ret.second >= 0){return false;}else{//开始执行插入Node* parent = ret.first;K newKey = key;Node* child = nullptr;while (1){InsertKey(parent, newKey, child);if (parent->_n != M){//当前节点没有满就直接退出break;}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++){brother->_keys[j] = parent->_keys[i];brother->_subs[j] = parent->_subs[i];if (parent->_subs[i] != nullptr){parent->_subs[i]->_parent = brother;}//赋值完之后得对原始数据进行重置parent->_keys[i] = K();parent->_subs[i] = nullptr;j++;}//指针数组多一个brother->_subs[j] = parent->_subs[i];if (parent->_subs[i] != nullptr){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{//通过循环转换成为往父节点里面进行插入child = brother;parent = parent->_parent;newKey = midkey;}} }}return true;
}
中序遍历
二叉树有中序遍历那么同样的道理B树也有中序遍历,中序遍历的顺序是左子树根节点右子树,首先用户拿不到根节点的值,所所以这里采用嵌套调用的方式来实现遍历:
void _InOrder(Node * cur)
{
}
void InOrder()
{_InOrder(_root);
}
然后在_InOrder函数里面就先判断一下cur是否为空,如果为空的话就直接返回,然后就创建一个for循环不停的遍历每个节点的孩子,循环的第一步就是跳转到对应的孩子,然后就打印对应位置的关键字的值,等关键字数组遍历完成之后再跳转到最右边的子树,那么这里代码如下:
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]); // 最后的那个右子树
}
有了上面的代码之后就可以对其进行测试,测试的代码如下:
nt main()
{BTree<int, 3> BTree;BTree.insert(52);BTree.insert(139);BTree.insert(75);BTree.insert(49);BTree.insert(145);BTree.insert(36);BTree.insert(50);BTree.insert(47);BTree.insert(101);BTree.InOrder();return 0;
}
代码的运行结果如下:
符合我们的预期。
B-树的性能测试
在项目中一般将M的值设置为1024,那么假设每个节点都是满的,那么根节点中会存储1023个数据和1024个子树,然后在第二层就存在1024个节点每个节点又有1023个关键字和1024个孩子,所以第二层存储的节点个数就是1024*1023,第二层的孩子指针就是1024*1024,那么第三层存储的节点个数就是1024*1024*1023,孩子指针的个数就是1024*1024*1024,所以第四层存储的关键字个数就是1024*1024*1024*1023,将每一层存储的节点个数加起来就是一个非常大的数字1023+1024*1023+1024*1024*1023+1024*1024*1024*1023,存储这么对数据但是在查找数据的时候最坏也只用对磁盘进行4次Io,上面是最好的情况那么最坏情况就是每个节点都只存储一半的数据这里粗略进行计算,第一层节点就只有一个数据和两个孩子,第二个层就有两个节点每个节点里面存储512个关键字和孩子,所以第三层就有1024个节点每个节点里面存储512个关键字和512个孩子,第四层就有1024*512个节点,每个节点存储512个关键字所以第四层就存储1024*512*512个关键字,所以当一个B树最坏情况装的就是1+2*512+2*512*512+2*512*512*512节点,可以看到及时最坏情况下b树也能够存储大量的数据,那么这就是B-树的性能。
B-树的删除
假设当前树的内容如下:
如果我们要删除36,那么这里就是直接删除并将40移动到36的位置
当删除一个元素时,如果当前节点里面的元素数量小于m/2 -1的话则优先找父亲借,然后父亲再兄弟节点节,所以这里先把49放到40的位置上:
然后再从兄弟那里找一个最小的元素放到父亲节点里面,所以图片就变成下面这样:
如果再删除49的话同样也是找付父亲借父亲找兄弟借,可是这里就存在一个问题兄弟借不到了,那么这时父亲就得找他的父亲借,所以先把53放到49的位置,然后把75放到53的位置,但是因为55比75要小那么这个时候就将55和75换一个位置,所以图片就变成下面这样:
然后父亲的父亲就找父亲的兄弟借,也就是将139放到原来75的位置,可是这时候父亲的兄弟就变的不符合规则了,那么这个时候他就会找他的孩子借,第一个孩子借不了就找第二个孩子借,所以145来到139的位置148来到145的位置,那么这里的图片如下:
然后再删除150这个时候就得将150放到145的位置上然后将160提到上面来:
然后再删除节点180因为当前节点的元素个数小于m/2-1并且他向父亲和兄弟也借不到元素,所以这个时候就得进行合并将180所在的节点删除然后将160放到148所在的节点
再删除节点160没有任何问题:
再删除节点145因为当前节点数量不够了并且无法向其他节点借时就得进行合并,这里就是将139和55进行合并,将101和148进行合并:
那么这就是大致的删除规则反正就是很复杂很难这里我们就不实现了大家了解即可。
B+树
B+树是B树的变形,是在B树基础上优化的多路平衡搜索树,B+树的规则跟B树基本类似,但是又在B树的基础上做了以下几点改进优化:
- 分支节点的子树指针与关键字个数相同。
- 分支节点的子树指针p[i]指向关键字值大小在[k[i],k[i+1])区间之间,这就说明B+树取消了最左边的那个子树
- 所有叶子节点增加一个链接指针链接在一起
- 所有关键字及其映射数据都在叶子节点出现。
那么将其规划一下就可以得到下面的图片:
所有的叶子节点增加一个链接指针链接在一起,那么这时想要遍历树中的每个元素时就可以直接通过叶子节点中的指针进行遍历,不需要像B-树一样从头开始遍历,通过上面的图片大家也可以看到分支节点和叶子节点中存在重复的值,那么分支节点里面存的值是叶子节点最小值的索引,也就是说通过分支节点里面的值我们就可以知道他的子节点中的最小值是多少,那么通过这个索引我们在查找元素的时候就很简单,假如说查找元素4,因为根节点的第一个元素的值为5,而4比5还小这就说明当前树中没有5,如果我们要查找元素60那么根节点中存在三个值5 28 65,因为60比28要大但是比65要小所以60只可能出现在28的子树里面,28指向的分支节点总又存储了三个值分别为28 35 56,因为60比56要大并且后面没有元素了,所以就存储在56对应的叶子节点,然后遍历叶子结点的内容即可,那么这就是B+树的内部构造接下来我们来看B+树的插入。
B+树的元素插入
B+树的每个节点都含有两块内容一个用来存储索引一个用来存储值,假设M的值为3插入的第一个元素为53图片就是下面这样:
上面的方块用来存储子节点的索引,下面的方块用来存储值,然后再插入数据139和75图片就变成了下面这样:
再插入数据49,因为49比53要小所以插入数据的同时还得修改索引的值:
并且当前的节点已经填满了所以这个时候就得进行分裂创建一个新的节点并将原来节点的一半分裂出去然后往索引里面添加值,那么这里的图片就变成下面这样
然后插入元素145 36图片就变成下面这样:
再插入元素101
节点满了就需要对其进行分裂:
然后再插入150和155,那么这个时候图片就变成了下面这样:
然后对节点进行分裂往父节点里面插入索引:
这时父节点也满了,那么这个时候也得对这个节点进行分裂
但是这个时候就没有了根节点,所以还得创建一个根节点用来指向两个分支节点:
那么这就是B+树的插入过程,大家理解了就行无需模拟实现。
B*树的介绍
B树是B+树的变形,在B+树的非根和非叶子节点再增加指向兄弟节点的指针
B树跟B+树的结构差不多就是在每个分支节点中也添加了一个指针用来指向同层的分支节点,然后B*树在分裂的时候与B+树有所不同:当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针。所以,B*树分配新结点的概率比B+树要低,空间使用率更高。通过以上介绍,大致将B树,B+树,B*树总结如下:
B树:有序数组+平衡多叉树;B+树:有序数组链表+平衡多叉树;B*树:一棵更丰满的,空间利用率更高的B+树。
相关文章:

数据结构---B树
目录标题 B-树的由来B-树的规则和原理B-树的插入分析B-树的插入实现准备工作find函数insert中序遍历 B-树的性能测试B-树的删除B树B树的元素插入B*树的介绍 B-树的由来 在前面的学习过程中,我们见过很多搜索结构比比如说顺序查找,二分查找,搜…...
c++11以后c++标准库定义的固定位宽的整数类型(Fixed width integer types)
Fixed width integer types Fixed width integer types (since C11) - cppreference.com 相关定义文件如下: Windows系统MSVC: Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.33.31629\include\cstdint Linux系统GCC: gcc\libstdc-v3\include\c_g…...
Object.values()
Object.values() 是ES2017新增的一个对象方法,它可以将一个对象自身的所有可枚举属性值,组成一个数组返回。 基本语法: Object.values(obj)示例: jsCopy codeconst obj {foo: bar,baz: 42 };Object.values(obj); // [bar, 42]Object.values()的特点: 只返回可枚举的属性值…...

Oracle 开发篇+Java调用OJDBC访问Oracle数据库
标签:JAVA语言、Oracle数据库、Java访问Oracle数据库释义:OJDBC是Oracle公司提供的Java数据库连接驱动程序 ★ 实验环境 ※ Oracle 19c ※ OJDBC8 ※ JDK 8 ★ Java代码案例 package PAC_001; import java.sql.Connection; import java.sql.ResultSet…...

linux 查询后台任务及杀掉进程
查看后台任务命令 jobs -l删除后台进程命令 kill -9 28719...

【Vue3 博物馆管理系统】使用Vue3、Element-plus菜单组件构建前台用户菜单
系列文章目录 第一章 定制上中下(顶部菜单、底部区域、中间主区域显示)三层结构首页 第二章 使用Vue3、Element-plus菜单组件构建菜单 [第三章 使用Vue3、Element-plus菜单组件构建轮播图] [第四章 使用Vue3、Element-plus菜单组件构建组图文章] 文章目…...
Windows 11清除无效、回收站、过期、缓存、补丁更新文件
Windows 11与之前的Windows版本类似,也需要定期清理无效、垃圾、过期、缓存文件来保持系统性能和存储空间的优化。以下是在Windows 11中进行这些清理操作的一些建议方法: 磁盘清理工具 Windows 11内置了磁盘清理工具,可以帮助你删除临时文件…...

栈和队列详解(2)
目录 一、什么是队列? 二、创建一个我们自己的队列 1.前置准备 1.1需要的三个文件 1.2结构体的创建和头文件的引用 2.接口的实现 2.1初始化队列 2.2入队 2.3队列元素个数和判空 2.4取队头元素和队尾元素 2.5出队 2.6摧毁队列 2.7测试接口 三、所有代码 1.…...

EMC传导干扰滤波电路设计
1.EMC概念 2.EMC 传导干扰详解 EMC传导滤波电路的设计--传导干扰详解 3.EMC 传导干扰的测量方法 4.EMC 滤波电路设计 5.浪涌抑制电路设计 6.开关电源的安全要求 7.当前开关电源灯的应用...

【win10专业版远程控制】 自带远程桌面公司内网电脑
使用win10专业版自带远程桌面公司内网电脑 文章目录 使用win10专业版自带远程桌面公司内网电脑 在现代社会中,各类电子硬件已经遍布我们身边,除了应用在个人娱乐场景的消费类电子产品外,各项工作也离不开电脑的帮助,特别是涉及到数…...
Ubuntu 20.04 中安装docker一键安装脚本
直接上脚本,依次执行如下命令即可 wget http://apollo-pkg-beta.bj.bcebos.com/docker_install.sh bash docker_install.shdocker install docker operation system Ubuntu 18.04 直接上脚本,依次执行如下命令即可 ways1 : wget https://github.com…...
Mysql之安装-字符集设置-用户及权限操作-sqlmode设置
1、概述 MySQL支持大型数据库,支持5000万条记录的数据仓库,32位系统表文件最大可支持4GB,64位系统支持最大的表文件为8TB。使用标准的SQL数据语言形式。 2、Linux的mysql安装 (1)检查是否已安装:rpm -qa…...

腾讯云香港服务器租用价格_CN2线路延迟速度测试
腾讯云香港服务器,目前中国香港地域轻量应用服务器可选配置2核2G20M、2核2G30M、2核4G30M,操作系统可选Windows和Linux,不只是香港云服务器,新加坡、硅谷、法兰克福和东京服务器均有活动,腾讯云服务器网分享腾讯云境外…...
机器人静力学与刚度模型学习笔记
总算进行到刚度模型了。。。 ❤ 2023.8.6 ❤ 机器人静力学 学习资料 →→→【4-10机器人的静力分析】 机器人末端广义力 F [ f m ] [ f x f y f z m x m y m z ] F\left[\begin{matrix}f\\m\\\end{matrix}\right]\left[\begin{matrix}f_x\\f_y\\f_z\\m_x\\m_y\\m_z\\\end{…...

geeemap学习总结(1)——Anaconda-VSCode-geemap环境安装与配置
配置conda geemap 环境 通过Anaconda配置geemap环境较为方便,首先需在系统中完成 Anaconda安装。创建名为geemap的环境conda create -n geemap切换到新建的环境conda activate geemap安装geemap依赖包conda install -c conda-forge geemap 安装mambaconda install …...

.netcore grpc一元方法详解
一、grpc服务端搭建 打开visual studio--》新建项目--》创建ASP.NET Core gRPC服务。 这里我是用的.NET 6.0做为底层框架,使用该框架支持grpc的功能更全面。令注使用nuget包Grpc.AspNetCore这里我使用的是2.40.0版本。 // 创建dollar.proto文件syntax "prot…...

自学网络安全(黑客)全网详细路线
前言 web渗透是网络安全大行业里入门板块,就像十年前的软件,前景非常被看好,薪资也很诱人。与软件测试和前端开发只需掌握一定的编程能力不同的是,渗透需要掌握的知识内容较多,花费的时间较长,渗透测试掌握…...

上半年210个数字化大单,花落谁家?
2023年,各地数字化采购项目有怎样的进展?最近,我们通过中国政府采购网、中国招投标公共服务平台、天眼查、企查查等渠道,梳理了2023年上半年政企数字化项目的中标情况,并从中看到今年数字化项目的市场特点。 01 金融千…...
Integer.bitCount()
先看一道算法题: 剑指 Offer 15. 二进制中1的个数 编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 1 的个数(也被称为 汉明重量).)。 提示: …...

【Gitee的使用】Gitee的简单使用,查看/创建SSH公匙、创建版本库、拉取代码、提交代码
推荐阅读 CSDN主页GitHub开源地址Unity3D插件分享简书地址我的个人博客 大家好,我是佛系工程师☆恬静的小魔龙☆,不定时更新Unity开发技巧,觉得有用记得一键三连哦。 一、前言 本篇文章简单介绍,如何在Gitee上面创建版本库、拉取…...

XML Group端口详解
在XML数据映射过程中,经常需要对数据进行分组聚合操作。例如,当处理包含多个物料明细的XML文件时,可能需要将相同物料号的明细归为一组,或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码,增加了开…...
Python爬虫实战:研究MechanicalSoup库相关技术
一、MechanicalSoup 库概述 1.1 库简介 MechanicalSoup 是一个 Python 库,专为自动化交互网站而设计。它结合了 requests 的 HTTP 请求能力和 BeautifulSoup 的 HTML 解析能力,提供了直观的 API,让我们可以像人类用户一样浏览网页、填写表单和提交请求。 1.2 主要功能特点…...

XCTF-web-easyupload
试了试php,php7,pht,phtml等,都没有用 尝试.user.ini 抓包修改将.user.ini修改为jpg图片 在上传一个123.jpg 用蚁剑连接,得到flag...

微信小程序之bind和catch
这两个呢,都是绑定事件用的,具体使用有些小区别。 官方文档: 事件冒泡处理不同 bind:绑定的事件会向上冒泡,即触发当前组件的事件后,还会继续触发父组件的相同事件。例如,有一个子视图绑定了b…...

《从零掌握MIPI CSI-2: 协议精解与FPGA摄像头开发实战》-- CSI-2 协议详细解析 (一)
CSI-2 协议详细解析 (一) 1. CSI-2层定义(CSI-2 Layer Definitions) 分层结构 :CSI-2协议分为6层: 物理层(PHY Layer) : 定义电气特性、时钟机制和传输介质(导线&#…...
连锁超市冷库节能解决方案:如何实现超市降本增效
在连锁超市冷库运营中,高能耗、设备损耗快、人工管理低效等问题长期困扰企业。御控冷库节能解决方案通过智能控制化霜、按需化霜、实时监控、故障诊断、自动预警、远程控制开关六大核心技术,实现年省电费15%-60%,且不改动原有装备、安装快捷、…...

CocosCreator 之 JavaScript/TypeScript和Java的相互交互
引擎版本: 3.8.1 语言: JavaScript/TypeScript、C、Java 环境:Window 参考:Java原生反射机制 您好,我是鹤九日! 回顾 在上篇文章中:CocosCreator Android项目接入UnityAds 广告SDK。 我们简单讲…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...

算法笔记2
1.字符串拼接最好用StringBuilder,不用String 2.创建List<>类型的数组并创建内存 List arr[] new ArrayList[26]; Arrays.setAll(arr, i -> new ArrayList<>()); 3.去掉首尾空格...

Python Ovito统计金刚石结构数量
大家好,我是小马老师。 本文介绍python ovito方法统计金刚石结构的方法。 Ovito Identify diamond structure命令可以识别和统计金刚石结构,但是无法直接输出结构的变化情况。 本文使用python调用ovito包的方法,可以持续统计各步的金刚石结构,具体代码如下: from ovito…...