数据结构 - 树

原文连接: blog.wangriyu.wang/2018/06-Tre…html

与数据库相关的树结构主要为 B 类树,B 类树一般用于数据库和操做系统的文件系统mysql

在学习 B 类树以前先复习一下二叉查找树的概念和红黑树golang

二叉树

二叉树 - Binary Tree 是每一个节点最多只有两个分支(即不存在分支度大于 2 的节点)的树结构。面试

分类

  • 完美二叉树 (Perfect Binary Tree): 除了叶子结点以外的每个结点都有两个孩子,每一层(固然包含最后一层)都被彻底填充
  • 彻底二叉树 (Complete Binary Tree): 除了最后一层以外的其余每一层都被彻底填充,而且全部结点都保持向左对齐
  • 满二叉树 (Full/Strictly Binary Tree): 除了叶子结点以外的每个结点都有两个孩子结点

遍历

  • 前序遍历: 首先访问根结点而后遍历左子树,最后遍历右子树。在遍历左、右子树时,仍然先访问根结点,而后遍历左子树,最后遍历右子树
  • 中序遍历: 首先遍历左子树,而后访问根结点,最后遍历右子树。在遍历左、右子树时,仍然先遍历左子树,再访问根结点,最后遍历右子树
  • 后序遍历: 首先遍历左子树,而后遍历右子树,最后访问根结点。在遍历左、右子树时,仍然先遍历左子树,而后遍历右子树,最后遍历根结点
  • 深度优先搜索: 顾名思义,查找时深度优先,从根结点访问最远的结点直到找到全部节点。前序,中序和后序遍历都是深度优先遍历的特例
  • 广度优先搜索: 广度优先遍历会先访问离根节点最近的节点,二叉树的广度优先遍历又称按层次遍历。算法借助队列实现

二叉查找树

二叉查找树 - Binary Search Tree: 也称二叉搜索树、有序二叉树。对于根树和全部子树都知足,每一个节点都大于左子树元素,而小于右子树元素,且没有键值相等的结点算法

搜索、插入、删除的复杂度等于树高,指望 O(\log_2^n),最坏 O(n)(数列有序,树退化成线性表)sql

二叉查找树动态展现: visualgo.net/zh/bst数据库

缺陷

当数据基本有序时,二叉查找树会退化成线性表,查找效率严重降低编程

因此后面出现了不少改进的平衡树结构以知足树高最坏也为 O(\log_2^n), 如伸展树 (Splay Tree)、平衡二叉树 (SBT)、AVL 树、红黑树等数据结构

红黑树

红黑树 - Red–black tree 是一种自平衡二叉查找树,除了符合二叉查找树的性质外,它还知足如下五条性质:性能

  1. 每一个结点要么是红的,要么是黑的
  2. 根结点是黑的
  3. 每一个叶子结点是黑的(叶子结点指树尾端 NIL 指针或 NULL 结点,不包含数据,只充当树在此结束的指示)
  4. 若是一个结点是红的,那么它的两个子节点都是黑的 (从根到每一个叶子的全部路径上不能有两个连续的红色节点)
  5. 对于任一结点而言,其到叶结点树尾端 NIL 指针的每一条路径都包含相同数目的黑结点

一棵红黑树

平衡优点

上述约束确保了红黑树的关键特性: 从根到叶子的最长路径不会超过最短路径的两倍

证实: 主要看性质 4 和 性质 5,假设从根到叶子的最短路径 a 上有黑色节点 n 个,最长路径 b 确定是交替的红色和黑色节点,而根据性质 5 可知从根到叶子的全部路径都有相同数目的黑色节点, 这就代表 b 的黑色节点也为 n 个,但 b 出现的红色节点不可能超过黑色节点个数,不然会破坏性质 4 (抽屉原理),因此从根到叶子的最长路径不会超过最短路径的两倍

调整

由于每个红黑树也是一个特化的二叉查找树,所以红黑树上的只读操做与普通二叉查找树上的只读操做相同。然而,在红黑树上进行插入操做和删除操做会致使再也不匹配红黑树的性质。 恢复红黑树的性质须要少许 O(\log_2^n) 的颜色变动(实际是很是快速的)和不超过三次树旋转(对于插入操做是两次)。虽然插入和删除很复杂,但操做时间仍能够保持为 O(\log_2^n) 次。

