B树和B+树

B树和B+树

标签(空格分隔): 数据结构


参考/转载 : https://www.cnblogs.com/nullzx


1. B树

1.1 B树的定义

B树也称为B-树, 它是一颗多路平衡的查找树, 当我们描述一颗B树的时候需要指定他的阶数, 阶数表示了一个节点最多有多少个孩子节点, 一般用m表示. 当m取2的时候, 就是我们常见的二叉搜索树.

一颗m阶的B树定义如下:

  1. 每个节点最多有m-1个关键字.
  2. 根节点最少有1个关键字.
  3. 非根节点最少有Math.ceil(m/2)-1个关键字.
  4. 每个根节点中的关键字都按照从小到大的顺序排列, 每个关键字的左子树中的所有关键字都小于它, 而右子树中的所有关键字节点都大于它.
  5. 所有叶子节点都位于同一层, 或者说根节点到每个叶子节点的长度都相同.

四阶B树

上图是一个阶数为4的B树, 在实际应用中的B树的阶数都非常大(通常大于100), 所以即使存储大量的数据, B树的高度依然会比较小, 每个节点中存储了关键字(Key)和关键字对应的数据(data), 以及孩子节点的指针. 我们将一个key何其对应的data成为一个记录, 但为了方便描述, 除非特别说明, 后续文中就采用key来代替(key,value)键值对这个整体. 在数据库中我们将B树(和B+树)作为索引结构, 可以加快查询速度, 此时B树中的key就代表键, 而data表示了这个键对应的条目在硬盘上的逻辑地址.


1.2 B树的插入操作

插入操作是指插入一条记录, 即(key,value)的键值对. 如果B树种已经存在需要插入的键值对, 则用需要插入的value替换旧的value. 如果B树不存在这个key, 则一定是在叶子节点中进行插入操作.

  1. 根据要插入的key的值, 找到叶子节点并插入.
  2. 判断当前节点的key的个数是否小于等于m-1, 若满足则结束, 否则进行第三步.
  3. 以根节点中间的key为中心分裂为左右两部分, 然后将这个中间的key插入到父节点中, 这个key的左子树指向分裂的左半部分, 这个key的右子树指向分裂后的右半部分, 然后将当前节点指向父节点, 继续进行第三步.

下面以5阶B树为例, 介绍B树的插入操作, 在5阶B树种, 节点最多有4个key,最少有两个key.


1.在空树中插入39.

1.空树

此时根节点就一个key, 此时根节点也是叶子节点.


2.继续插入22, 97和41 .

2.此时四个Key

根节点此时有4个key


3.继续插入53.

3.继续插入53

插入53的时候发现超过了每个节点的最大关键字个数4, 所以此时按照41为中心开始分裂, 结果如下图所示, 分裂后当前节点指针指向父节点, 满足B树条件, 插入操作结束. 当阶数m为偶数的时候, 需要分裂时就不存在排序恰好在中间的key, 那么我们选择中间位置的前一个key或中间位置的后一个key为中心进行分裂即可.

初始状态


4.依次插入13,21,40, 同样会造成分裂.
一次插入13,21,40


5.依次插入30,27,33; 36,35,34; 24,29. 结果如下图所示.

依次插入30,27,33; 36,35,34; 24,29.


6.插入key值为26的记录, 插入后的结果如下图所示.

插入值为26的记录

当前结点需要以27为中心分裂,并向父结点进位27,然后当前结点指向父结点,结果如下图所示。

以27为中心分裂

进位后导致当前结点(即根结点)也需要分裂,分裂的结果如下图所示。

此处输入图片的描述

分裂后当前结点指向新的根,此时无需调整。


在实现B树的代码中, 为了使代码的编写更加容易, 我们可以将节点中存储记录的数组长度定义为m而非m-1, 这样方便底层的节点由于分裂向上层插入一个记录的时候, 上层有多余的位置存储这个记录. 同时每个节点还可以存储它的父节点的引用, 这样就不必编写递归程序.

