原文连接: blog.wangriyu.wang/2018/06-Tre…html
与数据库相关的树结构主要为 B 类树,B 类树一般用于数据库和操做系统的文件系统mysql
在学习 B 类树以前先复习一下二叉查找树的概念和红黑树golang
二叉树 - Binary Tree 是每一个节点最多只有两个分支(即不存在分支度大于 2 的节点)的树结构。面试
二叉查找树 - Binary Search Tree: 也称二叉搜索树、有序二叉树。对于根树和全部子树都知足,每一个节点都大于左子树元素,而小于右子树元素,且没有键值相等的结点算法
搜索、插入、删除的复杂度等于树高,指望 ,最坏 O(n)(数列有序,树退化成线性表)sql
二叉查找树动态展现: visualgo.net/zh/bst数据库
当数据基本有序时,二叉查找树会退化成线性表,查找效率严重降低编程
因此后面出现了不少改进的平衡树结构以知足树高最坏也为 , 如伸展树 (Splay Tree)、平衡二叉树 (SBT)、AVL 树、红黑树等数据结构
红黑树 - Red–black tree 是一种自平衡二叉查找树,除了符合二叉查找树的性质外,它还知足如下五条性质:性能
上述约束确保了红黑树的关键特性: 从根到叶子的最长路径不会超过最短路径的两倍
证实: 主要看性质 4 和 性质 5,假设从根到叶子的最短路径 a 上有黑色节点 n 个,最长路径 b 确定是交替的红色和黑色节点,而根据性质 5 可知从根到叶子的全部路径都有相同数目的黑色节点, 这就代表 b 的黑色节点也为 n 个,但 b 出现的红色节点不可能超过黑色节点个数,不然会破坏性质 4 (抽屉原理),因此从根到叶子的最长路径不会超过最短路径的两倍
由于每个红黑树也是一个特化的二叉查找树,所以红黑树上的只读操做与普通二叉查找树上的只读操做相同。然而,在红黑树上进行插入操做和删除操做会致使再也不匹配红黑树的性质。 恢复红黑树的性质须要少许 的颜色变动(实际是很是快速的)和不超过三次树旋转(对于插入操做是两次)。虽然插入和删除很复杂,但操做时间仍能够保持为
次。
红黑树发生变动时须要 [变色] 和 [旋转] 来调整,其中旋转又分 [左旋] 和 [右旋]。
逆时针
旋转红黑树的两个节点 X-Y,使得父节点被本身的右孩子取代,而本身降低为左孩子顺时针
旋转红黑树的两个节点 X-Y,使得父节点被本身的左孩子取代,而本身降低为右孩子旋转过程当中只须要作三次指针变动就行
插入节点的位置跟二叉查找树的寻找方法基本一致,若是插入结点 z 小于当前遍历到的结点,则到当前结点的左子树中继续查找,若是 z 大于当前结点,则到当前结点的右子树中继续查找, 若是 z 依然比此刻遍历到的新的当前结点小,则 z 做为当前结点的左孩子,不然做为当前结点的右孩子。而红黑树插入节点后,为了保持约束还须要进行调整修复(变色加旋转)。
因此插入步骤以下: 红黑树按二叉查找树的规则找到位置后插入新节点 z,z 的左孩子、右孩子都是叶子结点 nil, z 结点初始都为红色,再根据下述情形进行变色旋转等操做,最后达到平衡。
好比上图插入 12 时知足情形 2:
如下情形须要做出额外调整:
下面着重讲讲后三种状况如何调整
当前结点的父结点是红色且祖父结点的另外一个子结点(叔叔结点)是红色
由于当前节点的父节点是红色,因此父节点不多是根节点,当前节点确定有祖父节点,也就有叔叔节点
解决步骤: 将当前结点的父结点和叔叔结点涂黑,祖父结点涂红,再把祖父结点当作新节点(即当前节点的指针指向祖父节点)从新检查各类情形进行调整
因为对称性,无论父结点是祖父结点的左子仍是右子,当前结点是其父结点的左子仍是右子,处理都是同样的
咱们插入 21 这个元素,当前节点指向 21:
此时会发现 2一、22 两个红色相连与性质 4 冲突,但 21 节点知足情形 3,修复后:
此时当前节点指向 21 的祖父节点,即 25。而 25 节点一样遇到情形 3 的问题,继续修复:
此时当前节点指向根节点,知足情形 1,将 14 节点涂黑便可恢复红黑树平衡
当前结点的父结点是红色,叔叔结点是黑色或者 nil,当前结点相对其父结点的位置和父节点相对祖父节点的位置不在同侧
解决步骤:
在上图的基础上咱们继续插入 5 这个元素:
能够看出 5 是父节点的左子,而父节点是祖父节点的右子,不一样侧则为情形 4,将当前节点指向 5 的父节点 6,并以 6 为支点进行右旋:
此时当前节点是 6,而 6 是父节点 5 的右子,父节点 5 也是祖父节点 1 的右子,同侧则转为情形 5,继续往下看
当前结点的父结点是红色,叔叔结点是黑色或者 nil,当前结点相对其父结点的位置和父节点相对祖父节点的位置在同侧
解决步骤:
在上一张图的基础上修改节点 5 为黑色,节点 1 为红色,再以 1 为支点左旋:
此时便恢复平衡
删除节点 X 时第一步先判断两个孩子是否都是非空的,若是都非空,就先按二叉查找树的规则处理:
在删除带有两个非空子树的节点 X 的时候,咱们能够找到左子树中的最大元素(或者右子树中的最小元素),并把这个最值复制给 X 节点,只代替原来要删除的值,不改变节点颜色。
而后咱们只要删除那个被复制出值的那个节点就行,由于是最值节点因此它的孩子不可能都非空。
由于只是复制了一个值,不违反任何性质,这就把原问题转化为如何删除最多有一个非空子树的节点的问题。它不关心这个节点是最初要删除的节点仍是被复制出值的那个节点。
咱们以图为例,图中三角形表明可能为空的子树:
节点 X 是要删除的节点,发现它的两个子树非空,咱们能够找左子树中最大的元素 Max (也能够找右子树中最小的元素 Min),把 Max 值(或者 Min 值)复制到 X 上覆盖原来的值,不修改其余属性,而后删除 Max 节点(或 Min 节点)便可,能够很清楚的看到最值节点最多只会有一个非空子树
接下来就是如何处理删除最多有一个非空子树的节点 X 的问题
简单情形:
若是 X 和它的儿子都是黑色,这是一种复杂的状况,咱们单拎出来说
咱们首先把要删除的节点 X 替换为它的儿子。出于方便,称呼这个新上位的儿子为 N,称呼它的兄弟为 S,使用 P 称呼 N 的新父亲,SL 称呼 S 的左儿子,SR 称呼 S 的右儿子
有如下六种情形须要考虑:
N 是新的根
咱们不须要作什么,由于全部路径都去除了一个黑色节点,而新根也是黑色的,因此性质都保持着
情形 二、五、6 涉及到左右不一样的状况,只取一种处理
S 是红色
完成这两个操做后,尽管全部路径上黑色节点的数目没有改变,但如今 N 有了一个黑色的兄弟和一个红色的父亲,因此咱们能够接下去按情形 四、情形 5 或情形 6 来处理
N 的父亲、S 和 S 的儿子都是黑色的
在这种情形下,咱们简单的重绘 S 为红色。结果是经过 S 的全部路径都少了一个黑色节点。这与删除 N 的初始父亲 X 形成经过 N 的全部路径少了一个黑色节点达成平衡。可是,经过 P 的全部路径如今比不经过 P 的路径少了一个黑色节点,因此仍然违反性质 5。要修正这个问题,咱们要从情形 1 开始,在 P 上作从新平衡处理
S 和 S 的儿子都是黑色,可是 N 的父亲是红色
在这种情形下,咱们简单的交换 N 的兄弟和父亲的颜色。这不影响不经过 N 的路径的黑色节点的数目,可是它在经过 N 的路径上对黑色节点数目增长了一,添补了在这些路径上删除的黑色节点
S 是黑色,S 的其中一个儿子是红色,且红色儿子的位置与 N 相对于父亲的位置处于同侧
全部路径仍有一样数目的黑色节点,可是如今 N 有了一个黑色兄弟,且兄弟的一个儿子仍为红色的,其位置与 N 相对于父亲的位置处于不一样侧,进入情形 6
情形 五、6 中父节点 P 的颜色能够为黑色也能够是红色
S 是黑色,S 的其中一个儿子是红色,且其位置与 N 相对于父亲的位置处于不一样侧
交换前 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:
此时 N 的兄弟 S 变为 22,P 变为 17,S 的左孩子是红色的 21,属于情形 5。S 作右旋转,并交换 22 和 21 的颜色:
此时 N 的兄弟 S 变为黑色的 21,但 21 的红色孩子节点 22 变为右侧,进入情形 6
P 节点 17 作左旋转,并将 S 的右节点涂黑,此时树恢复平衡
至此,咱们已经把节点都删了个遍,相信你对红黑树的删除操做应该了解了
红黑树动态展现: www.cs.usfca.edu/~galles/vis…
红黑树仍是典型的二叉搜索树结构,主要应用在一些 map 和 set 类型的实现上,好比 Java 中的 TreeMap 和 C++ 的 set/map/multimap 等。其查找的时间复杂度 与树的深度相关,下降树的深度能够提升查找效率。
Java 的 hashmap 和 golang 的 map 是用哈希实现的
可是大规模数据存储中,实现索引查询这样一个实际背景下,树节点存储的元素数量是有限的(若是元素数量很是多的话,查找就退化成节点内部的线性查找了), 这样致使二叉查找树结构因为树的深度过大而形成磁盘 I/O 读写过于频繁,进而致使查询效率低下,所以咱们该想办法下降树的深度,从而减小磁盘查找存取的次数。
一个基本的思想就是:采用多叉树结构
,因此出现了下述的平衡多路查找树
B-树,即为 B 树,不要读做 B 减树
B 树与红黑树最大的不一样在于,B 树的结点能够有许多子女,从几个到几千个。
B 树的定义有两种,一种以阶数为限制的 B 树(下文所述的),一种以度数为限制的 B 树(算法导论所描述的),二者原理相似,这里以阶数来定义
B 树属于平衡多路查找树。一棵 m 阶(m 阶即表明树中任一结点最多含有 m 个孩子)的 B 树的特性以下:
如图是一个典型的 2-3-4 树结构,也是阶为 4 的 B 树。从图中查询元素最多只须要 3 次磁盘 I/O 就能够访问到咱们须要的数据节点,将节点数据块读入内存后再查找指定元素会很快。若是一样的数据用红黑树表示,树高会增加不少,形成遍历节点的次数增多,访问磁盘的次数增多,查找性能会降低。
对于一棵包含 n 个元素、高度为 h 、阶数为 m 的 B 树: 影响 B 树高度的是每一个结点所包含的子树数,若是尽量使结点孩子数都等于 ,则层数最多,为最坏状况;若是尽量使结点孩子数都等于 m,则层数最少,为最好状况。因此有
底数 能够取很大,好比 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 树来查找数据时,每遍历到一个节点,便将其读入内存,比较其中的关键字,若能匹配到咱们要找的元素,便返回;若未能找到,经过比较肯定在哪两个关键字的值域区间, 便可肯定子树的节点指针,继续往下找,把下一个节点的数据读入内存,重复以上步骤
对于一棵 m 阶的 B 树来讲,插入一个元素(或者叫关键字)时,首先判断在 B 树中是否已存在,若是存在则不插入;若是不存在,则在对应叶子结点中插入新的元素,须要判断是否会超出关键字个数限制(m-1)
插入步骤:
仍是以上面的 2-3-4 树(阶数 m = 4)为例,咱们依次插入元素
以后的步骤相似,再也不一一叙述
删除操做是指删除 B 树中的某个节点中的指定关键字
删除步骤:
以上面的 2-3-4 树为例
阶数为 4,节点关键字个数范围应该是 [1, 3],即 math.Ceil(m/2)-1 = 1
这里第一步也能够选择下移 3,而后第二步跟左兄弟合并成 一、3 节点
删除操做就演示到这,B 树的内容讲完
B 树动态展现: www.cs.usfca.edu/~galles/vis…
B+ 树 是基于 B 树的变体,查找性能更好
同为 m 阶的 B+ 树与 B 树的不一样点:
如图所示的是将以前的 2-3-4 树的数据存到 B+ 树结构中的示意图,叶子节点保存了全部关键字信息而且叶子节点之间也用指针链接起来(一个顺序链表),而全部非叶子节点只包含子树根节点中对应的最大关键字,其做用只是用于索引
B+ 树还能够用另外一种形式定义:
中间节点最多有 m-1 个关键字,最少有
个关键字,与 B 树相同; 可是非叶子节点的关键字是左子树的最大关键字(或者右子树的最小关键字),与刚才的情形不一样
好比一样的数据此定义的 B+ 树表现形式以下:
这种形式中间节点占用更少,可能更常见一点,不过下面的讲解是按第一种定义来
B+ 树比 B 树更适合实际应用中操做系统的文件索引和数据库索引
数据库中关键字可能只是某个数据列的索引信息(好比以 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+ 树多一次盘块查找时间
因为非叶子节点并非最终指向文件内容的结点,而只是叶子结点中关键字的索引。因此任何关键字的查找必须走一条从根结点到叶子结点的路径。全部关键字查询的路径长度相同,致使每个数据的查询效率至关。而 B 树查找一个文件时查找到的路径长度是不一的。
若是是查找单一元素,B+ 树的查找过程与 B 树相似,只是每次查找都是从根查到叶
而进行范围查询的操做时,B+ 树只要遍历叶子节点就能够实现整棵树的遍历,而 B 树的范围查询要经过中序遍历,效率比较低下
B+ 树的插入与 B 树相似,先寻找关键字对应的位置插入,须要注意的是插入比当前子树的最大关键字还大的数时要修改祖先节点对应的关键字,由于 B+ 树内部结点存的是子树的最大关键字
好比在上面给出的 B+ 树中插入 105 这个元素,由于 105 大于当前子树最大关键字 101,因此须要修改父节点和祖父节点的边界关键字:
好比刚才插入 105 的叶子节点关键字个数达到 4 个,须要分裂,这里分裂与 B 树略有不一样。B 树是把节点按中间节点分红三份,再把中间节点上移;而 B+ 树是分红两份,再把左半节点的最大关键字添加进父节点
此时父节点也须要分裂
根节点未超出 4,结束;假如此时根节点也超出上界了,须要把根节点也分裂,生成一个新的根节点,且新的根节点的关键字为左右子树的最大关键字
B+ 树的删除与 B 树也相似,找到要删除的关键字,若是是当前子树的最大关键字,删除该关键字后还要修改祖先节点对应的关键字;若是不是当前子树的最大关键字,直接删除;
在上一张图的基础上删除 8,这是叶子的最大关键字,因此须要修改父节点和祖父节点的边界关键字:
咱们继续删除 7,此时该叶子节点关键字个数少于 1 须要调整,而兄弟节点有富余关键字,能够移动 5 到当前节点,修改父节点和祖父节点的边界关键字
继续删除 5,兄弟节点的关键字个数为下界值 1,不能外借,则合并当前节点和兄弟节点,并修改父节点指针及关键字,相应的祖父节点也须要修改边界关键字
B+ 树动态展现: www.cs.usfca.edu/~galles/vis…
B* 树是 B+ 树的变体,在 B+ 树的基础上(全部的叶子结点中包含了所有关键字的信息,及指向含有这些关键字记录的指针),B* 树多了两条性质:
下图的数据与以前 B+ 树的数据同样,但分支结构有所不一样(由于中间节点关键字范围变为[3, 4],不一样于以前 B+ 树的 [2, 4]),并且第二层节点之间也用指针链接起来
B+ 树节点满时就会分裂,而 B* 树节点满时会先检查兄弟节点是否满(由于每一个节点都有指向兄弟的指针):
B* 树存有兄弟节点的指针,能够向兄弟节点转移关键字的特性使得 B* 树分解次数变得更少,节点空间使用率更高
由于没有找到相关的内容,关于 B* 树的插入删除这里再也不讲解
本文依次介绍了二叉树 -> 二叉搜索树 -> 平衡二叉搜索树(红黑树) -> 平衡多路查找树(B 类树),各有特色,其中 B 类树是介绍的重点,由于实际运用中索引结构使用的是 B 类树
由于树的上面几层会反复查询,因此咱们能够把树的前几层存在内存中,而底层的数据存在外部磁盘里,这样效率更高
固然 B 树也存在弊端:
由于一旦肯定最大阶数,后面的使用过程当中就不能够修改关键字个数的范围
那么除非彻底重建数据库,不然没法改变键值的最大长度。这使得许多数据库系统将人名截断到 70 字符以内
后面一篇咱们会讲解另外一种 Mysql 的索引结构: 哈希索引,能够动态适应任意长度的键值