一、简介
记得当初在大学的时候,学过数据结构这门课程,由于当时不好好学习,只是为了应付考试,没有进行深入的理解,等到参加工作之后,才发现原来好多的底层都用到了数据结构,出来混迟早是要还的,今天简单的来聊一聊数据库中的树。
二、树的分类
2.1 简单分类
我想系统的整理一下树,先百度一下树的分类,看看在计算机科学当中都有哪些树,如下图:
我们发现树简直是太多了,不可能一个个的去了解它们,所以我今天只介绍下下面这几棵常见的树,如图所示:
2.2 基础知识
树:包含 n(n>0) 个结点的有穷集,每个元素成为结点 (node),有一个特定的结点被称为根结点或树根 (root)
结点的度:一个结点包含的子树的个数称为该结点的度
叶子结点和终端结点:度为 0 的结点。非终端结点或分支结点:度不为 0 的结点
双亲结点或父结点:若一个结点含有结结点,则这个结点称为其子结点的父结点
孩子结点或子结点:一个结点含有的子树的根结点称为该结点的子结点
兄弟结点:具有相同父结点的结点互称为兄弟结点
树的度:一颗树中,最大的结点的度称为树的度
结点的层次:从树开始定义起,根为第一层,根的子结点称为第二层,以此类推
树的高度或深度:树结点中最大的层次
堂兄弟结点:双亲在同一层次的结点互称为堂兄弟
结点的祖先:从根到该结点所经分支上的所有结点
子孙:以某结点为根的子树中任一结点都称为该结点的子孙
森林:由m棵互不相交的树的集合称为森林
如图所示:这就是一棵普通的树,不属于二叉树,因为它有 3 个叉
三、二叉树
3.1 定义
二叉树的每个结点最多只有两棵子树(不存在度大于 2 的结点),二叉树的子树有左右之分,次序不能颠倒,二叉树的第 i 层最多有 2^(i-1) 个结点,深度为 k 的二叉树至多有 (2^k-1) 个结点。
二叉树分为:完美二叉树、完全二叉树、二叉查找树。
3.2 完美二叉树
又称为满二叉树,即一颗深度为 k 且有 2^(k-1) 个结点的二叉树称为满二叉树,如下图所示:
3.3 完全二叉树
在一棵二叉树中,除了最后一层,都是满的,并且最后一层或者是满的,或者是右边缺少连续若干点,称为完全二叉树,如下图所示,左边是完全二叉树,右边则不属于完全二叉树。
3.4 二叉查找树
又称为二叉搜索树、二叉排序树。它或者是一颗空树,或者具有下列性质的二叉树:若它的左子树不空,则左子树上所有的结点的值均小于它的根结点的值;若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;它的左、右子树也分别为二叉排序树。(不存在相等值的结点),如下图所示:
二叉搜索树必须满足以下几个条件:
1、所有子树上面的结点的值都比根结点要小,右结点的值都比根结点的值大。
2、任意结点的左右子树也都是二叉查找树。
3、通过中序遍历,将得到一个有序的数列。
4、对其操作的最优的时间复杂度为 O(log2N) 相当于对数列进行二分查找法。最坏的时间复杂度为 O(N),相当于线性查找。N 为元素的个数。
四、自平衡二叉树
4.1 出现原因
自平衡二叉树出现的原因是为了解决二叉查找树退化成链表的问题(退化成链表,时间复杂度变成了 O(N) )。
4.2 平衡因子
结点的平衡因子是结点的左子树减去右子树的高度。 平衡二叉树为每个结点的平衡因子都为 1、-1、0 的二叉排序树 平衡二叉树的目的是为了减少二叉查找树层次,提高查找速度。
4.3 定义
自平衡二叉树是一种结构平衡的二叉搜索树,即叶结点高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树。
自平衡二叉树分为:平衡二叉树和红黑树
4.4 平衡二叉树
平衡二叉树又称为 AVL 树,它是二叉查找树最优的情况,把插入、查找、删除的时间复杂度最好情况和最坏情况都维持在 O(logN),但是频繁旋转(当发生插入、删除、修改时会发生旋转)会使插入和删除牺牲掉 O(logN) 左右的时间,不过相对二叉查找树来说,时间上稳定了很多。
二叉搜索树必须满足以下几个条件:
1、它是一个二叉查找树。
2、它的左右两棵子树的高度差绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树。
3、当删除、新 增、修改结点上的值时,它会通过左旋或右旋使二叉树保持平衡。
4、最坏的时间复杂度为 O(log2N)。
4.5 红黑树
4.5.1 出现原因
是为了解决平衡二叉树在删除等操作时需要频繁调整的情况。
4.5.2 定义
红黑树是一种弱平衡二叉树(叶结点高度差的绝对值可能会超过 1,由于是弱平衡,可以看到,在相同的结点情况下,AVL 树的高度低于红黑树,即红黑树会很高。但是再高也不会出现 2 倍的长度)。
可能关于”不会出现 2 倍的长度“,这个地方会有些问题,我也是想了好久才想明白,意思就是说 AVL 规定左右子树高度差不能超过 1,而红黑树左右子树高度可以超过 1,那最多可以超过多少呢?答案是最长的子树不能超过最短的子树的 2 倍,假设一棵子树长度是 8,那么另外一棵最多不能超过16,可以是15;最少不能低于 5,如果是 4,那么 8 就是 4 的二倍了,也不满足红黑树的要求了。
它也是一种二叉查找树,但在每个结点增加一个存储位表示结点的颜色,可以是红或黑(非红即黑)。通过对任何一条从根到叶子的路径上各个结点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍(这里是和平衡二叉树的主要区别)。
相对于要求严格的 AVL 树来说,红黑树的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,我们就用红黑树。如果应用场景中对插入删除不频繁,只是对查找要求较高,那么 AVL 还是较优于红黑树。
4.5.3 特性
1、对于黑平衡是指,从根结点开始搜索,一直搜索到叶子结点,所经历的黑色结点的个数是一样的。
2、黑平衡二叉树,严格意义上,不是平衡二叉树。 左右子树的高度差可能大于 1。
3、时间复杂度是 O(logn),最大高度:2logn
4、红黑树不会像二分搜索树一样退化为链表。 查找的时间上面会比 AVL 树慢一点,因为最大高度为 2logn 添加操作和删除操作比 AVL 树要快一些
5、根结点是黑色的
6、每个叶子结点都是黑色的(叶子是 NIL 结点)
7、每个红色的结点必须有两个黑色结点
8、NIL 结点就是空结点,二叉树中用 NIL 结点代替 NULL
9、主要用来存储有序的数据,时间复杂度为 O(logN)
10、java 中的 TreeSet、TreeMap 就是他的实现方式。
五、多叉树
5.1 背景
前面已经简单的介绍了二叉树和自平衡二叉树,这两种树查找效率已经足够高了,那为什么还有多叉树的出现呢?难道它两的时间复杂度比二叉查找树还小吗?
答案当然不是,多叉树的出现是因为另外一个问题,那就是磁盘 IO。众所周知,IO 操作的效率很低,如果应用场景内数据的存储量非常大,而我们在查询时不能一下子将所有数据加载到内存中,只能逐一加载磁盘页,每个磁盘页对应树的结点。造成大量磁盘 IO 操作(最坏情况下为树的高度)。(进行一次 IO,需要加载一个磁盘页、每个磁盘也存储一个结点的数据,当树很高时,结点就很多,此时需要进行很多次 IO)。
5.2 解决磁盘 IO
平衡二叉树由于树深度过大而造成磁盘 IO 读写过于频繁,进而导致效率低下。所以,我们为了减少磁盘 IO 的次数,就你必须降低树的深度,将 “瘦高” 的树变得 “矮胖” 。即:每个结点存储多个元素、摒弃二叉树结构,采用多叉树。
这样就引出来了一个新的查找树结构 ——多路查找树。 根据 AVL 给我们的启发,一颗平衡多路查找树(B 树)自然可以使得数据的查找效率保证在 O(logN) 这样的对数级别上。
5.3 概念纠正
之前有看到有很多文章把 B 树和 B-tree 理解成了两种不同类别的树,其实这两个是同一种树。
5.4 定义
B 树和平衡二叉树稍有不同的是 B 树属于多叉树,又名平衡多路查找树(查找路径不止两个),数据库索引技术里大量使用 B 树和 B+ 树的数据结构。
虽然要降低树的高度,但是不能设计成无限多路,那就会变成有序数组,有悖于设计的初衷了。
5.5 B树
5.5.1 特征
一个 m 阶的 B 树具有如下几个特征:B 树中所有结点的孩子结点最大值称为 B 树的阶,通常用 m 表示。一个结点有 k 个孩子时,必有 k-1 个关键字才能将子树中所有关键字划分为 k 个子集。
5.5.2 示例
下面就是一棵 3 阶的 B 树:
以此图为例:若查询的数值为 9:
第一次磁盘 IO:在内存中定位(与 17、35 比较),比 17 小,左子树;
第二次磁盘 IO:在内存中定位(与8、12 比较),比 8 大,比 12 小,中间子树;
第三次磁盘 IO:在内存中定位(与 9、10 比较),找到 9,终止。
整个过程中,我们可以看出:比较的次数并不比二叉查找树少,尤其适当某一结点中的数据很多时,但是磁盘 IO 的次数却是大大减少。比较是在内存中进行的,相比于磁盘 IO 的速度,比较的耗时几乎可以忽略。所以当树的高度足够低的话,就可以极大的提高效率。相比之下,结点中的元素多点也没关系,仅仅是多了几次内存交互而已,只要不超过磁盘页的大小即可。
5.5.3 注意
1、B 树主要用于文件系统以及部分数据库索引,例如: MongoDB。而大部分关系数据库则使用 B+ 树做索引,例如:mysql 数据库;
2、从查找效率考虑一般要求 B 树的阶数 m >= 3;
3、B 树上算法的执行时间主要由读、写磁盘的次数来决定,故一次 IO 操作应读写尽可能多的信息。因此 B 树的结点规模一般以一个磁盘页为单位。一个结点包含的关键字及其孩子个数取决于磁盘页的大小。
5.6 B+树
5.6.1 定义
B+ 树是 B 树的一个升级版,相对于 B 树来说 B+ 树更充分的利用了结点的空间,让查询速度更加稳定,其速度完全接近于二分法查找。
B+ 树所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
5.6.2 示例
下面是一棵 3 阶的 B+ 树:
1、B+ 跟 B 树不同 B+ 树的非叶子结点不保存关键字记录的指针,只进行数据索引,这样使得 B+ 树每个非叶子结点所能保存的关键字大大增加;
2、B+ 树叶子结点保存了父结点的所有关键字记录的指针,所有数据地址必须要到叶子结点才能获取到。所以每次数据查询的次数都一样;
3、B+ 树叶子结点的关键字从小到大有序排列,左边结尾数据都会保存右边结点开始数据的指针。
4、B+ 树通常有两个指针,一个指向根结点,另一个指向关键字最小的叶子结点。因些,对于 B+ 树进行查找两种运算:一种是从最小关键字起顺序查找,另一种是从根结点开始,进行随机查找。
5.6.3 优势
B+ 树的优势在于查找效率上。B+ 树的查找和B树一样,类似于二叉查找树。起始于根结点,自顶向下遍历树,选择其分离值在要查找值的任意一边的子指针。在结点内部典型的使用是二分查找来确定这个位置。
不同的是,B+ 树中间结点没有卫星数据(索引元素所指向的数据记录),只有索引,而B树每个结点中的每个关键字都有卫星数据;这就意味着同样的大小的磁盘页 B+ 树可以容纳更多结点元素,在相同的数据量下,B+ 树更加 “矮胖”,IO 操作更少 。
5.6.4 分析
B 树所有的结点都包含卫星数据:
B+ 树只有叶子结点包含卫星数据:
在数据库的聚集索引(Clustered Index)中,叶子结点直接包含卫星数据。在非聚集索引(NonClustered Index)中,叶子结点带有指向卫星数据的指针。
因为卫星数据的不同,导致查询过程也不同;B 树的查找只需找到匹配元素即可,最好情况下查找到根结点,最坏情况下查找到叶子结点,所说性能很不稳定,而 B+ 树每次必须查找到叶子结点,性能稳定。
在范围查询方面,B+ 树的优势更加明显 B树的范围查找需要不断依赖中序遍历。首先二分查找到范围下限,在不断通过中序遍历,知道查找到范围的上限即可。整个过程比较耗时。(sql 查询都是范围查找) 而 B+ 树的范围查找则简单了许多。首先通过二分查找,找到范围下限,然后同过叶子结点的链表顺序遍历,直至找到上限即可,整个过程简单许多,效率也比较高。
5.7 对比
1、B+ 树单一结点存储更多的元素,使得查询的 IO 次数更少;
2、B+ 树所有查询都要查找到叶子结点,查询性能稳定;
3、B+ 树所有叶子结点形成有序链表,便于范围查询。
5.8 索引为什么用 B+ 树
如果用 hash 来存储索引,时间复杂度为 O(1),那为啥 mysql 要用 B+ 树来存储索引呢? 这个和业务场景有关,如果只选择一个数据,那确实是 hash 快。
但是数据库经常选择多条,这时候由于 B+ 树索引有序,并且又有链表相连,它的查询效率比 hash 就快很多了。而且数据库中的索引一般是在磁盘上,数据量大的情况可能无法一次装入内存,B+ 树的设计可以允许数据分批加载,同时树的高度较低,提高查找效率。
六、遍历二叉树
6.1 先序遍历
6.1.1 访问方式
1、访问根结点
2、访问当前结点的左子树
3、若当前结点无左子树,则访问当前结点的右子树
4、采用先序遍历得到的序列为 1、2、4、5、3、6、7
6.2 中序遍历
6.2.1 访问方式
1、访问当前结点的左子树
2、访问根结点
3、访问当前结点的右子树
4、采用中序遍历得到的序列为 4、2、5、1、6、3、7
3.3 后序遍历
6.3.1 访问方式
1、从根结点出发,依次遍历各结点的左右子树,直到当前结点左右子树遍历完成后,才访问该结点元素。
2、采用后序遍历得到的序列为 4、5、2、6、7、3、1