这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战
一、二叉树的局限性
我们先来看一下最基础的二叉搜索树,如下图所示:
我们假设搜索插入的数值为 key:
1.如果 key 大于根节点,则在右子树中进行查找;
2.如果 key 小于根节点,则在左子树中进行查找;
3.如果 key 等于根节点,也就是找到了这个节点,返回根节点即可。
所以我们可以说,上面的二叉树,我们最多只需要查找三次就可以找到节点,因为树的深度是3。
由上规律我们可以发现,如果插入的节点是按顺序不断增大的,那有可能生成的二叉树将会成线性结构,如下所示:
因为树的深度变为7了,所以我们最多需要查找7次,才能找到节点。
当磁盘 I/O 次数越多,所消耗的时间也就越大。如果我们能让索引的数据结构尽量减少硬盘的 I/O 操作,所消耗的时间也就越小。
为了解决这个问题,人们提出了平衡二叉搜索树(AVL 树),它在二分搜索树的基础上增加了约束,每个节点的左子树和右子树的高度差不能超过 1,也就是说节点的左子树和右子树仍然为平衡二叉树。第一棵树就属于平衡二叉搜索树,搜索时间复杂度就是 O(log2n)。
即使通过平衡二叉搜索树进行了改进,树的深度也是 O(log2n),当 n 比较大时,深度也是比较高的。虽然平衡二叉树比较的效率高,但是随着数据的增长,树的深度也同样高,这就意味着磁盘 I/O 操作次数多,会影响整体数据查询的效率。
如果我们把二叉树改成 M 叉树(M>2)呢,很显然,树的高度将会降低,当数据越大时,M叉树的高度会远小于二叉树。所以B树的出现就是为了解决这个问题。
二、B树
B 树的结构如下图所示:
B 树作为平衡的多路搜索树,它的每一个节点最多可以包括 M 个子节点,M 称为 B 树的阶。同时你能看到,每个磁盘块中包括了关键字和子节点的指针。如果一个磁盘块中包括了 x 个关键字,那么指针数就是 x+1。对于一个 100 阶的 B 树来说,如果有 3 层的话最多可以存储约 100 万的索引数据。对于大量的索引数据来说,采用 B 树的结构是非常适合的,因为树的高度要远小于二叉树的高度。
一个 M 阶的 B 树(M>2)有以下的特性:
1.根节点的儿子数的范围是[2,M]。
2.每个中间节点包含 k-1 个关键字和 k 个孩子,孩子的数量 = 关键字的数量 +1,k 的取值范围为[ceil(M/2), M]。
3.叶子节点包括 k-1 个关键字(叶子节点没有孩子),k 的取值范围为[ceil(M/2), M]。
4.假设中间节点节点的关键字为:Key[1], Key[2], …, Key[k-1],且关键字按照升序排序,即 Key[i]<Key[i+1]。此时 k-1 个关键字相当于划分了 k 个范围,也就是对应着 k 个指针,即为:P[1], P[2], …, P[k],其中 P[1]指向关键字小于 Key[1]的子树,P[i]指向关键字属于 (Key[i-1], Key[i]) 的子树,P[k]指向关键字大于 Key[k-1]的子树。
5.所有叶子节点位于同一层。
上面那张图所表示的 B 树就是一棵 3 阶的 B 树。我们可以看下磁盘块 2,里面的关键字为(8,12),它有 3 个孩子 (3,5),(9,10) 和 (13,15),你能看到 (3,5) 小于 8,(9,10) 在 8 和 12 之间,而 (13,15) 大于 12,刚好符合刚才我们给出的特征。
然后我们来看下如何用 B 树进行查找。假设我们想要查找的关键字是 9,那么步骤可以分为以下几步:
1.我们与根节点的关键字 (17,35)进行比较,9 小于 17 那么得到指针 P1;
2.按照指针 P1 找到磁盘块 2,关键字为(8,12),因为 9 在 8 和 12 之间,所以我们得到指针 P2;
3.按照指针 P2 找到磁盘块 6,关键字为(9,10),然后我们找到了关键字 9。
你能看出来在 B 树的搜索过程中,我们比较的次数并不少,但如果把数据读取出来然后在内存中进行比较,这个时间就是可以忽略不计的。而读取磁盘块本身需要进行 I/O 操作,消耗的时间比在内存中进行比较所需要的时间要多,是数据查找用时的重要因素,B 树相比于平衡二叉树来说磁盘 I/O 操作要少,在数据查询中比平衡二叉树效率要高。
三、B+树
B+ 树和 B 树的差异在于以下几点:
1.有 k 个孩子的节点就有 k 个关键字。也就是孩子数量 = 关键字数,而 B 树中,孩子数量 = 关键字数 +1。
2.非叶子节点的关键字也会同时存在在子节点中,并且是在子节点中所有关键字的最大(或最小)。
3.非叶子节点仅用于索引,不保存数据记录,跟记录有关的信息都放在叶子节点中。而 B 树中,非叶子节点既保存索引,也保存数据记录。
4.所有关键字都在叶子节点出现,叶子节点构成一个有序链表,而且叶子节点本身按照关键字的大小从小到大顺序链接。
下图就是一棵 B+ 树,阶数为 3,根节点中的关键字 1、18、35 分别是子节点(1,8,14),(18,24,31)和(35,41,53)中的最小值。每一层父节点的关键字都会出现在下一层的子节点的关键字中,因此在叶子节点中包括了所有的关键字信息,并且每一个叶子节点都有一个指向下一个节点的指针,这样就形成了一个链表。
比如,我们想要查找关键字 16,B+ 树会自顶向下逐层进行查找:
1.与根节点的关键字 (1,18,35) 进行比较,16 在 1 和 18 之间,得到指针 P1(指向磁盘块 2)。
2.找到磁盘块 2,关键字为(1,8,14),因为 16 大于 14,所以得到指针 P3(指向磁盘块 7)。
3.找到磁盘块 7,关键字为(14,16,17),然后我们找到了关键字 16,所以可以找到关键字 16 所对应的数据。
整个过程一共进行了 3 次 I/O 操作,看起来 B+ 树和 B 树的查询过程差不多,但是 B+ 树和 B 树有个根本的差异在于,B+ 树的中间节点并不直接存储数据。这样的好处都有什么呢?
-
首先,B+ 树查询效率更稳定。因为 B+ 树每次只有访问到叶子节点才能找到对应的数据,而在 B 树中,非叶子节点也会存储数据,这样就会造成查询效率不稳定的情况,有时候访问到了非叶子节点就可以找到关键字,而有时需要访问到叶子节点才能找到关键字。
-
其次,B+ 树的查询效率更高,这是因为通常 B+ 树比 B 树更矮胖(阶数更大,深度更低),查询所需要的磁盘 I/O 也会更少。同样的磁盘页大小,B+ 树可以存储更多的节点关键字。
不仅是对单个关键字的查询上,在查询范围上,B+ 树的效率也比 B 树高。这是因为所有关键字都出现在 B+ 树的叶子节点中,并通过有序链表进行了链接。而在 B 树中则需要通过中序遍历才能完成查询范围的查找,效率要低很多。
四、总结
在磁盘页大小相同的情况下,树的构造更加矮胖,所需要进行的磁盘 I/O 次数更少,更适合进行关键字的范围查询。