一般来说,对于确定的m和确定类型的记录, 节点大小是固定的, 无论他存储了多少个记录. 但是分配固定节点大小的方法会存在浪费的情况, 比如key28,29所在的节点, 该节点所在的节点还有两个位置没用使用, 因为节点的前序key为27, 后继节点为30. 所有的整数值都已经用光了, 所以, 如果记录按照key的大小排序后, 再插入到B树中 , 节点的使用率就会很低, 最差的情况为50%


1.3 B树的删除操作.

删除操作是指, 根据key删除记录, 如果B树中的记录不存在对应key的记录, 则删除失败.

  1. 如果当前需要删除的key位于非叶子节点上, 则用后继key覆盖要删除的key, 然后在后继key所在的子支中删除该后继key. 此时后继key一定位于叶子节点上, 这个过程和二叉搜索树删除节点的方法类似. 删除这个记录后执行第2步.
  2. 该节点的key的个数大于等于Math.ceil(m/2)-1, 结束删除操作, 否则执行第三步.
  3. 如果兄弟节点key个树大于Math.ceil(m/2)-1, 则父节点中的key下移到该节点, 兄弟节点中的一个key上移, 删除操作结束.

否则, 将父节点中的key下一与当前以及他的兄弟节点合并, 形成一个新的节点. 原父节点中的key的两个孩子指针就变成了一个孩子指针, 指向这个新节点. 然后当前节点的指针指向父节点, 重复上方第二步.

有些节点可能左右兄弟都有, 那么我们随意选择一个兄弟节点进行操作即可.

下面以5阶B树为例, 介绍B树的删除操作, 5阶B树中, 节点最多有4个key, 最少有两个key


1.原始状态
原始状态


2.在上面的B树中删除21, 删除后节点中的关键字个数依然大于等于2, 所以删除结束.
此处输入图片的描述


3.在上述情况下接着删除27, 从上图可知27位于非叶子节点中, 所以用27的后继替换它, 从图中可以看出27的后继为28, 我们用28替换27, 然后在28(原27)的有孩子节点中删除28, 删除后的结果如图.
此处输入图片的描述

删除之后发现, 当前小于2, 而它的兄弟节点中有三个记录(当前节点还有一个右兄弟节点, 选择右兄弟节点会出现合并节点的情况, 其实不论选择左右那个都行, 只是B树的形态会不一样而已.), 我们可以从兄弟节点中接取一个key, 所以父节点中的28下移, 兄弟节点中的26上移, 删除结束.
此处输入图片的描述


4.在上述情况下接着32, 结果如下图
此处输入图片的描述

当删除后, 当前节点中只有一个key, 而兄弟节点中也仅有两个key。所以只能让父结点中的30下移和这个两个孩子结点中的key合并,成为一个新的结点,当前结点的指针指向父结点。结果如下图所示。

此处输入图片的描述


5.上述情况下,我们接着删除key为40的记录,删除后结果如下图所示。
此处输入图片的描述

同理,当前结点的记录数小于2,兄弟结点中没有多余key,所以父结点中的key下移,和兄弟(这里我们选择左兄弟,选择右兄弟也可以)结点合并,合并后的指向当前结点的指针就指向了父结点。

此处输入图片的描述

同理,对于当前结点而言只能继续合并了,最后结果如下所示。

此处输入图片的描述

合并后结点当前结点满足条件,删除结束。


2. B+ 树

点击查看代码
from bisect import bisect_left, bisect_right


