吐血整理之树树树

前言

数据在计算机中的存储结构主要为顺序存储结构、链式存储结构、索引存储结构、散列存储结构,其中链式存储结构最常见的示例是链表和树,链式存储结构主要有以下特点:
优点:逻辑相邻的节点物理上不必相邻,插入、删除灵活,只需改变节点中的指针指向

缺点:存储空间利用率极低,需通过指针维护结点间的逻辑关系;查找效率比顺序存储慢

二叉树简单知识

度:当前节点下的子节点个数
深度:所有节点的层次最大值

二叉树是每个节点最多有两个子树的树结构,左侧子树称为“左子树”(left subtree),右侧子树称为“右子树”(right subtree)。

二叉树的特点
至少有一个节点(根节点)
每个节点最多有两棵子树,即每个节点的度小于3
左子树和右子树是有顺序的,次序不能任意颠倒。
即使树种某节点只有一颗子树,也要区分它是左子树还是右子树。

满二叉树

除了叶子节点外每一个节点都有两个子节点,且叶子节点都在二叉树的同一高度上。
在这里插入图片描述
完全二叉树

如果二叉树中除去底层节点后为满二叉树,且底层节点依次从左到右分布,则此二叉树被称为完全二叉树。
在这里插入图片描述

二叉查找树(Binary Search Tree - BST)

二叉查找树树根节点的值大于其左子树中任意一个节点的值,小于其右子树中任意节点的值。
在这里插入图片描述
二叉查找树的插叙效率介于O(log n)~O(n)之间,理想的排序情况下查询效率为O(log n),极端情况下BST就是一个链表结构(如下图),此时元素查找的效率相当于链表查询O(n)。
在这里插入图片描述
二叉查找树需要注意的是删除节点操作的不同情况,删除节点根据节点位置会有以下三种情况:

删除节点的度为0,则直接删除

删除节点的度为1,则该子节点替代删除节点

删除的度为2.则从左子树中寻找最大的节点替代删除节点。对树结构改动最少、节点值最进行删除节点值的必然是左子树中的最大叶子节点值与右子树中的最小叶子节点值

为什么不用右子树中的最小叶子节点值取代删除节点?个人认为是为了维持范围值(纯属臆测):

右子树中的最小叶子节点值大于删除节点左子树中的所有节点,但若该叶子节点比删除节点大很多,这将会大大扩大左子树的范围值,左子树可插入的范围值也会大大增大,对左子树的查询效率造成较大的影响 左子树中的最大叶子节点值也大于删除节点左子树中其它所有的节点,虽然是使用该节点替代删除节点会缩小的左子树的值范围,但也减少左子树的插入范围值,对左子树的查询影响不大

由上可以看出,二叉查找树(BST)无法根据节点的结构改变(添加或删除)动态平衡树的排序结构,也因此对某些操作的效率造成一定的影响,而AVL树在BST的结构特点基础上添加了旋转平衡功能解决了这些问题。

平衡二叉搜索树 (Balanced binary search trees,又称AVL树、平衡二叉查找树)

AVL树是最早被发明的自平衡二叉搜索树,树种的任一节点的两个子树的高度差为1,所以也被称为高度平衡树,其查找、插入和删除在平均和最坏情况下的时间复杂度都是O(log n)。

平衡二叉搜索树右由Adelson-Velskii和Landis在1962年提出,因此又被命名为AVL树。平衡因子(平衡系数)是AVL树用于旋转平衡的判断因子,某节点的左子树与右子树的高度(深度)差值即为该节点的平衡因子。

AVL树的特点

具有二叉查找树的特点(左子树任一节点小于父节点,右子树任一节点大于父节点)

任一节点的左右子树高度差小于1,即平衡因子为范围为[-1,1]

为什么选择AVL树而不是BST

大多数BST操作(如搜索、最大值、最小值、插入、删除等)的时间复杂度为O(h),其中h是BST的高度。对于极端情况下的二叉树,这些操作成本可能变为O(n)。如果确保每次插入和删除后树的高度都保持O(log n),则可以保证这些操作的成本都是O(log n)。

节点插入、旋转

AVL树插入节点:

根据BST插入逻辑将新节点插入树中

从节点往上遍历检查每个节点的平衡因子,如发现有节点平衡因子不在[-1,1]范围内,则通过旋转重新平衡以u为根的子树

旋转的方式:

左旋转:用于平衡RR情况,对失衡节点u(unbalanced)及子树进行左旋

右旋转:用于平衡LL情况,对失衡节点u及子树进行右旋

左右旋转:用于平衡LR情况,对失衡节点失衡u的左节点ul左旋,再对失衡节点u右旋

