目录
一、二叉树
1、二叉树的定义
二叉树(binary tree)是 n ( n >= 0 ) 个节点的有限集合,该集合或者是空集(简称为空二叉树),或者是由一个根节点和两棵互不相交的、分别称为根节点的左子树和右子树的二叉树组成。如下图所示:
2、二叉树的特点
(1)每一个节点最多有两棵子树,所以二叉树中不存在度大于2的节点。
(2)二叉树是有序的,其次序不能任意颠倒,即使树中的某个节点只有一颗子树,也要区分它是左子树还是右子树,如下图所示:
3、二叉树的5种基本形态和3种特殊的二叉树
(1)空二叉树。(2)只有一个根节点。(3)根节点只有左子树。(4)根节点只有右子树。(5)根节点既有左子树也有右子树。
但是,在实际应用中,我们经常会遇到如下三种特殊的二叉树:斜树、满二叉树、完全二叉树。
3.1 斜树
所有节点都只有左子树的二叉树称之为左斜树。所有节点都只有右子树的二叉树称之为右斜树。左斜树和右斜树,统称为斜树。如下如图所示。
在斜树中,每一层都只有一个节点,所以,斜树的节点个数与其数的深度相同。
3.2 满二叉树
在一棵二叉树中,如果所有的分支节点都存在左子树和右子树,并且所有叶子节点都在同一层上,这样的二叉树称之为满二叉树。
图5-16(a)是一颗满二叉树,(b)不是满二叉树,因为虽然所有的分支节点都存在左右子树,但是叶子节点未在同一层上。
满二叉树的特点是:
(1)叶子节点只能出现在最下一层。
(2)只有度为0和度为2的节点。
3.3 完全二叉树
对一颗具有 n 个节点的二叉树按照层序编号,如果编号为 i ( 1=< i =< n )的节点与同样深度的满二叉树中编号为 i 的节点在二叉树中的位置完全相同,则这颗二叉树称之为 完全二叉树。显然,一颗满二叉树必定是一颗完全二叉树。
完全二叉树的特点是:
(1)叶子节点只能出现在最下两层,而且最下层的叶子节点都集中在二叉树的左部分。
(2)完全二叉树中如果有度为1的节点,只可能有一个,而且该节点只有左孩子节点。
如下图所示:
4、二叉树的基本性质
(1)二叉树的第 i 层上最多有 个节点( i >= 1 )。
(2)在一棵深度为 k 的二叉树中,最多有 个节点,最少有 k 个节点。
(3)在一棵二叉树中,如果叶子节点的个数是 n0,度为2的节点个数是 n2,那么则有:n0 = n2 + 1 。
(4)具有 n 个节点的完全二叉树的深度是 。、
(5)对一颗具有 n 个节点的完全二叉树中的节点从1开始按照层序编号,则对于任意的编号为 i ( 1<= i =< n )的节点(简称节点 i ),有:
A:如果 i >1,则节点 i 的双亲的编号是 int_down( i/2 ),否则节点 i 是根节点,无双亲。
B:如果 2i =< n,则节点i 的左孩子编号是 2i,否则节点i 无左孩子。
C:如果 2i +1 =< n,则节点i 的右孩子编号是 2i +1,否则节点i 无右孩子。
5、二叉树的遍历操作
5.1 前序遍历 5.2 中序遍历 5.3 后续遍历 5.4 层序遍历
【后续可以抽时间补上!】
对于一个大型的查找集合,要进行动态查找,应该如何存储这个查找集合呢?
假设使用顺序表来存储,如果记录的存储没有任何顺序,那么插入操作很简单,只需要将其放在顺序表的末端。但是,在一个无序的顺序表中进行顺序查找的平均查找时间是 O(N)。对于一个大型的查找集合,这太慢了。提高查找效率的方法是把记录按照某个关键码进行排序。如果使用单链表来实现,排序并不会提高查找效率;如果使用顺序表来实现,那么使用折半查找法只需要时间 O(logN),但是插入时间则需要时间 O(N),因为在有序表中新纪录的存储位置后,需要移动许多记录以便为新纪录腾出地方。那么,有没有一种存储结构的方法使得记录的插入与查找都能够很快的完成呢?二叉查找树就能够很好的解决这个问题。
二、二叉排序树
2.1 二叉查找树的定义
二叉排序树,又称之为二叉查找树,它是一颗空的二叉树,或者是具有如下性质的二叉树:
(1)若它的左子树不为空,则左子树上所有节点的值均小于根节点的值。
(2)若它的右子树不为空,则右子树上所有节点的值均大于根节点的值。
(3)它的左右子树也都是一颗二叉排序树。
从上述定义可以看出,二叉排序树是记录之间满足一定次序关系的二叉树,中序遍历二叉排序树可以得到一个按照关键码有序的序列,这也是二叉排序树的名称由来。
二叉排序树通常采用二叉链表进行存储,它的节点结构可以复用二叉链表的节点结构。
2.2 二叉排序树的查找及性能分析
由二叉排序树的定义可以知道,在二叉排序树中查找指定值K的过程是:
(1)若 root 是空树,则查找失败;
(2)若 k = root —> data,则查找成功;否则:
(3)若 k < root —> data,则在 root 的左子树上继续查找;否则:
(4)在 root 的右子树是哪个继续查找。
上述过程一直持续到 k 被找到为止,或者待查找的子树为空,如果待查找的子树为空,则说明查找失败了。二叉排序树的查找效率就在于只需要查找的两个子树之一。
基于二叉查找树的这种特点,在查找某个节点的时候,可以采取类似于二分查找的思想,快速找到某个节点。n 个节点的二叉查找树,正常情况下,查找的 时间复杂度 为 O(logN)。之所以说是正常情况下,是因为二叉查找树有可能出现一种极端的情况,比如左斜树或者右斜树等。举例如下:
这种情况的树,虽然也满足二叉树的条件,但是它已经近似的退化为一条链表,这样的二叉树,其查找的时间复杂度顿时变成了 O(N),所以必须防止这种情况发生,于是引申出了平衡二叉树。
三、平衡二叉树
1、概念
从上节的讨论可以知道,二叉排序树的查找效率取决于二叉排序树的形态,而构造一颗形态均匀的二叉排序树与节点插入的次序有关,但是节点的插入次序往往不是随人的意志而定的,这就要求我们找到一种动态平衡的方法,对于任意给定的关键码序列都能够构造出一颗形态均匀的平衡的二叉排序树,这就是平衡二叉树。
平衡二叉树,或者是一颗空的二叉排序树,或者的具有下列性质二叉排序树:
(1)根节点的左子树和右子树的深度最多相差1(也即 每个节点的平衡因子的绝对值小于等于1);
(2)根节点的左子树和右子树也都是平衡二叉树。
【注意】平衡二叉树,是基于二分法的策略来提高数据查找速度的一种二叉树的数据结构,查找的 时间复杂度 为 O(logN)。
1.1 平衡因子
节点的平衡因子,是该节点的左子树的深度与右子树的深度之差。如下图所示,给出了两棵平衡二叉树,每个节点的所示数字是该节点的平衡因子。
1.2 最小不平衡子树
最小不平衡子树,是指在平衡二叉树的构造过程中,以距离插入点最近的、且平衡因子的绝对值大于1的节点为根的子树。
平衡二叉树的基本思想是:在构造二叉排序树的过程中,每当插入一个节点时,首先检查是否因插入而破坏了树的平衡性,若是,则找出最小不平衡子树,在保持二叉排序树特性的前提下,调整最小不平衡子树中各节点之间的链接关系,进行相应的旋转,使之成为新的平衡子树。
1.3 构造平衡二叉树的一个案例
也可以看另一个讲解:https://zhuanlan.zhihu.com/p/56066942
四、红黑树
4.1 为什么有了平衡二叉树还需要红黑树
虽然平衡二叉树解决了二叉排序树退化为近似链表导致查找效率低下的缺点,能够把查找时间控制在 O(logN),但是却不是最佳的。因为平衡二叉树要求每个节点的左子树和右子树的高度差至多等于 1,这个要求太严,导致每次进行【插入/删除】节点的时候,几乎都会破坏平衡二叉树的第一个规则,进而都需要通过左旋或者右旋来进行调整,使之再次成为一颗符合要求的平衡二叉树。所以,从这里引出了红黑树。
4.2 红黑树的特性
显然,如果在【插入/删除】很频繁的场景中,平衡二叉树需要频繁进行节点的旋转调整,这会使平衡二叉树的性能大打折扣,为了解决这个问题,于是有了红黑树,红黑树具有如下特点:
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 【这里的叶子节点,是指为空(NIL或NULL)的叶子节点】。
(4)如果一个节点是红色的,那么它的子节点必须是黑色的。
(5)从一个节点到该节点的叶子节点的所有路径上包含相同数目的黑节点。【这里是指到叶子节点的路径】。
案例:包含 n 个内部节点的红黑树的高度是 O(log(n))。如下图所示:
4.3 红黑树的使用场景
Java 使用到红黑树的有 TreeSet 和 JDK1.8的HashMap。红黑树的【插入/删除】都要满足以上 5 个特性,操作非常复杂。
为什么要使用红黑树呢?原因主要是:红黑树是一种平衡二叉树,复杂的定义和规则都是为了保证树的平衡性。如果树不保证平衡性,有可能会变成链表:
保证平衡性的最大的目的:就是降低树的高度,因为树的查找性能取决于树的高度。树的高度越高,查找性能越低。树的高度越低,查找性能越高。
五、B树:平衡多路查找树
5.1 概念
B树和平衡二叉树稍有不同的是 B树属于多叉树,又名 平衡多路查找树(即 查找路径不只两个),数据库的索引技术就大量使用B树和B+树的数据结构。
5.2 规则
(1)排序方式:所有节点的关键字都是按照递增次序排列,并且遵循左小右大的原则。
(2)子节点数:非叶子节点的子节点数 > 1,且 <= M,空树除外。(注:M 阶代表一个树节点最多有多少个查找路径,M=M路,当 M=2 则是二叉树,M=3 则是三叉树。)
(3)关键字数:枝节点的关键字数量大于等于 ceil(m/2) - 1 个,且小于等于 M-1 个。(注:ceil()是个朝正无穷方向取整的函数。如 ceil(1.1) 结果为2。)
(4)所有叶子节点均在同一层,叶子节点除了包含了关键字和关键字记录的指针外也有指向其子节点的指针,只不过其指针地址都为 null ,对应下图最后一层节点的空格子。
用一个图和一个实际的例子来理解 B 树(便于理解直接用实际字母的大小来排列 C>B>A ):
5.3 B树的查询流程(比如要从上图中找到 E)
(1)获取根节点的关键字进行比较,当前根节点关键字为 M,E<M (26个字母顺序),找到指向左边的子节点(二分法规则,左小右大)。
(2)拿到关键字 D 和 G,D < E < G ,所以直接找到 D 和 G 的中间节点所指向的磁盘块。
(3)拿到 E 和 F,因为 E = E,所以直接返回关键字和指针信息(如果树结构里面没有包含所要查找的节点,则返回null )。
5.4 B树的节点插入流程
定义一个五阶的B树(平衡5路查找树),现在要把【 3、8、31、11、23、29、50、28 】这些数字构建出一个五阶B树出来。遵循规则:
(1)节点拆分规则:当前是要组成一个五路查找树,那么此时 M=5,关键字数必须 <= 5-1(这里关键字数是8, > 4,所以就要进行节点拆分)。
(2)排序规则:满足节点本身比左边节点大,比右边节点小。
先插入 3、8、31、11:
再插入23、29:
再插入50、28:
至此,【 3、8、31、11、23、29、50、28 】这些数字构建出的五阶B树完成。
5.5 B树的节点删除流程
节点删除规则:
(1)节点合并规则:当前是要组成一个5路查找树,那么此时M=5,关键字数必须大于等于ceil(5/2)(这里关键字数 < 2,就要进行节点合并)。
(2)排序规则:满足节点本身比左边节点大,比右边节点小。
(3)关键字数小于2时先从子节点取,子节点没有符合条件时就向父节点取,取中间值往父节点放。
特点:
B 树相对于平衡二叉树的不同是:每个节点包含的关键字增多了,特别是在 B 树应用到数据库中的时候,数据库充分利用了磁盘块的原理(磁盘上的数据存储是采用块的形式存储的,每个块的大小为 4 K,每次磁盘 IO 进行数据读取时,同一个磁盘块的数据可以一次性读取出来),把节点大小的限制充分使用在磁盘块大小范围。由此,树的节点关键字增多后,B树的层级比原来的二叉树大幅度减少,从而大幅度减少数据查找的IO次数和查找时间。
六、B+树
6.1 概念
B+ 树是 B 树的一个升级版,B+ 树更加充分的利用了节点空间,让查询速度更加快速与稳定,其查询速度完全接近于二分查找。为什么说 B+ 树查找的效率要比 B 树更高、更稳定呢?
6.2 规则
(1)B+ 树跟 B 树不同,B+ 树的非叶子节点不保存关键字记录的指针,只进行数据索引,这样使得 B+ 树每个非叶子节点所能保存的关键字个数大大增加。
(2)B+ 树叶子节点保存了父节点的所有关键字记录的指针,所有的真实数据地址必须要查询到叶子节点时才能获取到,所以,在B+ 树中,每次数据查询的次数都一样。
(3)B+ 树叶子节点的关键字从小到大有序排列,左边结尾数据都会保存右边节点开始数据的指针。
(4)非叶子节点的子节点数 = 关键字数。
6.3 特点
(1)B+ 树的层级更少:相较于 B 树,B+ 树中的每个非叶子节点存储的关键字个数更多,树的层级更少,所以查询数据更快。
(2)B+ 树查询速度更加稳定:B+ 树所有关键字的数据地址都存在叶子节点上,所以,每次查找数据的次数都相同,所以,B+ 树的查询速度要比 B 树更稳定。
(3)B+ 树天然具备排序功能:B+ 树所有的叶子节点数据构成了一个有序链表,在查询范围区间的数据时更方便,数据紧密性很高,缓存的命中率也会比 B 树更高。
(4)B+ 树全节点遍历更快:B+ 树遍历整棵树只需要遍历所有的叶子节点即可,而不需要像 B 树需要对每一层进行遍历,这有利于数据库做全表扫描。
(5)B 树相对于 B+ 树的优点是:如果经常访问的数据离根节点很近,而 B 树的非叶子节点本身存有关键字数据的地址,所以,这个时候数据检索的效率会要比 B+ 树更高。
七、总结
7.1 相同的思想
从二叉排序树、平衡二叉树、红黑树、B 树、B+ 树,总体来看,它们所贯彻的思想都是相同的:都是想要充分采用数据平衡策略和二分查找法 来提升数据查找速度。
7.2 不同的方式来利用磁盘空间
不同点是它们一个一个在演变的过程中通过 IO 从磁盘读取数据的原理进行一步步的演变,每一次演变都是为了让节点的空间更合理更充分的运用起来,从而使树的层级减少,从而达到快速查找数据的目的。
99、参考
(1)数据结构(C++版) 清华大学出版社 王红梅版
(2)https://www.jianshu.com/p/b597aa97c9de
(3)https://zhuanlan.zhihu.com/p/56066942