红黑树发生变动时须要 [变色] 和 [旋转] 来调整,其中旋转又分 [左旋] 和 [右旋]。

  • 变色就是更改颜色
  • 左旋: 以 X 为支点逆时针旋转红黑树的两个节点 X-Y,使得父节点被本身的右孩子取代,而本身降低为左孩子

左旋

  • 右旋: 以 X 为支点顺时针旋转红黑树的两个节点 X-Y,使得父节点被本身的左孩子取代,而本身降低为右孩子

右旋

旋转过程当中只须要作三次指针变动就行

插入和删除

插入节点

插入节点的位置跟二叉查找树的寻找方法基本一致,若是插入结点 z 小于当前遍历到的结点,则到当前结点的左子树中继续查找,若是 z 大于当前结点,则到当前结点的右子树中继续查找, 若是 z 依然比此刻遍历到的新的当前结点小,则 z 做为当前结点的左孩子,不然做为当前结点的右孩子。而红黑树插入节点后,为了保持约束还须要进行调整修复(变色加旋转)。

因此插入步骤以下: 红黑树按二叉查找树的规则找到位置后插入新节点 z,z 的左孩子、右孩子都是叶子结点 nil, z 结点初始都为红色,再根据下述情形进行变色旋转等操做,最后达到平衡。

  • 情形 1: 若是当前节点是根结点,为知足性质 2,因此直接把此结点 z 涂为黑色
  • 情形 2: 若是当前结点的父结点是黑色,因为不违反性质 2 和性质 4,红黑树没有被破坏,因此此时也是什么也不作

好比上图插入 12 时知足情形 2:

插入 12

如下情形须要做出额外调整:

  • 情形 3: 若是当前结点的父结点是红色祖父结点的另外一个子结点(叔叔结点)是红色
  • 情形 4: 当前结点的父结点是红色叔叔结点是黑色或者 nil,当前结点相对其父结点的位置和父节点相对祖父节点的位置不在同侧
  • 情形 5: 当前结点的父结点是红色叔叔结点是黑色或者 nil,当前结点相对其父结点的位置和父节点相对祖父节点的位置在同侧

下面着重讲讲后三种状况如何调整

情形 3

当前结点的父结点是红色且祖父结点的另外一个子结点(叔叔结点)是红色

由于当前节点的父节点是红色,因此父节点不多是根节点,当前节点确定有祖父节点,也就有叔叔节点

解决步骤: 将当前结点的父结点和叔叔结点涂黑,祖父结点涂红,再把祖父结点当作新节点(即当前节点的指针指向祖父节点)从新检查各类情形进行调整

因为对称性,无论父结点是祖父结点的左子仍是右子,当前结点是其父结点的左子仍是右子,处理都是同样的

咱们插入 21 这个元素,当前节点指向 21:

插入 21

此时会发现 2一、22 两个红色相连与性质 4 冲突,但 21 节点知足情形 3,修复后:

调整情形 3

此时当前节点指向 21 的祖父节点,即 25。而 25 节点一样遇到情形 3 的问题,继续修复:

继续调整情形 3

此时当前节点指向根节点,知足情形 1,将 14 节点涂黑便可恢复红黑树平衡

情形 4

当前结点的父结点是红色,叔叔结点是黑色或者 nil,当前结点相对其父结点的位置和父节点相对祖父节点的位置不在同侧

解决步骤:

  • 若是当前节点是父节点的右子,父节点是祖父节点的左子,以当前结点的父结点作为新结点(即当前节点的指针指向父节点),并做为支点左旋
  • 若是当前节点是父节点的左子,父节点是祖父节点的右子,以当前结点的父结点作为新结点(即当前节点的指针指向父节点),并做为支点右旋

在上图的基础上咱们继续插入 5 这个元素:

插入 5

能够看出 5 是父节点的左子,而父节点是祖父节点的右子,不一样侧则为情形 4,将当前节点指向 5 的父节点 6,并以 6 为支点进行右旋:

插入情形 4

此时当前节点是 6,而 6 是父节点 5 的右子,父节点 5 也是祖父节点 1 的右子,同侧则转为情形 5,继续往下看

情形 5

当前结点的父结点是红色,叔叔结点是黑色或者 nil,当前结点相对其父结点的位置和父节点相对祖父节点的位置在同侧

解决步骤:

  • 首先把父结点变为黑色,祖父结点变为红色
  • 若是当前节点是父节点的左子,父节点是祖父节点的左子,以祖父结点为支点右旋
  • 若是当前节点是父节点的右子,父节点是祖父节点的右子,以祖父结点为支点左旋

