数据结构---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路平衡搜索树,可以是空树或者满足一下性质:

  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。

假设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树的基础上做了以下几点改进优化:

  1. 分支节点的子树指针与关键字个数相同。
  2. 分支节点的子树指针p[i]指向关键字值大小在[k[i],k[i+1])区间之间,这就说明B+树取消了最左边的那个子树
  3. 所有叶子节点增加一个链接指针链接在一起
  4. 所有关键字及其映射数据都在叶子节点出现。

那么将其规划一下就可以得到下面的图片:
在这里插入图片描述
所有的叶子节点增加一个链接指针链接在一起,那么这时想要遍历树中的每个元素时就可以直接通过叶子节点中的指针进行遍历,不需要像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+树。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/77699.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

一文学透设计模式——抽象工厂模式

创建者模式 抽象工厂模式 概念 抽象工厂模式是围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。这种类型的设计模式属于创建型模式&#xff0c;它提供了一种创建对象的最佳方式。 这是很多地方对于抽象工厂模式的描述&#xff0c;说实话感觉不是特别好懂。…

阿里云二级域名配置

阿里云二级域名配置 首先需要进入阿里云控制台的域名管理 1.选择域名点击解析 2.添加记录 3.选择A类型 4.主机记录设置【可以aa.bb或者aa.bb.cc】 到时候会变成&#xff1a;aa.bb.***.com 5.解析请求来源设置为默认 6.记录值 设置为要解析的服务器的ip地址 7.TTL 默认即…

linux Ubuntu 更新镜像源、安装sudo、nvtop

1.更换镜像源 vi ~/.pip/pip.conf在打开的文件中输入: pip.conf [global] index-url https://pypi.tuna.tsinghua.edu.cn/simple按下:wq保存并退出。 2.安装nvtop 如果输入指令apt install nvtop报错&#xff1a; E: Unable to locate package nvtop 需要更新一下apt&a…

<C/C++>日期和时间的使用(time相关函数大全)

1、函数详解及示例 1- time_t time(time_t *time); 1&#xff09;功能&#xff1a;获取或设置系统时间。 2&#xff09;参数&#xff1a;若给定参数&#xff0c;则将当前时间保存到该参数中&#xff1b;若不给定参数&#xff0c;参数填NULL。 3&#xff09;返回值&#xff1…

RTT(RT-Thread)时钟管理

目录 时钟管理 时钟节拍 RTT工程目录结构介绍 配置文件&#xff1a;rtconfig.h 获取系统节拍 获取系统节拍数函数 实例 定时器 RT_Thread定时器介绍 定时器源码分析&#xff08;了解即可&#xff09; rt_system_timer_init (硬件定时器初始化) rt_system_timer_thr…

pycharm出现python test运行报错(pytest模式)

pycharm出现python test运行报错 一、python test 执行代码报错二、删除运行配置三、修改pycharm默认配置为 unittests四、成功&#xff01; 一、python test 执行代码报错 二、删除运行配置 三、修改pycharm默认配置为 unittests 四、成功&#xff01;

怎么修改pdf文件中的文字?分享几种编辑方法

怎么修改pdf文件中的文字&#xff1f;PDF格式的文件通常具有很高的可读性和稳定性&#xff0c;但是如果需要修改其中的文字&#xff0c;就需要使用专门的PDF编辑器。本文将介绍几种PDF编辑的方法&#xff0c;下面就跟着我一起来看看这几款工具吧。 方法一&#xff1a;使用迅捷P…

python爬虫1:基础知识

python爬虫1&#xff1a;基础知识 前言 ​ python实现网络爬虫非常简单&#xff0c;只需要掌握一定的基础知识和一定的库使用技巧即可。本系列目标旨在梳理相关知识点&#xff0c;方便以后复习。 目录结构 文章目录 python爬虫1&#xff1a;基础知识1. 基础认知1.1 什么是爬虫&…

servlet生命周期和初始化参数传递

servlet生命周期和初始化参数传递 1、servlet生命周期 只有第一次访问才会初始化&#xff0c;之后访问都只执行service中的。 除非tomcat关闭重新启动&#xff1a; 2、初始化参数传递

Rust 编程小技巧摘选(6)