右左旋转:用于平衡RL情况,对失衡节点u失衡方向的右子节点ur右旋,在对失衡节点u进行左旋

LL - 插入节点是失衡节点u左子节点ul上的左子树节点
gif图中的高度是从叶子节点开始计算的,因为插入节点后是从下往上检测节点的平衡因子,所以叶子节点高度恒为1更方便平衡因子的运算
在这里插入图片描述
在这里插入图片描述
LR - 插入节点是失衡节点u左子节点ul上的右子树节点
在这里插入图片描述
在这里插入图片描述
RR - 插入节点是失衡节点u右子节点ur上的右右子树节点
在这里插入图片描述
在这里插入图片描述
RL - 插入节点是失衡节点u右子节点ur上的左子树节点
在这里插入图片描述
在这里插入图片描述

规律总结

失衡节点到其最底部叶子节点的高度不会超过4

失衡节点哪里不平衡就会往哪里的反向旋转

添加的节点到失衡节点的路径如果是一条直线(级LL或RR),则只需对失衡节点u进行反向旋转

添加的节点到失衡节点的路径如果是一条曲线(即LR或RL),则需先对该路径上失衡节点u的子节点(ul/ur)进行旋转,再对失衡节点进行旋转

失衡节点u旋转后会成为它原来子树(ul/ur)中的一颗子树,如果u旋转时替代u的子树已有u旋转方向上的子树,那么该子树会断裂成为u的子树(如下LR的u右旋,uls已有右子树T2,故会T2断裂以BST的规则重新插入成为u的子树)

节点删除步骤

对删除节点D根据BST规则执行删除

选择平衡,该步骤与插入区别不大,从D节点往上遍历检查每个节点的平衡因子,若发现有节点失衡,则通过旋转重新平衡以u为根的子树

例子:
在这里插入图片描述

根据BST规则删除节点133,155替代133位置

从155位置往上检测到100为失衡节点u,左高右低为LR情况,对u左子节点ul=37左旋,再对u节点执行右旋(可以看成对50同时插入2个子节点导致100节点失衡)

AVL树伪代码

public class AVL {
	private Node root;
	
	private class Node{
		/*height 从叶子节点开始计算(即叶子节点恒为1,
		 方便遍历父节点的平衡因子的计算)*/
		int val,height;
		Node left;
		Node right;
		public Node (int val) {
			height = 1;
			this.val = val;
		}
	}
	public void insert(int val) {
		if (root == null) {
			root = new Node(val);
		} else {
			insert(root,val);
		}
	}
	private void insert(Node node, int val) {
		// TODO Auto-generated method stub
		/*
		 *获取失衡因子:left.height - right.height 
		 */
	}
}

红黑树(Red - Black Tree)

红黑树是一种自平衡二叉搜书树,且红黑树节点遵循以下规则:

(1) 每个节点或者是黑色,或者是红色。
(2) 根节点是黑色。
(3) 每个叶子节点是黑色。 [注意:这里叶子节点,是指为空的叶子节点!]
(4) 如果一个节点是红色的,则它的子节点必须是黑色的。
(5) 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

对比AVL树

AVL树比红黑树更加平衡,但AVL树可能再插入和删除过程中引起更多旋转。因此,如果应用程序涉及许多频繁的插入和删除操作,则应首选Red Black树(如Java 1.8中的HashMap)。如果插入和删除操作的频率较低,而搜索操作的频率较高,则AVL树应优于红黑树。

个人引申的疑问

为什么红黑树也算平衡树?它的平衡因子是什么?

为什么AVL比红黑树更平衡?为什么AVL树插入和删除会引起更多的旋转?

插入节点

红黑树插入节点后违反的主要规则是两个连续的红色节点

插入步骤:

将新节点n按照BST规则插入,且使新节点为红色

根据n的父节点p情况执行不同的操作

1.当新增节点父节点是黑色,直接插入、不会破坏树的平衡
在这里插入图片描述
2.当新增节点父节点是红色,叔节点是红色,破坏性质4,不能出现连续的红节点,需要平衡操作
在这里插入图片描述
3.当新增节点父节点是红色,叔节点不存在或者为黑色(叔节点肯定不为黑色,如果是黑色,破坏了性质5),还是破坏性质4,需要平衡操作
(a)当父节点是祖父节点左孩子,且新增节点是父节点左孩子,先变色,再右旋
在这里插入图片描述
(b)当父节点是祖父节点的左孩子,新增节点是父节点右孩子
在这里插入图片描述
©当父节点是祖父节点右孩子、新增节点是父节点右孩子
在这里插入图片描述
(d)当父节点是祖父节点的右孩子,新增节点是父节点的左孩子在这里插入图片描述
引申疑问自答