在上一张图的基础上修改节点 5 为黑色,节点 1 为红色,再以 1 为支点左旋:

插入情形 5

此时便恢复平衡

删除节点

删除节点 X 时第一步先判断两个孩子是否都是非空的,若是都非空,就先按二叉查找树的规则处理:

在删除带有两个非空子树的节点 X 的时候,咱们能够找到左子树中的最大元素(或者右子树中的最小元素),并把这个最值复制给 X 节点,只代替原来要删除的值,不改变节点颜色。

而后咱们只要删除那个被复制出值的那个节点就行,由于是最值节点因此它的孩子不可能都非空。

由于只是复制了一个值,不违反任何性质,这就把原问题转化为如何删除最多有一个非空子树的节点的问题。它不关心这个节点是最初要删除的节点仍是被复制出值的那个节点。

咱们以图为例,图中三角形表明可能为空的子树:

取值复制

节点 X 是要删除的节点,发现它的两个子树非空,咱们能够找左子树中最大的元素 Max (也能够找右子树中最小的元素 Min),把 Max 值(或者 Min 值)复制到 X 上覆盖原来的值,不修改其余属性,而后删除 Max 节点(或 Min 节点)便可,能够很清楚的看到最值节点最多只会有一个非空子树


接下来就是如何处理删除最多有一个非空子树的节点 X 的问题

简单情形:

  1. 若是 X 的两个儿子都为空,即均为叶子,咱们将其中任意一个看做它的儿子
  2. 若是 X 是一个红色节点,它的父亲和儿子必定是黑色的,因此简单的用它的黑色儿子替换它就行,这并不会破坏性质 3 和性质 4,经过被删除节点的全部路径只是少了一个红色节点,这样能够继续保证性质 5
  3. 若是 X 是黑色而它的儿子是红色,若是只是删除这个黑色节点,用它的红色儿子代替的话,会破坏性质 5,咱们能够重绘它的儿子为黑色,则曾经经过 X 的全部路径将经过它的黑色儿子,这样能够继续保持性质 5

若是 X 和它的儿子都是黑色,这是一种复杂的状况,咱们单拎出来说

咱们首先把要删除的节点 X 替换为它的儿子。出于方便,称呼这个新上位的儿子为 N,称呼它的兄弟为 S,使用 P 称呼 N 的新父亲,SL 称呼 S 的左儿子,SR 称呼 S 的右儿子

有如下六种情形须要考虑:

情形 1

N 是新的根

咱们不须要作什么,由于全部路径都去除了一个黑色节点,而新根也是黑色的,因此性质都保持着

情形 二、五、6 涉及到左右不一样的状况,只取一种处理

情形 2

S 是红色

  • 交换兄弟 S 和父亲 P 的颜色
  • 若是 N 是其父亲的左节点,咱们在 N 的父亲上作左旋,把红色兄弟转换成 N 的祖父
  • 若是 N 是其父亲的右节点,咱们在 N 的父亲上作右旋,把红色兄弟转换成 N 的祖父

删除情形 2

完成这两个操做后,尽管全部路径上黑色节点的数目没有改变,但如今 N 有了一个黑色的兄弟和一个红色的父亲,因此咱们能够接下去按情形 四、情形 5 或情形 6 来处理

情形 3

N 的父亲、S 和 S 的儿子都是黑色的

  • 重绘 S 为红色
  • 将 P 做为新的 N,从情形 1 开始,在 P 上作平衡处理

删除情形 3

在这种情形下,咱们简单的重绘 S 为红色。结果是经过 S 的全部路径都少了一个黑色节点。这与删除 N 的初始父亲 X 形成经过 N 的全部路径少了一个黑色节点达成平衡。可是,经过 P 的全部路径如今比不经过 P 的路径少了一个黑色节点,因此仍然违反性质 5。要修正这个问题,咱们要从情形 1 开始,在 P 上作从新平衡处理

情形 4

S 和 S 的儿子都是黑色,可是 N 的父亲是红色

  • 交换 N 的兄弟 S 和父亲 P 的颜色

删除情形 4

在这种情形下,咱们简单的交换 N 的兄弟和父亲的颜色。这不影响不经过 N 的路径的黑色节点的数目,可是它在经过 N 的路径上对黑色节点数目增长了一,添补了在这些路径上删除的黑色节点