目录 Rust 编程小技巧(6) 1. 打印字符串 2. 重复打印字串 3. 自定义函数 4. 遍历动态数组 5. 遍历二维数组 6. 同时遍历索引和值 7. 迭代器方法的区别 8. for_each() 用法 9. 分离奇数和偶数 10. 判断素数&#xff08;质数&#xff09; Rust 编程小技巧(6) 1. 打印…

SpringBoot+Vue 实现图片验证码功能需求

文章底部有个人公众号&#xff1a;热爱技术的小郑。主要分享开发知识、有兴趣的可以关注一下。为何分享&#xff1f; 踩过的坑没必要让别人在再踩&#xff0c;自己复盘也能加深记忆。利己利人、所谓双赢。 前言 写过验证码保存到Redis中的需求开发、也写过验证码调用第三方接口…

阿里云瑶池 PolarDB 开源官网焕新升级上线

导读近日&#xff0c;阿里云开源云原生数据库 PolarDB 官方网站全新升级上线。作为 PolarDB 开源项目与开发者、生态伙伴、用户沟通的平台&#xff0c;将以开放、共享、促进交流为宗旨&#xff0c;打造开放多元的环境&#xff0c;以实现共享共赢的目标。 立即体验全新官网&…

MacOS上配置docker国内镜像仓库地址

背景 docker官方镜像仓库网速较差&#xff0c;我们需要设置国内镜像服务 我的MacOS docker版本如下 设置docker国内镜像仓库地址 点击Settings点击Docker Engine修改配置文件&#xff0c;添加registry-mirrors {"builder": {"gc": {"defaultKeepS…

IDEA基础使用

IDEA基础使用 1、IDEA中显示用法和用户截图展示有调用显示无调用显示 对应方法 2、如何找出项目中所有不被调用方法截图展示对应方法 3、常用代码(Code)说明及快捷键:4、未完待续待日后更新。。。总结&#xff1a;欢迎指导&#xff0c;也祝码友们代码越来越棒&#xff0c;技术越…

SpringCloud Gateway获取请求响应body大小

前提 本文获取请求、响应body大小方法的前提 : 网关只做转发逻辑&#xff0c;不修改请求、相应的body内容。 SpringCloud Gateway内部的机制类似下图&#xff0c;HttpServer&#xff08;也就是NettyServer&#xff09;接收外部的请求&#xff0c;在Gateway内部请求将会通过Htt…

五、JVM-垃圾回收算法

常见的回收算法&#xff1a;标记清除算法、复制算法、标记-整理算法、分代收集算法 1、标记清除算法 第一步&#xff1a;标记&#xff08;找出内存中需要回收的对象&#xff0c;并且把它们标记出来&#xff09; 第二步&#xff1a;清除 &#xff08;清除掉被标记需要回收的对…

Windows下JDK安装与环境变量配置

文章目录 每日一句正能量前言安装步骤配置环境变量验证环境变量是否配置成功后记 每日一句正能量 生命,就像一场永无休止的苦役,不要惧怕和拒绝困苦,超越困苦,就是生活的强者。任何经历都是一种累积,累积的越多,人就越成熟;经历的越多,生命就越有厚度。 本来不想写JDK的安装的&…

全国高校招投标信息在哪里看?

很多投标人在查询招标信息的时候常常没有找到合适的&#xff0c;但是现在网上查询投标信息的网站是很多的。而学校招标信息获取的渠道是比较少的&#xff0c;企业的反而更多一些&#xff0c;那么我们能在那些渠道获取这些信息&#xff1f; 1.教育部网站 教育部提供了招标信息…

8.Winform界面打包成DLL提供给其他的项目使用

背景 希望集成一个Winform的框架&#xff0c;提供权限菜单&#xff0c;根据权限出现各个Winform子系统的菜单界面。不希望把所有的界面都放放在同一个解决方案下面。用各个子系统建立不同的解决方案&#xff0c;建立代码仓库&#xff0c;进行管理。 实现方式 将Winform的UI界…

微信小程序页面传值为对象[Object Object]详解

微信小程序页面传值为对象[Object Object]详解 1、先将传递的对象转化为JSON字符串拼接到url上2、在接受对象页面进行转译3、打印结果 1、先将传递的对象转化为JSON字符串拼接到url上 // info为对象 let stationInfo JSON.stringify(info) uni.navigateTo({url: /pages/statio…