红黑树是根据节点的值和颜色维持平衡的,即可把颜色看成平衡因子,所以即使左右子树的高度差>=2也不一定像AVL树一样为了保持平衡而旋转

AVL树的结构主要是围绕节点值与左右子树高度来保持平衡的,从节点值的角度考虑自然比红黑树更平衡,且值搜索时AVL效率更高,但插入与删除较多时AVL树旋转操作会比红黑树更多,效率自然慢

以上也是Java 8的HashMap中树节点实现结构采用红黑树而不是AVL树的原因

删除节点

1.被删除节点是红色
(a)红色是叶子节点,直接删除
在这里插入图片描述
(b)红色节点存在两个孩子(不可能存在一个孩子),找到代替节点,替代节点为红色:只能为红色节点,替代节点替换被删节点,删掉替代节点(以前驱结点为例)
在这里插入图片描述
替换节点为黑色:(可能是黑色叶子节点或者替换节点存在一个红色左孩子),先平衡替换节点,替换节点替换被删节点,再删除替换节点。
平衡操作:替代节点的父节点是红色,父节点变黑、兄弟节点变红,然后进行替换节点替换被删节点,删除替换节点.(先平衡后替换,最后删除)

2.被删节点是黑色(部分需要平衡操作,下面平衡的步骤详解)
     (a)、被删节点是黑色叶子节点,需要平衡操作
     (b)、存在替代节点,且为黑色,先平衡操作,在删除替代节点,替代节点和被删节点替换。
     (c)、替代节点是红色(只能是红色的叶子节点,负责不满足平衡树),直接删除替代节点,替代节点与被删节点替换即可

B-Tree(B树)

  • 大多数自平衡树(如AVL树和红黑树)都会假定所有数据都在主内存中,但我们必须考虑无法容纳在主内存中的大量数据。

  • 当键的数量很大时,将以块的形式从磁盘读取数据,与主存储器访问时间相比,磁盘访问时间非常高。

  • B树是一种自平衡搜索树,设计的主要思想是减少磁盘访问次数。大多数树操作(增、删、改、查、最大值、最小值等)都需要O(h)磁盘访问,h为树的高度。

  • B树通过在节点中放置最大可能的键来保持B树的高度较低。通常,B树节点的大小保持与磁盘块大小相等。由于B树的高度较低,因此与平衡的二叉搜索树(AVL树、红黑树等)相比,大多数操作的磁盘访问次数显著减少

  • 磁盘块是一个虚拟的概念,是操作系统中最小的逻辑存储单位,操作系统与磁盘打交道的最小单位是磁盘块。

  • 一颗m阶(m指一个节点中最多包含的子节点树)B树特点如下:

  • 所有叶子处于同一水平位置

  • 除根结点外的每个节点都必须至少包含m/2-1个key,并且最多具有m-1个key,除根以外的所有非叶子节点必须至少具有m/2个子节点

  • 节点的子节点数等于节点的key数加1

  • 节点的所有key都按键值升序排序,两个键k1和k2之间的子key包含k1和k2范围的所有键

  • 与其他平衡二叉搜索树一样,搜索、插入和删除的时间复杂度为O(log n) p.s:B树由属于最小度t定义,t的值取决于磁盘块的大小,数值上m = 2t。

搜索

B-树搜索类似搜索二叉树,算法与递归算法相似。在B树中,搜索过程也是从根节点开始,通过与节点key值比较搜索,搜索操作的时间复杂度为O(log n)。具体的搜索步骤如下:

1.将搜索值与树中根节点的第一个key进行比较

2.匹配则显示“找到给定节点”并结束搜索,否则进入步骤3

3.检查搜索值是大于还是小于当前key值。
搜索值小于当前key:左子树中获取第一个key进行比较,重复2、3步骤
搜索值大于当前key:将搜索值与同一节点中的下一个key进行比较,重复2、3步骤,知道精确匹配,或搜索值与叶子节点中的最后一个key值相比较

如果叶子节点中的最后一个键值也不匹配,则显示“找不到元素”并结束搜索
在这里插入图片描述

插入

在这里插入图片描述
该B树最下度为3,所以节点最多有5个key,最少有2个key。
b) 插入B:孩子未满,直接插入
c) 插入Q:孩子已满,分裂子树,key T上移到父节点中,然后在将Q插入到适当的孩子中
d) 插入L:root已满,生成新root节点,分裂老root节点,在适当子树中插入适当孩子中
e) 插入F:孩子已满,分裂子树,key C上移到父节点,在适当节点中插入Q

