本文始发于个人公众号:TechFlow,原创不易,求个关注
今天是分布式系统的第八篇文章,核心内容是B+树的原理。
今天的文章是上周B树的延伸,所以新关注的或者是有所遗忘的同学建议先从下方链接回顾之前的内容。
B+树的特性
B+树和B树一样都是多路平衡树,也叫多叉树。两者的性质也基本一致,在具体来看详细内容之前,我们先来总体看下B+树的特性,先有个大概的印象。
我个人认为B+树大部分特性都和B树一样,唯一不同的只有以下几点:
- 所有的数据都存储在叶子节点,中间节点不存放数据
- 中间节点的元素数量和子树数量一致,而B树子树数量比元素数量多1
- 叶子节点是一个链表,可以通过指针顺序查找
我贴一张图,大家应该可以很直观地感受到。
从上图我们可以看到,所有出现在中间节点的元素都能在叶子节点当中找到,这对应了刚才说的所有数据都存放在叶子节点当中。然后我们还可以发现,中间节点当中的元素数量和子树数量一致,同样对应了区间分割。父节点的每个元素对应了一个子树中最大的元素。
至于最后的链表,应该很好理解,无非是链表出现在树当中,看起来有些新奇而已。
我看上图最大的感受就是像,和B树实在是太像了,就好像两个孪生兄弟,猛地看上去几乎一模一样,细微分辨才能发现一点差别。那么针对这样一颗熟悉又陌生的树,我们应该怎么去增删改查呢?
让我们继续往下看,在此之前,我想说一句,虽然B+树是B树的提升版,但是实现难度上,其实是降低的。也就是说整体而言,它比B树更容易实现。
B+树的查
由于B+树当中所有的数据都存储在叶子节点,所以我们在查找的时候,必须要一直查找到叶子节点为止。也就是说不会再有中途退出的情况,这样就简化了我们的判断,几乎不再需要临时退出了。
另一个特性是B+树当中的元素数量和子树数量一致,并且每个元素都代表一棵子树当中的最大值。通过这个限制,我们可以很轻松地确定我们要查找的元素究竟在哪棵子树当中。而B树则有可能出现超界的情况,我们需要特殊判断。
举个例子,这是一棵B树:
假设我们查找的元素是12,我们在根节点当中判断,先通过二分查找查找到9,发现12 > 9,于是我们去最右侧的子树当中检查。
而如果是B+树,会是这样,为了作图方便,我省去了叶子节点中横向的指针。
可以看到我们直接二分就可以精准地找到对应的子树,我们直接往下递归就好了。如果超界了,则说明肯定不在树上,可以提前返回。
所以这就是一个非常简单的树上查找,应该说只要理解了树的形状和递归的思路,应该是没有难度的。不过有一点需要注意,我们的查找接口并不只提供给外部,我们自己在插入的时候也会需要先找到对应的位置(节点)再执行插入的。显然我们要插入的元素十有八九不在树上(不然还叫什么插入),这个时候就不能返回空了,我们需要返回一个实实在在的节点。
所以我们可以传入一个flag,标记是否是我们内部调用的插入阶段,flag默认是False,所以对外部调用没有影响。
如果flag是True,我们在中途没有找到的时候就不能提前退出了,需要继续查找。并且我们还有可能更新一下最右侧元素的值。
还用上图举个例子:
如果我们插入一个元素15,整棵树会变成:
看到了吗,根节点当中的12被替换成了15,这也对应上了之前说的节点中的每个元素都对应子树中的最大值。我们先插入再去更新父亲当然也是可以的,但我们也可以在查找的时候直接进行更新,当我们发现待插入的元素比当前节点最大的元素还要大时,直接进行替换,这样可以省去一些代码。
我们来看下代码:
def _query(self, node, key, flag=False):
"""
:param node: 当前节点
:param key: 待寻找的key
:param flag: 是否是插入阶段
:return: True/False 是否找到,对应的节点,key在节点中的下标
"""
# 如果是叶子节点,说明没有childs
if node.is_leaf:
# 校验当前是否为空