情形 5

S 是黑色,S 的其中一个儿子是红色,且红色儿子的位置与 N 相对于父亲的位置处于同侧

  • 若是 N 是其父亲的左节点,S 的左儿子是红色,右儿子是黑色,则在 S 上作右旋转
  • 若是 N 是其父亲的右节点,S 的左儿子是黑色,右儿子是红色,则在 S 上作左旋转
  • 将 S 和它以前的红色儿子交换颜色

删除情形 5

全部路径仍有一样数目的黑色节点,可是如今 N 有了一个黑色兄弟,且兄弟的一个儿子仍为红色的,其位置与 N 相对于父亲的位置处于不一样侧,进入情形 6

情形 五、6 中父节点 P 的颜色能够为黑色也能够是红色

情形 6

S 是黑色,S 的其中一个儿子是红色,且其位置与 N 相对于父亲的位置处于不一样侧

  • 交换 N 的父亲 P 和 S 的颜色
  • 若是 N 是其父亲的右节点,S 的左儿子是红色,右儿子是黑色,则在 N 的父亲上作右旋转,并使 S 的左儿子涂黑
  • 若是 N 是其父亲的左节点,S 的左儿子是黑色,右儿子是红色,则在 N 的父亲上作左旋转,并使 S 的右儿子涂黑

删除情形 6

交换前 N 的父亲能够是红色也能够是黑色,交换后,N 增长了一个黑色祖先,因此经过 N 的路径都增长了一个黑色节点,S 的右子树黑色节点个数也没有变化,达到平衡

实例

仍是以以前的图为例

删除

咱们自下而上开始尝试删除每个节点:

  • 假如要删除元素 1,根据简单情形中的第二条,咱们直接删除 1,并用一个 nil 节点代替便可,元素 六、十二、21 的处理与此相同

  • 假如要删除元素 5,由于左右子树均不为空,因此找左子树的最大值 1 (或者右子树的最小值 6),用找到的值代替 5 (这里只是值替换,其余均不变),而后去删除 1 节点,这就转到问题 1 上了

  • 假如要删除元素 11,根据简单情形的第三条,咱们直接删除 11,并用子节点 12 代替,同时把 12 涂黑便可,元素 22 的处理与此相同

  • 假如要删除元素 25,由于左右子树均不为空,因此找左子树的最大值 22 (或者右子树的最小值 27),咱们这里用值 22 代替 25,颜色不变。而后去删除 22 节点,这变成上一个问题了

  • 假如要删除元素 27,黑色的 nil 叶子节点代替 27 节点,由于兄弟节点 22 有一个红色孩子,且在左边,和 nil 节点相对父亲 25 的位置不一样侧,属于情形 6,因此第一步交换 22 和 25 的颜色,再以 25 为支点作右旋转,而后将 21 节点涂黑便可

  • 假如要删除元素 8,选择右子树最小值 11 替换 8。而后去删除节点 11,对应问题 3

  • 假如要删除元素 17,选择左子树最大值 15 替换 17。而后去删除节点 15,过程看下一个问题

  • 假如要删除元素 15,删除的元素和替代的元素都是黑色,这属于复杂情形。检查其类型能够匹配到情形 2,元素 15 是被移除的 X,代替它的是 nil 节点,即为 N,17 为 P,25 为 S,根据上文可知第一步先交换 P 和 S 的颜色,而后以 P 为支点进行左旋,此时 N 多了一个黑色的兄弟 22 和红色的父亲 17:

删除 15

此时 N 的兄弟 S 变为 22,P 变为 17,S 的左孩子是红色的 21,属于情形 5。S 作右旋转,并交换 22 和 21 的颜色:

删除 15

此时 N 的兄弟 S 变为黑色的 21,但 21 的红色孩子节点 22 变为右侧,进入情形 6

删除 15

P 节点 17 作左旋转,并将 S 的右节点涂黑,此时树恢复平衡

  • 假如要删除根节点 14,取左子树最大值 12 代替 14。而后去删除节点 12,对应问题 1

至此,咱们已经把节点都删了个遍,相信你对红黑树的删除操做应该了解了

红黑树动态展现: www.cs.usfca.edu/~galles/vis…

实际问题

红黑树仍是典型的二叉搜索树结构,主要应用在一些 map 和 set 类型的实现上,好比 Java 中的 TreeMap 和 C++ 的 set/map/multimap 等。其查找的时间复杂度 O(\log_2^n) 与树的深度相关,下降树的深度能够提升查找效率。

