树 Story —— 多路查找树

1,128 阅读9分钟

- 树 Story 第四篇 - 多路查找树

本文详细阐述了多路查找树原理,适合新手阅读,以及老手回顾。 全文三千字,阅读时间 20 分钟。

有别于二叉查找树,多路查找树的子节点不局限于 2 个,同时节点内的 key 不局限于 1个。

下面我们以 2-3 树(最简单的多路查找树)为例来讲述。

What ?!

有人可能第一次听说「2-3」树这个说法。

何为 2-3 树 ?

2-3树是最简单的B-树(或-树)结构,其每个非叶节点都有两个或三个子女,而且所有叶都在统一层上。2-3树不是二叉树,其节点可拥有3个孩子。不过,2-3树与满二叉树相似。高为h的2-3树包含的节点数大于等于高度为h的满二叉树的节点数,即至少有2^h-1个节点。——百度百科

每个节点里可以包含 1 或 2 个 key,子节点数量是父节点 key 的数量 +1,左子节点 key 小于父节点左 key,右子节点 key大于父节点右 key,中间子节点 key 大于左 key 小于右 key。

包含两个孩子的节点称为「2-节点」,二叉树中的节点都是2-节点。

包含三个孩子的节点称为「3-节点」。

注:文中提到的 key 为一个节点里的数据索引,一个节点可能会包括 2 个 key。

2-3 树的特型总结一下就是:

  1. 所有叶子节点在一个同一层。
  2. 如果一个非叶子节点有 1 个 key,其必有 2 个子节点,并大于左子节点,小于右子节点。
  3. 如果一个非叶子节点有 2 个 key,其必有 3 个子节点,左 key 大于左子节点,右 key 大于右子节点,中子节点大于左 key 并小于右 key。

为何 2-3 树 ?

2-3树与二叉树的区别为:

  1. 二叉树每个节点只有一个 key,而 2-3 树可以有 2个
  2. 二叉树的叶子节点可以不在同一层(满二叉树除外),而 2-3 树的叶子节点在同一层

如果没有 「3-节点」那么它就是一个棵「满二叉树」

2-3 树与二叉树的本质性区别就在于节点内包含多个 key,而这个特性意味着 2-3 树的一个节点可以包含更多的数据。而数据对于 IO 操作来说,是非常重要的。硬盘的一次 IO 操作,相比于 CPU 的计算效率来说,简直是太慢了。不仅会导致进程阻塞,而且在执行效率上会大打折扣。在大规模的数据集查找过程中,可能不能所有数据都要放到内存里,这样就会有硬盘 IO 的操作。而多路查找树相比于二叉树,有更少的查询次数。

通常在缓存查找树结构的时候,会以层为单位缓存,多路查找树单层的数据会存储更多数据,从而减少 IO 次数。比如上图,假如 2-3 树缓存了第二层节点 [10, 20 , 80],如果要查找数据 9,需要一次 IO:由 [10,20] 读到第三层 [9]。二叉树缓存根节点 [15, 80],则需要两次 IO: 先读取到第三层 [10, 20, 70, 90 ],再读到第四层 [9]。

综上我们可以看到,多路查找树可以为数据查找节省更多的 IO 操作。由于每个节点内 key 的数量是有限的,所以多路查找树的时间复杂度与二叉查找树是一样的: O(logN)

插入 Key

因为 2-3 树是多路查找树,所以插入 Key 的节点的位置肯定是在叶子节点上。那么插入的情形有如下两种情况:

  1. 插入 Key 的节点包含一个 key,则直接插入。
  2. 插入 Key 的节点包含两个 key,则需要将节点分裂,形成单 key 两子节点的结构,并将新节点视为「新插入 Key 的节点」,参照情况 1 进行处理。

1. 插入 Key 的节点包含一个 key,则直接插入。

2. 插入 Key 的节点包含两个 key,则需要将节点分裂,形成单 key 两子节点的结构,并将新节点视为「新插入 Key 的节点」,参照情况 1 进行处理。

分裂 [6,9] 节点

插入节点和父节点都已经包含了两个 key

如果向上抛到根节点,key 超出了限制,无法满足 2-3 树的要求,说明当前 2-3 树已经不能容纳要存储的数据,则需要将根节点分裂,新增一层,策略同情况 2。

删除 Key

删除 Key 的节点包含四种情况:

  1. 包含 1个 key
  2. 包含 2个 key
  3. 叶子节点
  4. 非叶子节点

删除 与插入不同,插入只能从叶子节点插入,然后向上平衡。而删除可以在叶子节点删除,也可以在非叶子节点删除,所以删除的情况会更复杂一些。

  1. 删除 key 的节点是叶子节点,且有两个 key ,那么直接删除 key 即可。
  2. 删除 key 的节点是非叶子节点,那么需要找到要删除 key 的后继 key 并交换两个 key,然后再继续判断删除此 key (位置已经交换)的情形。
  3. 删除 key 的节点是叶子节点,且只有一个 key,那么删除 key 等于删除节点。叶子节点被删除后,2-3 树被破坏,需要重新平衡。