class BPlusTree(object):
    def __init__(self, capacity):

        self._tree = BPlusTreeLeaf([], None, capacity)
        self._capacity = capacity

    def insert(self, key):
        pkey, ppointer = self._tree.insert(key)
        if pkey is not None:
            new_master_node = BPlusTreeNode([pkey], [self._tree, ppointer], self._capacity)
            self._tree = new_master_node

    def keys(self):
        return self._tree.keys()

    def find(self, key):
        return self._tree.find(key)

    def num_nodes(self):
        return self._tree.num_nodes()

    def num_leaves(self):
        return self._tree.num_leaves()

    def num_keys(self):
        return self._tree.num_keys()

    def height(self):
        return self._tree.height() + 1

    def stats(self):
        return (self.height(), self.num_nodes(), self.num_leaves(), self.num_keys())

    def __str__(self):
        s = ""
        for level in range(1, self.height() + 1):
            if level == 1:
                s += ' ' * self.num_keys() * 2 + str(self._tree)
            elif level == self.height():
                leaf = self._tree._pointers[0]
                for i in range(self.height() - 2):
                    leaf = leaf._pointers[0]

                while leaf._next is not None:
                    s += str(leaf) + "->"
                    leaf = leaf._next
                s += str(leaf)
            elif level == 2:
                s += ' ' * self.num_keys()
                for child in self._tree._pointers:
                    s += str(child) + " "
            elif level == 3:
                s += ' ' * self.num_keys()
                for child in self._tree._pointers:
                    for kid in child._pointers:
                        s += str(kid) + " "
            s += "\n"
        return s


class BPlusTreeNode(object):
    def __init__(self, keys, pointers, capacity):

        self._keys = keys  # in sorted order
        self._pointers = pointers  # one more than the number of keys
        self._capacity = capacity

    def keys(self):
        return self._pointers[0].keys()

    def find(self, key):
        return self._pointers[bisect_right(self._keys, key)].find(key)

    def insert_here(self, position, key, pointer):  # inserting at node level
        self._keys.insert(position, key)
        self._pointers.insert(position + 1, pointer)

        cap = self._capacity
        split_index = (cap + 1) // 2
        if len(self._keys) > cap:
            pkey = self._keys[split_index]
            self._keys.remove(pkey)

            new_lateral_node = BPlusTreeNode(self._keys[split_index:], self._pointers[split_index + 1:], cap)

            self._keys = self._keys[:split_index]
            self._pointers = self._pointers[:split_index + 1]

            return (pkey, new_lateral_node)
        return (None, None)

    def insert(self, key):

        (pkey, ppointer) = self._pointers[bisect_right(self._keys, key)].insert(key)

        if pkey is not None:
            position = bisect_left(self._keys, pkey)
            (promoted_key, new_lateral_node) = self.insert_here(position, pkey, ppointer)
            return (promoted_key, new_lateral_node)
        return (None, None)

    def num_nodes(self):
        return 1 + sum(map(lambda n: n.num_nodes(), self._pointers))

    def num_leaves(self):
        return sum(map(lambda n: n.num_leaves(), self._pointers))

    def num_keys(self):
        return sum(map(lambda n: n.num_keys(), self._pointers))

    def height(self):
        return 1 + self._pointers[0].height()

    def __str__(self):
        return str(self._keys)


class BPlusTreeLeaf(object):
    def __init__(self, keys, next_leaf, capacity):
        self._keys = keys  # in sorted order
        self._next = next_leaf  # next BPlusTreeLeaf
        self._capacity = capacity

    def keys(self):
        all_keys = []
        for element in self._keys:
            all_keys.append(element)
        current = self
        while current._next is not None:
            current = current._next
            all_keys.extend(current._keys)
        return all_keys

    def find(self, key):
        return key in self._keys

    def insert(self, key):

        index = bisect_left(self._keys, key)
        if index == len(self._keys) or self._keys[index] != key:
            self._keys.insert(index, key)

        cap = self._capacity
        split_index = (cap + 1) // 2

        if len(self._keys) > cap:
            new_leaf = BPlusTreeLeaf(self._keys[split_index:], self._next, cap)

            self._keys = self._keys[:split_index]
            self._next = new_leaf

            return (new_leaf._keys[0], new_leaf)
        return (None, None)

    def num_nodes(self):
        return 0

    def num_leaves(self):
        return 1

    def num_keys(self):
        return len(self._keys)

    def height(self):
        return 0

    def __str__(self):
        return str(self._keys)


Test_tree = BPlusTree(3)
for i in [59, 97, 15, 44, 59, 72, 97, 10, 15, 21, 37, 44, 51, 59, 63, 72, 85, 91, 97]:
    Test_tree.insert(i)

Test_tree.insert(13)

