数据结构——B树和B+树

平衡二叉树或红黑树的查找效率最高,时间复杂度是O(nlogn)。但不适合用来做数据库的索引树。

因为磁盘和内存读写速度有明显的差距,磁盘中存储的数据需要先读取到内存中才能进行高速的检索。而数据库当中存储着海量的数据,光是数据库索引就有可能占据几个GB甚至更大的空间。当我们要查找数据的时候,显然不可能把整个索引树读到内存中。因此,我们只能以索引树的节点为基本单元,每次把单一节点从磁盘读取到内存当中,进行后续操作。

如果磁盘当中的索引树是一棵平衡二叉树,查找的时候,在最坏情况下,磁盘I/O的次数等于索引树的高度。

为了减少磁盘I/O,我们需要把原本“瘦高”的树结构变得“矮胖”,让每一个节点承载更多的元素,拥有更多的孩子。

B树和B+树,就是这样的数据结构,因此它们非常适合做数据库和文件系统的索引。

一、B树

1.B树的特征

B树也被称为B-树(中间的横线是一个连接符,不是“B减树”!)

B树单一节点拥有的最多子节点数量,称为B树的“阶”。一个m阶的B树,具有如下几个特征:

  1. 根节点至少有两个子节点。
  2. 每个中间节点都包含k-1个元素(也被称为关键字)和k个孩子,其中m/2 <= k <= m。
  3. 每一个叶子节点都包含k-1个元素,其中m/2 <= k <= m。
  4. 所有的叶子节点都位于同一层。
  5. 每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。

如图是一个3阶的B树:
在这里插入图片描述

B树的查找过程(以上图中查找5为例):
在这里插入图片描述
B树查询中的比较次数其实不比平衡二叉树少,但是节点内部元素的比较是在内存中进行的,时间成本几乎可以忽略。真正影响性能的,是磁盘I/O的次数,只要树的高度降低,磁盘I/O次数就会相应减少,从而提高整体性能。

2.B树的插入操作

B树的插入操作,首先是在叶子节点进行的,可以分成两种情况:

1)插入元素未改变规则,即叶子节点元素数量不超过m-1

未改变规则,就不用做任何调整。

2)插入元素使叶子节点元素过多,超过m-1个元素
在这里插入图片描述
新插入的元素4使相应的叶子节点元素超过2个,打破了3阶B树的规则。于是,需要进行节点的“分裂”,把节点m/2位置的元素上升到父节点,剩余的左半边元素和右半边元素分成两个独立的叶子节点:
在这里插入图片描述

此时,父节点(2,4,6)的元素同样超过了2个,我们继续对父节点
进行分裂,元素4上升到祖父节点:
在这里插入图片描述

这一次并没有打破B树的规则,插入调整完毕。

3.B树的删除操作

分为4种情况:

1)删除元素在叶子节点,未改变规则

没有打破B树的规则,因此不用做任何调整。

2)删除元素在叶子节点,剩余元素不足,相邻兄弟节点有多余元素
在这里插入图片描述

如图所示,删除叶子节点的元素8之后,该节点少于1个元素。此时可以向左侧兄弟节点“借调”一个元素过来。但元素5不能直接放到元素8的位置,需要上升到父节点当中,再把元素5和8之间的元素6移动到元素8的位置:
在这里插入图片描述
3)删除元素在叶子节点,剩余元素不足,相邻兄弟节点没有多余元素
在这里插入图片描述
如上图所示,删除叶子节点的元素3之后,该节点少于1个元素,左右兄弟也没有多余的节点可以借调。此时我们可以把相邻叶子节点“合并”。删除的元素3和右侧相邻节点的元素8之间是元素6,我们可以把元素6和8合并成一个叶子节点:
在这里插入图片描述

4)删除元素在中间节点
在这里插入图片描述

如上图所示,删除的是中间节点的元素12,我们可以选择该元素的前驱或后继元素来顶替它的位置。在本例中,选择后继元素13更合适:
在这里插入图片描述

4.B树的缺点

B树不方便进行范围查询。

数据库的查询,不止涉及单一结果查询(比如查找学号是110235的学生),也会涉及一个区间内结果的查询(比如查找数学成绩60~80分的所有学生)。
对于前者,B树很容易实现,对于后者,由于区间内结果分布在各个层次的节
点当中,B树实现起来会非常烦琐,需要进行中序遍历,在父节点和子节点之间不断切换,给磁盘I/O带来很多负担。

二、B+树

B+树是B树的升级,它的结构设计为范围查询提供了便利。

B+树的特征

一个m阶的B+树具有如下几个特征:

  1. 有k个子树的中间节点包含k个元素(B树中是k-1个元素),每个元素不保存数据,所有数据都保存在叶子节点。
  2. 所有的叶子节点包含了全部元素,依照元素的大小升序排列,叶子节点之间用双向指针相连接。
  3. 所有中间节点的元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。