后继节点交换后,如果叶子节点包含 2 个 key,则可以直接删除此 key。但是如果叶子节点只包含 1 个 key,删除 key 后,需要重新平衡。

1. 删除 key 的节点是叶子节点,且有两个 key 。

2. 删除 key 的节点是非叶子节点。

需要找到要删除 key 的后继 key 并交换两个 key,然后再继续判断删除此 key (位置已经交换)的情形。

当删除 key 所在节点只有 1 个 key时,删除 key 等同于删除节点。这时需要重新平衡 2-3 树

3. 重新平衡删除 key 的节点是叶子节点,且只有一个 key

那么删除 key 等于删除节点。叶子节点被删除后,2-3 树被破坏,需要重新平衡

重新平衡时需要参考父节点和兄弟节点的 key 的个数:

  1. 父节点 1 个 key,兄弟节点 2 个 key。则将兄弟节点分裂,与父节点重新构成 2-3 树结构。

  2. 父节点 1 个 key,兄弟节点 1 个 key。则将兄弟节点与父节点合并为一个节点,对新节点遵循1、2、3种父节点和子节点的情况再进行平衡。

  3. 父节点 2 个 key,兄弟节点有「左中右」三种情况。由于左与右是对称的,所以又延伸里的 4 种子情况。

    i. 删除节点为右(左)节点,且中间节点只有 1 个 key,则父节点中的右(左) key 与中间节点合并。此时父节点有 1 个 key,2个子节点,满足 2-3 树。

    ii. 删除节点为右(左)节点,且中间节点有 2个 key,则父节点的右(左) key 下移作为新右节点,中间节点(子树)的右(左) key 与父节点合并。此时父节点有 2 个 key,3 个子节点,满足 2-3 树。

    iii. 删除节点为中间节点,且右(左)节点有 1 个 key,则父节点的右(左) key 下移,与右(左)节点合并。此时父节点有 1 个 key,2 个子节点,满足 2-3 树。

    iv. 删除节点为中间节点,且右(左)节点有 2 个 key,则父节点的右(左)key 下移作为新中间节点,右(左)节点(子树)左(右)key 与父节点合并。此时父节点有 2 个 key,3 个子节点,满足 2-3 树。

注:以上注明节点(子树)代表在递归情况下,并不是选择节点的 key,而且选择子树的最大(右)或最小(左)key

1. 父节点 1 个 key,兄弟节点 2 个 key。

则将兄弟节点分裂,与父节点重新构成 2-3 树结构。

兄弟节点分裂后,左 key 节点为新父节点,父节点下移为新左节点,右 key 节点仍为新右节点

2. 父节点 1 个 key,兄弟节点 1 个 key。

则将兄弟节点与父节点合并为一个节点,对新节点遵循1、2、3种父节点和子节点的情况再进行平衡。

3. 父节点 2 个 key

兄弟节点有「左中右」三种情况。由于左与右是对称的,所以又延伸里的 4 种子情况。

i. 删除节点为右(左)节点,且中间节点只有 1 个 key,则父节点中的右(左) key 与中间节点合并。此时父节点有 1 个 key,2个子节点,满足 2-3 树。

ii. 删除节点为右(左)节点,且中间节点有 2个 key,则父节点的右(左) key 下移作为新右节点,中间节点的右(左) key 与父节点合并。此时父节点有 2 个 key,3 个子节点,满足 2-3 树。

iii. 删除节点为中间节点,且右节点(左节点)有 1 个key,则父节点的右(左) key 下移,与右节点(左节点)合并。此时父节点有 1 个 key,2 个子节点,满足 2-3 树。

iv. 删除节点为中间节点,且右(左)节点有 2 个 key,则父节点的右(左)key 下移作为新中间节点,右(左)节点左(右)key 与父节点合并。此时父节点有 2 个 key,3 个子节点,满足 2-3 树。

简而言之,删除节点后,主要的操作有:

  • 父节点 key 下移成为新子节点
  • 子节点 key 上移与父节点合并

总结

2-3 树是最简单的多路查找树,多路查找树相比于二叉树,有更低的层级,更宽的数据范围。这些特性恰当的解决了由于树比较高在磁盘 IO 中频繁读写的问题。除了 2-3树,还有 2-3-4树,与2-3树大同小异,2-3-4 树单节点的 key 数量比 2-3 树多一个,子节点同时也可以多一个。

不难看出 MySQL 里面常用的 B 树或 B+ 树的结构也是一个多路查找树,详情请看下一篇「B 树」。

练习题

删除节点 70