找了好长时间关于B+树的时间复杂度的博客,没找到几篇相关的,有相关的博客基本都是错误的,当然不排除我没找到的情况。
B+树的建立我就简单的说一下,避免大家搞混B树和B+树,这也是面试官喜欢考察的地方(好多面试官对这里也是比较糊涂的,因为面试官不一定是做数据库或者索引相关研究的)。
B树和B+树最大的区别就是,B树是将各种信息保存在所有节点中的,B+树是将各种信息保存在叶子中的。这样一来,对于每一个要查找的值来说,B+树都需要从根节点到叶子节点找一遍,那么时间复杂度与树高成正比;B树只需要找到相应节点就停止查找,所用时间从 O ( 1 ) − O ( h ) O(1)-O(h) O(1)−O(h)都有可能。由此看来,B+树性能看似比B树更差(既然这样,为啥还用B+树?),但是所用时间更稳定(为什么稳定性重要?)。
先来看看B+树的结构:
上图是一个典型的B+树非叶子节点构成。14,52,78是索引值,不是实际的值。
上图就是一个B+树,n=3。什么是n,一会说。
每个非叶子节点的pointer指向下一个节点。你看,由于数据结构的原因,我们不会为每个节点分配不同的数据结构存储信息,所以对于上图的B+树,每个节点保存3个索引值和4个pointer值。对于23, 31, 43这个节点,最左pointer指向索引值小于23却大于左侧节点(7)的索引值的节点。而索引值为31的下方有两个pointer,左边是指向小于31的,右边是指向大于等于31而小于43的指针。到了叶子节点,2下面就成了只有一个指针,指向响应的磁盘块,同样的,3, 5也均指向响应的磁盘块,还剩一个指针指向下一个节点。这个指针在做范围查询的时候比较有用:我们从根节点到叶子节点找到范围下界,再进行遍历到上界即可,所需要的时间复杂度是
O
(
n
+
h
)
O(n+h)
O(n+h),如果是没有这个指针,我们需要遍历每一个树杈,时间复杂度是
O
(
n
∗
h
)
O(n*h)
O(n∗h),这里的
h
h
h是树高。
什么是上面提到的n?
B+树将结构分为三部分:
- 根节点
- 中间节点
- 叶子节点
这篇文章并不是给初学B+树的同学看的,重点是后面的时间复杂度,所以怎么维持B+树, 每个节点的性质就不提了。
我们把每个节点成为“块”, 英文是block。而每个节点内部最多含有多少索引值,这个数目就是n。这个n又称为B+树的阶,英文是order of B+tree。 因此,当我们知道n的值时,我们便知道了该节点的子树有几棵。
比如n=3,则子树就有4棵的,假设一个根节点是22, 28, 35,那么该树的分叉为i<=22, 22<i<=28, i<28<=35,i>35。
一个盘pointer指向一个block。一个节点共有n+1个pointer,n个索引值有:
(
n
+
1
)
∗
∣
b
l
o
c
k
a
d
d
r
e
s
s
∣
+
n
∗
∣
k
e
y
s
∣
≤
b
l
o
c
k
s
i
z
e
(n+1)*|blockaddress| + n*|keys| \leq blocksize
(n+1)∗∣blockaddress∣+n∗∣keys∣≤blocksize
意思是,所有块地址的大小(块指针的大小)加上块索引值的大小,小于等于一个块的大小:
n
≤
∣
b
l
o
c
k
s
i
z
e
∣
−
∣
b
l
o
c
k
a
d
d
r
e
s
s
∣
∣
b
l
o
c
k
a
d
d
r
e
s
s
∣
+
∣
k
e
y
s
∣
≤
∣
b
l
o
c
k
s
i
z
e
∣
∣
b
l
o
c
k
a
d
d
r
e
s
s
∣
+
∣
k
e
y
s
∣
n\leq\frac{|blocksize| - |blockaddress|}{|blockaddress| + |keys|}\leq\frac{|blocksize|}{|blockaddress| + |keys|}
n≤∣blockaddress∣+∣keys∣∣blocksize∣−∣blockaddress∣≤∣blockaddress∣+∣keys∣∣blocksize∣
为什么看似效率更低的B+树比B树更广泛应用?
说了只是看似嘛,实际上B+树的效率更高。实际应用中,数据库中存放的数据很多,不像我们测试时一样,进用几行数据代表全部。而数据库的一条经验法则就是: 尽可能多的一次性将数据放进内存中处理。举个例子,内存容量以及性能允许一次性存放300条数据,但是数据库中有3000条数据要处理。那么我们每次放300条,需要放10次,I/O操作就是10。众所周知,I/O操作(硬盘到内存)往往比单纯的检索更费时间,因此I/O操作越少越好。
设:300条数据查找时间为300,I/O操作需要30,那么300*10+30*10 = 3300
如果每次放入30条数据:30*100 + 20*100 = 5000,这里由于每次传输的数据少,将每次传输时间假设少于30。可见I/O操作的耗时。
那么,由于B+树不将数据信息存放进节点,也就是说节点信息的容量变小,那么一次性可以放进内存的节点数变多,意味着I/O操作变少,以此提高性能。
为什么稳定性重要?
本来对于这个问题我也是不知道答案的,经过我问了老师后,老师回答如下:
简单解释一下,就是稳定性的优势体现在前文所说的范围查找。我们知道内存是连续的,如果用树的遍历去做范围查找,会不断在树中进行跳跃,直接访问节点还好,但是是通过树的节点访问内存的,也就是在内存中进行跳跃,这样的话性能会降低。
此外,无论是对于B树还是B+树,绝大部分的数据都是存放在叶子节点中的(树的特性,满节点二叉树的孩子节点比根节点多1倍),因此对于大部分数据的查询依旧是从根节点到叶子节点,性能不会带来多少进步。
时间复杂度
假设一个含有N个值,阶为n的B+树,它的查找,插入,删除的时间复杂度是多少?
答:B+树的插入与删除操作都仅用
O
(
1
)
O(1)
O(1)的时间,因此三者的时间复杂度相同。关键是判断查找的时间复杂度。
很显然,和树高是有关系的。什么时候树高最大呢?那就是树的分叉最少的时候,也就是根节点只分两叉。每个节点有
⌈
n
/
2
⌉
\lceil{n/2}\rceil
⌈n/2⌉个枝杈(这是B+树的定义,每个节点(除根外)必须至少含有这么多子节点)。这么做,是避免一个B+树太空而影响性能。
根节点把树分成了两棵子树,平均一下每一子树的值为N/2,设树高为h,则
(
n
2
)
h
≥
N
2
(\frac{n}{2})^h \geq \frac{N}{2}
(2n)h≥2N
意思是,每一个节点有至少n/2个选择对应下一个节点,共有h次这样的选择(每层一次选择),这个选择需要覆盖所有的可能出现的叶子节点,因此树高为
h
≥
log
n
2
(
N
/
2
)
h\geq \log_\frac{n}{2}(N/2)
h≥log2n(N/2)
换成时间复杂度的大O表示法,则可以变为
O
(
log
n
N
)
O(\log_nN)
O(lognN)或者
O
(
log
N
)
O(\log N)
O(logN)