Test_tree.insert(95)
Test_tree.insert(99)

2.1 B+树的定义

此处输入图片的描述

各种资料上B+树的定义各有不同, 一种定义方式是关键字个数和孩子节点的个数相同. 这里我们采用的是维基百科上所定义的方式, 即关键字个数比孩子节点个数小1, 这种方式是和B树基本等价的, 上图就是一颗阶数为4的B+树.

除此之外B+树还有以下要求:

  1. B+树包含两种类型的节点: 内部节点(也称为索引节点)和叶子节点. 根节点本身既可以是内部节点, 也可以是叶子节点. 根节点的关键字个数最少可以只有一个.
  2. B+树和B树最大的不同是内部节点不保存数据, 只用于索引, 所有数据(或者说是记录)都保存在叶子节点中.
  3. mB+树表示了内部节点(索引节点)最多有m-1个关键字(或者说内部节点最多有m个子树), 阶数m同时限制了叶子节点最多存储m-1个记录.
  4. 内部节点中的key都按照从小到大的顺序排列, 对于内部节点中的一个key,左树中所有的key都小于它, 右子树中的key都大于等于它. 叶子节点中的记录也按照key的大小排列.
  5. 每个叶子节点都存有相邻叶子节点的指针, 叶子节点本身依关键字的大小自小而大的顺序链接.

2.2 B+树的插入操作

  1. 若为空树, 创建一个叶子节点, 然后将记录插入其中, 此时这个叶子节点也是根节点, 插入操作结束.
  2. 针对叶子类型节点: 根据key值找到叶子节点, 向这个叶子节点插入记录。插入后,若当前结点key的个数小于等于m-1,则插入结束。否则将这个叶子结点分裂成左右两个叶子结点,左叶子结点包含前m/2个记录,右结点包含剩下的记录,将第m/2+1个记录的key进位到父结点中(父结点一定是索引类型结点),进位到父结点的key左孩子指针向左结点,右孩子指针向右结点。将当前结点的指针指向父结点,然后执行第3步。
  3. 针对索引类型结点:若当前结点key的个数小于等于m-1,则插入结束。否则,将这个索引类型结点分裂成两个索引结点,左索引结点包含前(m-1)/2个key,右结点包含m-(m-1)/2个key,将第m/2个key进位到父结点中,进位到父结点的key左孩子指向左结点, 进位到父结点的key右孩子指向右结点。将当前结点的指针指向父结点,然后重复第3步。

下面是一颗5阶B树的插入过程,5阶B数的结点最少2个key,最多4个key。


1.空树中插入5

此处输入图片的描述


2.一次插入8, 10, 15

此处输入图片的描述


3.插入16

此处输入图片的描述

插入16之后超过了关键字的个数限制, 所以需要进行分裂, 在叶子节点分裂的时候, 分裂出来的左节点两个记录, 右边三个记录, 中间key成为索引节点中的key, 分裂后当前节点指向了父节点(根节点), 结果如下图所示.

此处输入图片的描述

当然我们还有另一种分裂方式, 给左节点三个记录, 右节点两个记录,此时索引节点中的key就变为15.


4.插入17

此处输入图片的描述


5.插入18, 插入之后如下图所示.

此处输入图片的描述

当前节点的关键字个数大于5, 进行分裂. 分裂成两个节点, 左节点两个记录, 右节点三个记录, 关键字16进位到父节点(索引类型)中, 将当前节点的指针指向父节点.

此处输入图片的描述

当前节点的关键字个数满足条件, 插入结束.


6.插入若干条数据后

此处输入图片的描述


7.在上图中插入7, 结果如下图所示.

此处输入图片的描述

当前结点的关键字个数超过4,需要分裂。左结点2个记录,右结点3个记录。分裂后关键字7进入到父结点中,将当前结点的指针指向父结点,结果如下图所示。

此处输入图片的描述

当前节点的关键字个数超过4, 需要继续分裂, 左节点两个关键字, 右节点三个关键字, 关键字16进入到父节点中, 将当前节点指向父节点, 结果如下图所示.