Java 的 hashmap 和 golang 的 map 是用哈希实现的

可是大规模数据存储中,实现索引查询这样一个实际背景下,树节点存储的元素数量是有限的(若是元素数量很是多的话,查找就退化成节点内部的线性查找了), 这样致使二叉查找树结构因为树的深度过大而形成磁盘 I/O 读写过于频繁,进而致使查询效率低下,所以咱们该想办法下降树的深度,从而减小磁盘查找存取的次数。

一个基本的思想就是:采用多叉树结构,因此出现了下述的平衡多路查找树

B 树 (B - Tree)

B-树,即为 B 树,不要读做 B 减树

B 树与红黑树最大的不一样在于,B 树的结点能够有许多子女,从几个到几千个。

定义

B 树的定义有两种,一种以阶数为限制的 B 树(下文所述的),一种以度数为限制的 B 树(算法导论所描述的),二者原理相似,这里以阶数来定义

B 树属于平衡多路查找树。一棵 m 阶(m 阶即表明树中任一结点最多含有 m 个孩子)的 B 树的特性以下:

  1. 除根节点外全部节点关键字个数范围: [\lceil\frac m2\rceil-1, m-1]
  2. 若非叶子节点含 n 个关键字,则子树有 n+1 个,由关键字范围可知子树的个数范围: [\lceil\frac m2\rceil, m]
  3. 根节点至少包含一个关键字,至少有两个孩子(除非 B 树只存在一个节点: 根结点),即根节点关键字个数范围: [1, m-1],孩子数范围: [2, m]
  4. 全部叶子节点都处在同一层,即高度都同样
  5. 每一个节点中的关键字从小到大排列,节点当中 k-1 个元素正好是 k 个孩子包含的元素的值域划分

2-3-4 树

如图是一个典型的 2-3-4 树结构,也是阶为 4 的 B 树。从图中查询元素最多只须要 3 次磁盘 I/O 就能够访问到咱们须要的数据节点,将节点数据块读入内存后再查找指定元素会很快。若是一样的数据用红黑树表示,树高会增加不少,形成遍历节点的次数增多,访问磁盘的次数增多,查找性能会降低。

对于一棵包含 n 个元素、高度为 h 、阶数为 m 的 B 树: 影响 B 树高度的是每一个结点所包含的子树数,若是尽量使结点孩子数都等于 \lceil\frac m2\rceil,则层数最多,为最坏状况;若是尽量使结点孩子数都等于 m,则层数最少,为最好状况。因此有

\log _m{(n + 1)} \leq h \leq \log _{\lceil\frac m2\rceil}{(\frac{n + 1}{2})} + 1

底数 \lceil\frac m2\rceil 能够取很大,好比 m 能够达到几千,从而在关键字数必定的状况下,使得最终的 h 值尽可能比较小,树的高度比较低。

实际运用中 B 树中的每一个结点根据实际状况能够包含大量的关键字信息和分支(但不能超过磁盘块的大小,根据磁盘驱动的不一样,通常块的大小在 1k~4k 左右);这样树的深度下降了,意味着查找一个元素只要不多的结点从外存磁盘中读入内存,就能够很快地访问到要查找的数据

查找

一个节点的结构能够定义为:

type BTNode struct {
  KeyNum   int       // 关键字个数,math.Ceil(m/2)-1 <= KeyNum < 阶数 m
  Parent   *BTNode   // 指向父节点的指针
  IsLeaf   bool      // 是否为叶子,叶子节点 children 为 nil
  Key      []int     // 关键字切片,长度为 KeyNum
  Children []*BTNode // 子节点指针切片,长度为 KeyNum+1
}
复制代码

以上面 2-3-4 树的根节点为例:

B 树节点

 全部数据以块的方式存储在外磁盘中,咱们经过 B 树来查找数据时,每遍历到一个节点,便将其读入内存,比较其中的关键字,若能匹配到咱们要找的元素,便返回;若未能找到,经过比较肯定在哪两个关键字的值域区间, 便可肯定子树的节点指针,继续往下找,把下一个节点的数据读入内存,重复以上步骤

插入

对于一棵 m 阶的 B 树来讲,插入一个元素(或者叫关键字)时,首先判断在 B 树中是否已存在,若是存在则不插入;若是不存在,则在对应叶子结点中插入新的元素,须要判断是否会超出关键字个数限制(m-1)

