目录
嗨,感谢你打开这篇文章!👋 如果阅读中发现任何笔误或表述不清的地方,欢迎随时留言告诉我~你的视角会让内容变得更完善!
一、平衡二叉树
1、平衡二叉树概述
1、引入
之前我们学习过二叉查找树,发现它的查询效率比单纯的链表和数组的查询效率要高很多,最理想的情况下时间复杂度可以达到O(logn),大部分情况下,确实是这样的,但在最坏情况下,二叉查找树的性能还是很糟糕。
例如我们依次往二叉查找树中插入9,8,7,6,5,4,3,2,1这9个数据,那么最终构造出来的树是长得下面这个样子:
在极端的情况下,二分查找树可能会退化成为链表,我们会发现,如果我们要查找1这个元素,查找的效率依旧会很低。效率低的原因在于这个树并不平衡,全部是向左边分支,如果我们能够把这棵二分查找数进行调整让左右子树的高度相等,并且左右子树的节点数也趋近于相等,那么查找效率就会大大提高,我们可以将这棵树调整成为一棵平衡二叉树。
2、平衡二叉树的概念
平衡二叉树是一种二叉排序树,其中每一个节点的左子树和右子树的高度差至多等于1,平衡二叉树又称为AVL树。平衡二叉树是一种高度平衡的二叉排序树,意思是说,要么它是一棵空树,要么它的左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值不超过1。
3、平衡因子
平衡因子(BF,Balance Factor 而不是 Boy Friend)指的是:左子树和右子树高度差。一般来说 BF 的绝对值大于 1,平衡树二叉树就失衡,需要「旋转」纠正。
4、最小失衡子树/最小不平衡子树
距离插入节点最近的,并且 BF 的绝对值大于 1 的节点为根节点的子树叫做最小失衡子树。
2、平衡二叉树的旋转
1、2 种「旋转」方式:
左旋:
(1)旧根节点为新根节点的左子树
(2)新根节点的左子树(如果存在)为旧根节点的右子树
右旋:
(1)旧根节点为新根节点的右子树
(2)新根节点的右子树(如果存在)为旧根节点的左子树
2、4 种「旋转」纠正类型:
(1)LL 型:插入左孩子的左子树,右旋
(2)RR 型:插入右孩子的右子树,左旋
(3)LR 型:插入左孩子的右子树,先左旋,再右旋
(4)RL 型:插入右孩子的左子树,先右旋,再左旋
1、LL 型失衡「右旋」
第三个节点(1)插入的 时候,BF(3) = 2,BF(2) = 1 LL 型失衡,右旋,根节点顺时针旋转。
(1)旧根节点(节点 3)为新根节点(节点 2)的右子树
(2)新根节点的 右子树 (如果存在)为旧根节点的左子树
2、RR 型失衡「左旋」
第三个节点(3)插入的 时候,BF(1)=-2 ,BF(2)=-1,RR 型失衡,左旋,根节点逆时针旋转。
(1)旧根节点(节点 1)为新根节点(节点 2)的左子树
(2)新根节点的左子树(如果存在)为旧根节点的右子树
3、LR型
第三个节点「3」插入的 时候,BF(3)=2 ,BF(1)=-1 LR 型失衡,先「左旋」再「右旋」。
左旋:
(1)旧根节点(节点 1)为新根节点(节点 2)的左子树
(2)新根节点的左子树(如果存在)为旧根节点的右子树
右旋:
(1)旧根节点(节点 3)为新根节点(节点 2)的右子树
(2)新根节点的 右子树(如果存在)为旧根节点的左子树
4、RL型
第三个节点(1)插入的 时候,BF(1)=-2, BF(3)=1 RL 型失衡,先「右旋」再「左旋」
右旋:
(1)旧根节点(节点 3)为新根节点(节点 2)的右子树
(2)新根节点的 右子树(如果存在)为旧根节点的左子树
左旋:
(1)旧根节点(节点 1)为新根节点(节点 2)的左子树
(2)新根节点的左子树(如果存在)为旧根节点的右子树
二、红黑树
1、红黑树的概念
红黑树也叫 R-B Tree,全称是Red-Black Tree,它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。
红黑树的特性:
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。[注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
2、红黑树的应用
红黑树的应用比较广泛,主要是用它来存储有序的数据,它的时间复杂度是O(lgn),效率非常之高。
例如,Java集合中的TreeSet和TreeMap,C++ STL中的set、map,以及Linux虚拟内存的管理,都是通过红黑树去实现的。
三、哈夫曼树
1、哈夫曼树的概念
当用 n 个节点(都做叶子节点且都有各自的权值)试图构建一棵树时,如果构建的这棵树的带权路径长度最小,称这棵树为“最优二叉树”。
2、哈夫曼树相关术语
1、路径和路径长度
在一棵树中,从一个节点往下可以达到的子或孙子节点之间的通路,称为路径(在上图种从根节点到节点 a 之间的通路就是一条路径)。通路中分支的数目称为路径长度。若规定根节点的层数为1,则从根节点到第L层节点的路径长度为L-1(上图中从根节点到节点 c 的路径长度为 3)。
2、节点的权
给每一个节点赋予一个新的数值,被称为这个节点的权。“权”一般代表“重要度”、概率等,我们形象的用一个具体的数字来表示,然后通过数字的大小来决定谁重要,谁不重要,谁的概率大,谁的概率低。
3、节点的带权路径长度
节点到根节点的路径长度乘以该节点的权(上图中节点 b 的带权路径长度为 2 * 5 = 10 )。
4、树的带权路径长度
树中各个叶节点的路径长度该叶节点的权的和(各叶子节点的带权路径长度之和),常用WPL(Weight Path Length)表示。
上图所示的这颗树的带权路径长度为:WPL = 7 * 1 + 5 * 2 + 2 * 3 + 4 * 3
3、哈夫曼树的构建方法
第一步: 我们将所有的节点都作为单独根节点。
第二步: 我们将最小的两个节点C和A组建为一个新的二叉树,权值为左右节点之和。
第三步: 将上一步组建的新节点加入到剩下的节点中,排除上一步组建过的左右子树,我们选中B组建新的二叉树,然后取权值。
第四步: 同上。
练习:请根据哈夫曼树的构建方法将以下结点构建成一棵哈夫曼树
4、哈夫曼树的访问
当哈夫曼树构造好以后,我们该如何访问到哈夫曼树上的叶子节点呢?
当我们在构造哈夫曼树时可以通过一定的的方法获取到每个叶子节点从根节点开始的访问路径,如果向左记为0,向右记为1,最终每个节点都可以用若干个0 1组成一个编码,该编码我们称之为“哈夫曼编码”。
假如有如下哈夫曼树: 每个叶子节点的哈夫曼编码为:
5、哈夫曼树的应用
哈夫曼编码是一种编码方式,可以用于无损数据压缩。编码之后的字符串的平均长度、期望值降低,从而达到无损压缩数据的目的。
数据在传输时只需要传输对应的哈夫曼编码即可,例如:发送 statespteasi,我们仅需要发送如下哈夫曼编码:
110111100111110010011100101111101100
如果我们发送哈夫曼编码给接收方,接收方该如何解码呢?
(1)通过一定的手段重构哈夫曼树或发送方将哈夫曼树提前发送给接收方
(2)通过哈夫曼编码遍历哈夫曼树
四、B树
1、B树的基本概念
B树是一种树状数据结构,它能够存储数据、对其进行排序并允许以O(logn)的时间复杂度进行查找、顺序读取、插入和删除等作。
假如当前有一颗m阶的B树(注意阶的意思是指每个节点的子节点的个数),那么其符合:
(1)每个节点最多有m个子节点
(2)除了根节点和叶子节点之外,其他的每个节点最少有m/2(向上取整)个子节点
(3)根节点至少有两个子节点,(除了第一次插入的时候,此时只有一个节点,根节点同时是叶子节点)
(4)所有的叶子节点都在同一层
(5)有k个子节点的父节点包含k-1个关键码
除了上面B树的性质外,B树还有几个特点:
1,树高平衡,所有的叶节点都在同一层
2,关键码没有重复,父节点中的关键码是其子节点的分解
3,B树保证树种至少有一部分比例的节点是满的。为什么这样说,在上面的性质2中,我们知道每个节点最少可以有 m/2个节点,注意这刚好是一半,没有太满,是因为可以给后续的添加,删除留有余地,这样以来节点不会频繁的触发不平衡,没有太空则意味着B树能够保证降低树的高度。
2、B树的插入
插入规则:在叶子结点上插入结点
假设现在构建一棵四阶B树,开始插入“30”,直接作为根节点,
插入“60”,大于“30”,放右边,
插入“90”,大于“60”,放右边,
继续插入“120”,直接添加的结果如下图,此时超过了节点可以存放容量,对于四阶B树每个节点最多存放3个值,此时需要执行分裂操作
分裂操作,先选取待分裂节点的中值,这里为“60”,然后将中值“60”放到父节点中,因为这里还没有父节点,那么直接创建一个新的父节点存放“60”,而原来小于“60”的那些值作为左子树,原来大于“60”的那些值作为右子树
继续插入“95”,因为比“60”大,放到右子树中,
继续插入“200”,因为比“60”大,放到右子树中 ,
此时超过了节点可以存放容量,需要执行分裂操作, 找到“90 95 120 200”之间的中值“95”,然后将中值“95”放到父节点中,父节点中的“90”小于“95”,于是放到“90”右边,而原来小于“95”的那些值作为左子树,原来大于“95”的那些值作为右子树
继续插入10, 100
继续插入 300
插入300后,需要执行分裂操作, 找到“100 120 200 300 ”的中值“300”,然后将中值“120”放到父节点中,父节点中的“60 95”小于“120”,于是放到最右边,而原来小于“120”的那些值作为左子树,原来大于“120”的那些值作为右子树
继续插入500,600
插入600后,需要执行分裂操作, 找到“200 300 500 600”的中值“300”,然后将中值“300”放到父节点最右边,而原来小于“300”的那些值作为左子树,原来大于“300”的那些值作为右子树
根节点中元素个数超过了4, 选取根节点的中值“95”,然后将中值“95”放到父节点中,由于还没有父节点,那么直接创建一个新的父节点存放“95”,而原来小于“95”的那些值作为左子树,原来大于“95”的那些值作为右子树。
插入250,比根节点大,往根节点的右子树遍历,因为右子树不是叶子节点,继续往下,因为250介于120和300之间,往第二个分支
3、B树的查找
对B树进行查找就比较简单,查找过程有点类似二叉搜索树,从根节点开始查找,根据比较数值找到对应的分支,继续往子树上查找。
比如查找“250”,"250"大于“95”,往右子树,“250”介于“120 300”之间,往第二个分支,在第二个分支中可以找到250。
五、B+树
1、B+树的基本概念
B+树是B树的升级。
(1)不同于B树,B+树的非叶子节点不再保存关键字记录的指针,只进行数据索引
(2)B+树叶子节点保存了父节点的所有关键字记录的指针,所有数据地址必须要到叶子节点才能获取,所以每次查询效率一样
(3)所有叶子节点都有一个指向右边叶子节点的指针
(4)所有数据都保存在叶子节点
2、 B+树的插入
下面是一颗5阶B树的插入过程,5阶B数的结点最少2个关键字,最多4个四个关键字。
空树中插入5
依次插入8,10,15
插入16
插入16后超过了关键字的个数限制,所以要进行分裂。在叶子节点分裂时,分裂出来的左节点2个记录,右节点3个记录。结果如下图所示:
还有另一种分裂方式,给左节点3个记录,右节点2个记录,10在左节点或在右节点都可以。
插入17, 18
插入18后关键字个数大于5,进行分裂。分裂成两个节点,左节点2个记录,右节点3个记录,关键字16进位到父节点(索引类型)中,将当前节点的指针指向父节点
插入6、9、19、20
插入20后需要进行分裂
插入21、22
插入22后分裂
插入7
插入7后需要分裂
根节点的关键字个数超过4,需要继续分裂。左节点2个关键字,右节点2个关键字,关键字16进入到父节点中,将当前节点指向父节点,结果如下图所示:
3、B+树的应用
Mysql数据库中使用B+树做索引:
在对数据库进行查询时,我们可能经常会使用类似如下查询语句:select name from table where ID >=80 and ID <=90,去查找某个区间的内容。
因为B+树的所有叶子节点是一种链式结构,因此在查找80到90之间的所有数据时,我们只需要找到80,然后沿着链表往后遍历即可找到80到90之间的所有数据。