当前节点的关键字满足条件, 插入结束.


2.3 B+树的删除操作

如果叶子节点中没有相应的key, 则删除失败. 否则执行下面的步骤.

  1. 删除叶子节点中对应的key. 删除后若节点的key的个数大于等于Math.ceil(m-1)/2 - 1, 删除操作结束, 否则执行第二步.
  2. 若兄弟节点key有富余(大于Math.ceil(m-1)/2 – 1),向兄弟结点借一个记录,同时用借到的key替换父结(指当前结点和兄弟结点共同的父结点)点中的key,删除结束。否则执行第3步。
  3. 若兄弟结点中没有富余的key,则当前结点和兄弟结点合并成一个新的叶子结点,并删除父结点中的key(父结点中的这个key两边的孩子指针就变成了一个指针,正好指向这个新的叶子结点),将当前结点指向父结点(必为索引结点),执行第4步(第4步以后的操作和B树就完全一样了,主要是为了更新索引结点)。
  4. 若索引结点的key的个数大于等于Math.ceil(m-1)/2 – 1,则删除操作结束。否则执行第5步.
  5. 若兄弟结点有富余,父结点key下移,兄弟结点key上移,删除结束。否则执行第6步
  6. 当前结点和兄弟结点及父结点下移key合并成一个新的结点。将当前结点指向父结点,重复第4步。注意,通过B+树的删除操作后,索引结点中存在的key,不一定在叶子结点中存在对应的记录。

下面是一颗5阶B树的删除过程,5阶B数的结点最少2个key,最多4个key。


1.初始状态
此处输入图片的描述


2.删除22, 删除后结果如下图.

此处输入图片的描述

删除后叶子结点中key的个数大于等于2,删除结束


3.删除15, 删除后的结果如下图所示.

此处输入图片的描述

删除后当前结点只有一个key,不满足条件,而兄弟结点有三个key,可以从兄弟结点借一个关键字为9的记录,同时更新将父结点中的关键字由10也变为9,删除结束。

此处输入图片的描述


4.删除7,删除后的结果如下图所示.

此处输入图片的描述

当前结点关键字个数小于2,(左)兄弟结点中的也没有富余的关键字(当前结点还有个右兄弟,不过选择任意一个进行分析就可以了,这里我们选择了左边的),所以当前结点和兄弟结点合并,并删除父结点中的key,当前结点指向父结点。

此处输入图片的描述

此时当前结点的关键字个数小于2,兄弟结点的关键字也没有富余,所以父结点中的关键字下移,和两个孩子结点合并,结果如下图所示。

此处输入图片的描述


为什么MySQL要用B+树做索引?

HASH的查找和删除的时间复杂度都是O(1). 但是如果出现HASH碰撞的话, 需要使用扩展链表法(JDK1.8之后, 如果链表长度大于8则转换为红黑树, 如果低于6则,转换为链表). 这样的话如果数据库中存储的数据量比较大, 那么发生HASH碰撞发生的次数就会越来越多. 时间复杂度会越来越高. 而且我们平时查询的时候经常会用到ORDER BY GROUP BY, 在这种排序查询的状态下, 他的时间复杂度就会变成O(N).

使用B+树的话, 因为它只有叶子节点保存数据, 所以可以将索引节点一次加载到内存当中, 而消耗很少的内存量. 同样大小的空间B+树可以比B树, 加载更多的索引节点. 而且B+树由于其叶子节点存储数据的原因, 它的查找每个元素所花费时间的方差也就更低. 这样也更稳定可控.

数据库引擎为InnoDB的时候, 数据库一般的层数为三层, 最多存储数据量为2.1千万. 如果大于这个数据量就需要分库分表, 否则层数大于四层, 会造成查询效率降低.

为什么不使用平衡二叉树/红黑树?

红黑树也属于平衡二叉树的一种, 因为在这种情况下, 树会变得很高, 在磁盘I/O比较慢的时候, 加载数据页造成的频繁的读写会降低系统的性能.


posted @ 2019-09-28 20:29  X-POWER  阅读(1706)  评论(0编辑  收藏  举报