插入步骤:

  1. 根据元素大小查找插入位置,确定是最底层的叶子节点,将元素插入到该节点中
  2. 若是叶子节点的关键字个数小于等于 m-1,说明未超出限制,插入结束;不然进入下一步
  3. 若是叶子节点的关键字个数大于 m-1 个,以结点中间的关键字为中心分裂成左右两部分,而后将这个中间的关键字插入到父结点中,这个关键字的左子树指向分裂后的左半部分,这个关键字的右子树指向分裂后的右半部分。
  4. 而后将当前结点指向父结点,若是插入刚才的中间关键字后父节点的关键字个数也超出限制,继续进行第 3 步;不然结束插入

仍是以上面的 2-3-4 树(阶数 m = 4)为例,咱们依次插入元素

  • 首先插入 一、二、3,由于关键字个数均未超过 m-1,因此直接插入便可:

插入 1 2 3

  • 当插入 4 时,该节点关键字个数达到 m,须要分裂,这里能够选 3 (也能够选 2) 做为中间字,分裂后:

插入 4

  • 继续插入 五、7,对应 4 所在的叶子节点:

插入 5 7

  • 当插入 8 时,也须要分裂,将中间字 5 上移至父节点,4 成为 5 的左区间子树,7 8 成为 5 的右区间子树:

插入 8

以后的步骤相似,再也不一一叙述

删除

删除操做是指删除 B 树中的某个节点中的指定关键字

删除步骤:

  1. 若是当前要删除的关键字位于非叶子结点 N 上,则用后继最小关键字(找前继最大关键字也能够)覆盖要删除的关键字,而后在后继关键字所在的子树中删除该后继关键字。此后继关键字必定位于叶子结点上,这个过程和二叉搜索树删除结点的方式相似。删除这个后继关键字后进入第 2 步,若是本来要删除的关键字自己就位于叶子上一样删除关键字后进入第二步
  2. 该结点(假设为 M)关键字个数大于等于 math.Ceil(m/2)-1,结束删除操做,不然进入第 3 步
  3. 此时结点 M 关键字个数小于 math.Ceil(m/2)-1
    • 若是相邻兄弟结点(左右均可以)关键字个数大于 math.Ceil(m/2)-1,则父结点中取一个临近的关键字下移到 M,兄弟结点中取一个临近关键字上移至父节点,删除操做结束;
    • 若是相邻的兄弟节点关键字个数都不大于 math.Ceil(m/2)-1,将父结点中临近的关键字 key 下移至 M,合并 M 和它的兄弟节点造成一个新的结点。原父结点中的 key 的两个孩子指针就变成一个孩子指针,指向这个新结点。而后当前结点的指针指向父结点,重复第 2 步

以上面的 2-3-4 树为例

2-3-4 树

阶数为 4,节点关键字个数范围应该是 [1, 3],即 math.Ceil(m/2)-1 = 1

  • 删除关键字 2 或者 8,不影响节点

删除 2 和 8

  • 删除关键字 4,该叶子节点 X 关键字个数变为 0 小于范围下界,同时左右两个相邻兄弟的关键字个数都不大于 1,须要合并节点。
    • 第一步,将父节点的 5 下移到 X 上
    • 第二步,合并 X 和右兄弟节点 7 造成一个包含 五、7 的新节点
    • 第三步,父节点中本来 5 的左右两个孩子指针变为一个并指向这个新节点

删除 4

这里第一步也能够选择下移 3,而后第二步跟左兄弟合并成 一、3 节点

  • 继续删除 1,此时与上一个问题不一样,该叶子节点的兄弟有富余的关键字,咱们只须要把父节点的临近的一个关键字下移到该叶子节点代替删除的元素,而后把兄弟节点的一个临近关键字上移至父节点便可,这个操做有点相似红黑树的左旋操做

删除 1

  • 如今尝试删除非叶子节点 5,用后继最小关键字 7 代替 5,而后删除 7 所在的叶子节点。
    • 此时会引发连锁反应,7 所在的叶子节点如今为空,而兄弟节点关键字又不大于 1,须要合并
    • 将关键字 7 又从父节点移至原来的叶子上,合并成含 三、7 的新节点,假设新节点为 N,父节点的孩子指针变为一个并指向 N
    • 而父节点如今关键字是空的,并且其兄弟(N 的叔叔)关键字也不大于 1,也须要合并
    • 根节点取出关键字 9 下移到 N 的父节点上,合并 N 的父节点和叔叔节点,产生一个包含 九、15 的新节点,根节点的孩子指针减小一个且左子树指向这个新节点