删除
  1. 待删除key如果在当前节点中,转2,否则转8
  2. 当前节点是叶子,直接删除,完成删除操作。否则转3
  3. 待删除key分割的子树中,前一棵子树key的数量大于t-1,转4,否则转5.
  4. 从前一颗子树中删除该子树根节点中最大的key,将该key替换当前节点中待删除key,完成删除操作。
  5. 待删除key分割的子树中,后一棵子树的key数量打于t-1,转6,否则转7.
  6. 从后一颗子树中的根结点中删除该节点最小的key,用该key替换待删除key,完成删除。
  7. 合并该节点分割的两个子树,并从合并之后的子树中删除待删除key。
  8. 找到key可能存在的子树Tn,转9
  9. 该子树前一颗子树Tn-1的根节点key数量大于t-1,转10,否则转12
  10. 将Tn-1中最大的key替换当前节点中适当的key,并将被替换的key插入到Tn中,转11
  11. 将Tn-1中最后一个孩子,移动到Tn中适当的位置,将删除操作传递到Tn中。
  12. Tn的后一颗子树Tn+1的根节点key数量大于t-1,转13,否则转?
  13. 将Tn+1中最小的key替换当前节点,并将被替换的key插入到Tn+1中,转14
  14. 将Tn+1中最小的子树移动到Tn中,将删除操作传递Tn中。
    删除中,可能会出现根节点没有key的情况,所以删除结束之后需要检查根节点,若发生这种情况,需要将根节点更新为原根节点的唯一的一颗子树。
    示例:
    在这里插入图片描述

B+树

B+树是B-树的变体,也是一种多路搜索树, 它与 B- 树的不同之处在于:

  1. 所有关键字存储在叶子节点出现,内部节点(非叶子节点并不存储真正的 data)
  2. 为所有叶子结点增加了一个链指针

在这里插入图片描述
B-树vsB+树

1.B树的由于每个节点都有key和data,所以查询的时候可能不需要O(logn)的复杂度,甚至最好的情况是O(1)就可以找到数据,而B+树由于只有叶子节点保存了data,所以必须经历O(logn)复杂度才能找到数据
2.B+树叶节点两两相连可大大增加区间访问性,可使用在范围查询等,而B-树每个节点 key 和 data 在一起,则无法区间查找。
3.由于B+树的叶子节点的数据都是使用链表连接起来的,而且他们在磁盘里是顺序存储的,所以当读到某个值的时候,磁盘预读原理就会提前把这些数据都读进内存,使得范围查询和排序都很快
4.由于B树的节点都存了key和data,而B+树只有叶子节点存data,非叶子节点都只是索引值,没有实际的数据,这就时B+树在一次IO里面,能读出的索引值更多。从而减少查询时候需要的IO次数!

拓展
InnoDB的主键选择与插入优化

在使用InnoDB存储引擎时,如果没有特别的需要,请永远使用一个与业务无关的自增字段作为主键。

经常看到有帖子或博客讨论主键选择问题,有人建议使用业务无关的自增主键,有人觉得没有必要,完全可以使用如学号或身份证号这种唯一字段作为主键。不论支持哪种论点,大多数论据都是业务层面的。如果从数据库索引优化角度看,使用InnoDB引擎而不使用自增主键绝对是一个糟糕的主意。

上文讨论过InnoDB的索引实现,InnoDB使用聚集索引,数据记录本身被存于主索引(一颗B+Tree)的叶子节点上。这就要求同一个叶子节点内(大小为一个内存页或磁盘页)的各条数据记录按主键顺序存放,因此每当有一条新的记录插入时,MySQL会根据其主键将其插入适当的节点和位置,如果页面达到装载因子(InnoDB默认为15/16),则开辟一个新的页(节点)。

如果表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页。如下图所示:

在这里插入图片描述
这样就会形成一个紧凑的索引结构,近似顺序填满。由于每次插入时也不需要移动已有数据,因此效率很高,也不会增加很多开销在维护索引上。

如果使用非自增主键(如果身份证号或学号等),由于每次插入主键的值近似于随机,因此每次新纪录都要被插到现有索引页得中间某个位置:
在这里插入图片描述
此时MySQL不得不为了将新记录插到合适位置而移动数据,甚至目标页面可能已经被回写到磁盘上而从缓存中清掉,此时又要从磁盘上读回来,这增加了很多开销,同时频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续不得不通过OPTIMIZE TABLE来重建表并优化填充页面。

因此,只要可以,请尽量在InnoDB上采用自增字段做主键。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值