在这里插入图片描述
如上图是一颗B+树,每一个父节点的元素都出现在子节点中,是子节点的最大(或最小)元素。父节点的所有元素都出现在子节点,因此叶子节点包含了整棵树的全量信息。并且每一个叶子节点都带有指向相邻叶子节点的指针,形成了一个双向有序链表。

B+树的设计,让我们可以很方便地进行范围查询。

要查找区间范围内的元素时,首先自顶向下,查找区间的最小值所在的叶子节点,再通过链表指针,遍历到相邻叶子节点,直到在叶子节点中找到区间最大值,这样就成功找出了区间内的所有元素。

  • 4
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
稀疏矩阵是指其中大部分元素为零的矩阵。由于大多数矩阵都是稠密的,即大多数元素都不为零,因此通常情况下,我们使用一个二维数组来表示一个矩阵。但是,对于稀疏矩阵来说,这种方法会造成很大的浪费,因为大量的空间被用来存储零元素。为了解决这个问题,我们可以使用稀疏矩阵三元组表示法。 稀疏矩阵三元组表示法是将稀疏矩阵中每个非零元素的行、列和值存储在一个三元组中。其数据结构如下所示: ``` struct Triple{ int row, col; double value; }; ``` 其中,row表示非零元素所在的行,col表示非零元素所在的列,value表示非零元素的值。我们可以使用一个数组来存储所有的非零元素,这个数组就是稀疏矩阵的三元组。 稀疏矩阵三元组表示法的优点是它可以节省存储空间,缺点是它不方便进行矩阵运算。因此,在进行矩阵运算时,我们需要将稀疏矩阵转换成其他更方便进行矩阵运算的表示方法,如压缩矩阵和坐标矩阵等。 对于稀疏矩阵的求解,可以使用稀疏矩阵三元组表示法结合三元组高斯消元算法来进行求解。三元组高斯消元算法是一种针对稀疏矩阵的高斯消元算法,其基本思想是将矩阵化为上三角矩阵或下三角矩阵,然后通过回代或者前代求解方程。由于矩阵中大部分元素为零,因此在进行高斯消元时,我们只需要考虑非零元素,这样可以大大提高计算效率。 三元组高斯消元算法的基本步骤如下: 1. 将稀疏矩阵转换成三元组表示法; 2. 对三元组按照行和列的顺序进行排序; 3. 从第一个非零元素开始,进行高斯消元,将矩阵化为上三角矩阵或下三角矩阵; 4. 通过回代或者前代求解方程。 具体实现可以参考以下代码: ``` void SparseTripletGaussElimination(SparseTriplet& triplet, vector<double>& b) { int n = triplet.rows; vector<Triple> A(triplet.data, triplet.data + triplet.num); sort(A.begin(), A.end(), [](const Triple& a, const Triple& b){ return a.row < b.row || (a.row == b.row && a.col < b.col); }); vector<int> row(n+1), col(triplet.num), diag(n); vector<double> val(triplet.num); for (int i = 0; i < triplet.num; i++) { row[A[i].row]++; } for (int i = 1; i <= n; i++) { row[i] += row[i-1]; } for (int i = 0; i < triplet.num; i++) { int r = A[i].row, c = A[i].col; double v = A[i].value; int k = row[r]++; // 获取 r 行中下一个非零元素的位置 col[k] = c; val[k] = v; if (r == c) { diag[r] = k; // 记录对角线元素的位置 } } for (int k = 0; k < n-1; k++) { if (val[diag[k]] == 0) { // 对角线元素为零,无法消元 throw runtime_error("zero pivot encountered"); } for (int i = diag[k]+1; i < row[k+1]; i++) { int r = col[i]; double factor = val[i] / val[diag[k]]; for (int j = diag[k]+1; j < row[k+1]; j++) { if (col[j] == r) { val[j] -= factor * val[diag[k]]; } } b[r] -= factor * b[k]; } } if (val[diag[n-1]] == 0) { // 对角线元素为零,无法消元 throw runtime_error("zero pivot encountered"); } for (int k = n-1; k >= 0; k--) { double sum = 0; for (int i = diag[k]+1; i < row[k+1]; i++) { sum += val[i] * b[col[i]]; } b[k] = (b[k] - sum) / val[diag[k]]; } } ``` 其中,SparseTriplet是稀疏矩阵三元组表示法的数据结构,b是待求解的方程的右侧向量。在实现中,我们首先将三元组按照行和列的顺序进行排序,然后将其转换成压缩矩阵的形式,接着进行高斯消元,并通过回代或者前代求解方程。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值