删除 5

删除操做就演示到这,B 树的内容讲完

B 树动态展现: www.cs.usfca.edu/~galles/vis…

B+ 树 (B+ - Tree)

B+ 树 是基于 B 树的变体,查找性能更好

同为 m 阶的 B+ 树与 B 树的不一样点:

  1. 全部非叶子节点,每一个节点最多有 m 个关键字,最少有 \lceil\frac m2\rceil 个关键字(比 B 树的限制多一个),其中每一个关键字对应一棵子树
  2. 全部的非叶子结点能够当作是索引部分,结点中仅含有其子树根结点中最大(或最小)关键字,不包含关键字数据的指针(B 树是包含这个指针的)
  3. 全部的叶子结点中包含了所有关键字的信息,及指向含这些关键字记录的指针,且叶子结点自己依关键字的大小自小到大顺序连接.(而 B 树的所有关键字信息分散在各个节点中)

B+ 树

如图所示的是将以前的 2-3-4 树的数据存到 B+ 树结构中的示意图,叶子节点保存了全部关键字信息而且叶子节点之间也用指针链接起来(一个顺序链表),而全部非叶子节点只包含子树根节点中对应的最大关键字,其做用只是用于索引

B+ 树还能够用另外一种形式定义:

中间节点最多有 m-1 个关键字,最少有 \lceil\frac m2\rceil-1 个关键字,与 B 树相同; 可是非叶子节点的关键字是左子树的最大关键字(或者右子树的最小关键字),与刚才的情形不一样

好比一样的数据此定义的 B+ 树表现形式以下:

B+ 树

这种形式中间节点占用更少,可能更常见一点,不过下面的讲解是按第一种定义来

优点

B+ 树比 B 树更适合实际应用中操做系统的文件索引和数据库索引

  1. B+ 树索引节点能够存储更多的关键字,磁盘 I/O 能够更少

数据库中关键字可能只是某个数据列的索引信息(好比以 ID 列建立的索引),而索引指向的数据记录(某个 ID 对应的数据行)咱们称做卫星数据,推荐看下博文 数据库的最简单实现MySQL索引背后的数据结构及算法原理

B- 树中间节点和叶子节点都会带有关键字和卫星数据的指针,B+ 树中间节点只带有关键字,而卫星数据的指针均放在叶子节点中

由于没有卫星数据的指针,因此 B+ 树内部结点相对 B 树占用空间更小。若是把全部同一结点的关键字存放在同一盘块中,那么对于 B+ 树来讲盘块所能容纳的关键字数量也就更多,一次性读入内存中时能查找的关键字也就更多。相对来讲 IO 读写次数也就下降了,性能就提高了。

举个例子,假设磁盘中的一个盘块能容纳 16 bytes,而一个关键字占 2 bytes,一个卫星数据指针占 2bytes。对于一棵 9 阶 B 树来讲,一个结点最多含 8 个关键字(8*4 bytes),即一个内部结点须要 2 个盘块来存储。而对于 B+ 树来讲,内部结点不含卫星数据的指针,因此一个内部节点只须要 1 个盘块。当须要把内部结点读入内存中的时候,B 树就比 B+ 树多一次盘块查找时间

  1. B+ 树的查询效率更加稳定

因为非叶子节点并非最终指向文件内容的结点,而只是叶子结点中关键字的索引。因此任何关键字的查找必须走一条从根结点到叶子结点的路径。全部关键字查询的路径长度相同,致使每个数据的查询效率至关。而 B 树查找一个文件时查找到的路径长度是不一的。

  1. B+ 树对范围查询操做更友好

若是是查找单一元素,B+ 树的查找过程与 B 树相似,只是每次查找都是从根查到叶

而进行范围查询的操做时,B+ 树只要遍历叶子节点就能够实现整棵树的遍历,而 B 树的范围查询要经过中序遍历,效率比较低下

插入

B+ 树的插入与 B 树相似,先寻找关键字对应的位置插入,须要注意的是插入比当前子树的最大关键字还大的数时要修改祖先节点对应的关键字,由于 B+ 树内部结点存的是子树的最大关键字

好比在上面给出的 B+ 树中插入 105 这个元素,由于 105 大于当前子树最大关键字 101,因此须要修改父节点和祖父节点的边界关键字:

插入 105

  • 若是插入元素的节点未超出上界限制,则结束;不然将节点分裂,中间节点上移到父节点中,再判断父节点是否须要调整

好比刚才插入 105 的叶子节点关键字个数达到 4 个,须要分裂,这里分裂与 B 树略有不一样。B 树是把节点按中间节点分红三份,再把中间节点上移;而 B+ 树是分红两份,再把左半节点的最大关键字添加进父节点

分裂叶子

此时父节点也须要分裂

分裂父节点

根节点未超出 4,结束;假如此时根节点也超出上界了,须要把根节点也分裂,生成一个新的根节点,且新的根节点的关键字为左右子树的最大关键字

删除

B+ 树的删除与 B 树也相似,找到要删除的关键字,若是是当前子树的最大关键字,删除该关键字后还要修改祖先节点对应的关键字;若是不是当前子树的最大关键字,直接删除;

在上一张图的基础上删除 8,这是叶子的最大关键字,因此须要修改父节点和祖父节点的边界关键字:

删除 8

  • 若是删除元素的节点未低于下界限制,则结束;不然分两种状况处理:
    • 若是兄弟节点有富余关键字,则从兄弟节点中移动一个关键字到当前节点,修改父节点对应边界关键字便可
    • 若是兄弟节点关键字个数都处于下界值,不能外借元素,则合并当前节点和兄弟节点,修改父节点的孩子指针以及边界关键字,此时父节点关键字个数也少了一个,将当前节点的指针指向父节点继续判断处理

咱们继续删除 7,此时该叶子节点关键字个数少于 1 须要调整,而兄弟节点有富余关键字,能够移动 5 到当前节点,修改父节点和祖父节点的边界关键字

删除 7

继续删除 5,兄弟节点的关键字个数为下界值 1,不能外借,则合并当前节点和兄弟节点,并修改父节点指针及关键字,相应的祖父节点也须要修改边界关键字

删除 5

B+ 树动态展现: www.cs.usfca.edu/~galles/vis…

B* 树 (B* - Tree)

B* 树是 B+ 树的变体,在 B+ 树的基础上(全部的叶子结点中包含了所有关键字的信息,及指向含有这些关键字记录的指针),B* 树多了两条性质:

  • 中间结点也增长了指向兄弟的指针,即每一层节点均可以横向遍历
  • B* 树定义了非叶子结点关键字个数至少为 \lceil\frac {2m}3\rceil,即块的最低使用率为 2/3,代替 B+ 树的 1/2

下图的数据与以前 B+ 树的数据同样,但分支结构有所不一样(由于中间节点关键字范围变为[3, 4],不一样于以前 B+ 树的 [2, 4]),并且第二层节点之间也用指针链接起来

B* 树

优点

B+ 树节点满时就会分裂,而 B* 树节点满时会先检查兄弟节点是否满(由于每一个节点都有指向兄弟的指针):

  • 若是兄弟节点未满则向兄弟节点转移关键字,而后修改原节点和兄弟结点的关键字以及会受最大关键字变更影响的祖先的边界关键字
  • 若是兄弟节点已满,则从当前节点和兄弟节点各拿出 1/3 的数据建立一个新的节点出来,而后在父结点增长新结点的指针

B* 树存有兄弟节点的指针,能够向兄弟节点转移关键字的特性使得 B* 树分解次数变得更少,节点空间使用率更高

由于没有找到相关的内容,关于 B* 树的插入删除这里再也不讲解

总结

本文依次介绍了二叉树 -> 二叉搜索树 -> 平衡二叉搜索树(红黑树) -> 平衡多路查找树(B 类树),各有特色,其中 B 类树是介绍的重点,由于实际运用中索引结构使用的是 B 类树

由于树的上面几层会反复查询,因此咱们能够把树的前几层存在内存中,而底层的数据存在外部磁盘里,这样效率更高

固然 B 树也存在弊端:

由于一旦肯定最大阶数,后面的使用过程当中就不能够修改关键字个数的范围

那么除非彻底重建数据库,不然没法改变键值的最大长度。这使得许多数据库系统将人名截断到 70 字符以内

后面一篇咱们会讲解另外一种 Mysql 的索引结构: 哈希索引,能够动态适应任意长度的键值

Reference

相关文章
相关标签/搜索