今天的博客是在上一篇博客的基础上进行的延伸。上一篇博客我们主要聊了二叉排序树,详情请戳《二叉排序树的查找、插入与删除》。本篇博客我们就在二叉排序树的基础上来聊聊平衡二叉树,也叫AVL树,AVL是发明平衡二叉树的两个科学家的名字的缩写,在此就不做深究了。其实平衡二叉树就是二叉排序树的一种,比二叉排序树多了一个平衡的条件。在一个平衡二叉树中,一个结点的左右子树的深度差不超过1。
本篇博客我们就依照平衡二叉树的特点,在创建二叉排序树的同时要保证结点的左右子树的深度差不超过1的规则。当我们往二叉排序树中插入结点时,我们要对二叉排序树的平衡性进行检查,如果因插入这个新的结点二叉排序树的平衡性被打破了,我们就得根据打破平衡二叉树的类型对二叉排序树进行调整使其再次进入到平衡二叉树的状态。当然,在删除结点时也要二叉树的平衡进行检查,发现不平衡时立马进行纠正。今天博客介绍的就是平衡二叉树的创建于结点的删除。废话少说,进入今天博客的主题。
一、平衡二叉树的结点
在博客的第一部分呢,我想先给出平衡二叉树的结点结构。当然是在上篇博客中的二叉排序树的结点上进行修改的。下方这个AVLTreeNote就是我们本篇平衡二叉树所使用的结点类。该类与二叉排序树的结点类差不多,就是增加了额外的三个字段。
- depth字段用来记录以该结点为根结点的树的深度,因为下方求平衡因子时会使用到该字段。
- balanceFactor字段就是我们所说的平衡因子,其实就是左子树的深度减去右子树的深度,因为一棵平衡二叉树的左右子树的深度差不会超过1,所以一颗平衡二叉树的节点的平衡因子为-1,0,或者1。如果为其他值,那么说明该平衡二叉树已不再平衡,需要被平衡。
- fatherNote字段用来指向该结点父节点,我们在调整二叉树的平衡时会用到该指针。
上面就是我们添加的三个字段,下方我们会分别给出depth与balanceFactor字段的计算方式。
从上面的代码段中我们可以看出,depth和balanceFactor这两个字段都是计算属性。接下来我们将给出这两个计算属性具体的计算方法。
下方这段代码就是depth计算属性的计算方法。计算方法也是比较简单的,当该结点的左右子树都存在时,depth就等于左右子树深度较大的那个值进行加1操作。如果左右子树有一个为空的话,那么depth的值就为不为空的那个子树的高度加1. 如果左右子树都为空的话,那么depth的值就为0。具体算法如下所示:
接下来我们来看一下平衡因子balanceFactor的计算方法,如下所示。从下方的代码段中,我们也不难看出计算方法也是比较简单的。如果左右子树都存在的话,平衡因子balanceFactor就等于左子树的深度减去右子树的深度。如果左子树不为空,右子树为空的话,那么balanceFactor就等于leftChild.depth + 1, 反之就等于-(rightChild.depth + 1)。如果左右子树都等于nil的话,那么平衡因子就为0。具体算法如下所示:
二、打破平衡的类型以及调整方法
平衡二叉树创建的过程与二叉排序树的创建过程大体相同,只不过是在新的节点插入到二叉排序树后,我们要对其进行平衡的检查。在检查过程中,如果发现不平衡的节点(平衡因子不为1,0或者-1的情况)我们就要对其进行相应的调整,让其平衡。当然插入节点打破平衡的情况总结起来总共有四种,也就是本部分要聊的这几种。大体可以分为左左(LL), 左右(LR), 右右(RR), 右左(RL),下方会对每一种情况进行详细的介绍,并给出调整平衡的方案,并且给出具体的代码的实现。
1、左左(LL)的情况
当我们往一个节点的左(Left)孩子的左(Left)孩子添加一个结点时,导致该节点出现了不平衡的情况,我们称之为这种不平衡的情况为左左(LL)的不平衡情况。下方这个示意图就是左左情况以及左左情况的平衡调整。
在下方示意图中,我们插入了一个3节点,导致8的平衡因子变成了2,因此我们要对8下方的树进行调整,将其调整为平衡二叉树。平衡的具体步骤以及指针的变化方式如下所示,在此就不做过多赘述了。
根据上述的示意图,我们不难给出左左情况下调整情况下的代码,下方就是具体的代码实现。如果我们要对8进行调整,那么我们只需要将8所对应的结点指针作为下方函数的参数即可。具体代码的意思请结合着上方示意图的步骤来看,因为每个结点都会有一个父节点指针,在调整完,不要忘记调整父节点的指向呢,其他的就不做过多赘述了。
2.左右(LR)的情况
该情况与上述情况类似,只不过是往一个结点的左子树的右子树上添加了一个新结点时导致的不平衡。这种不平衡的方式调整起来会麻烦一些,不过还算是好理解。我们先将左右的情况转换成左左的情况,然后调用左左的方法来进行调整。下方示意图就是将左右转换成左左的情况,然后再按照左左的情况进行调整。因为下方示意图比较明确了,在此就不做过多赘述了。
根据上面的示意图,给出相应的代码实现并不困难。代码段中的前几行代码就是将左右的情况转换成左左的情况,然后调用我们上一部分左左的方法进行调整。具体做法如下代码一致。
3.右右(RR)的情况
右右的情况与左左的情况极为相似,就是方向不同。右右情况就是因为往结点的右孩子的右孩子上添加了一个结点,然后导致该结点不平衡的情况。右右不平衡情况的调整与左左的步骤类似,只是操作的子节点不同。下方示意图就是右右不平衡情况的调整步骤。每一步的详细操作请看示意图下方的解说。
根据上述的示意图,然后在根据我们之前左左情况的代码,给出右右情况的代码要简单的多。下方的方法就是右右情况调整的代码段,其实就是根据左左情况改的。如下所示:
4.右左(RL)的情况
看完上面三步,右左什么意思就一目了然了。右左肯定是往该结点的右子树的左结点上添加一个结点导致以该结点为根节点的子树不平衡的情况。这种情况下,要进行树的调整也是比较好做的,我们就类比左右的那种情况。我们先将右左转换成右右的情况,然后在按照右右的情况进行调整即可。下方就是这种情况调整的完整过程。
根据上述过程,给出具体的代码实现并不困难,下发就是根据上述示意图给出的具体代码实现。
三、平衡二叉树创建的完整示意图
上面聊完调节平衡二叉树的具体方法后,接下来为了更直观的了解平衡二叉树的创建步骤,我们来一个完整的实例。观察一下一棵平衡二叉树从无到有的整个过程,下方就是整个过程。在本部分中,我们需要将下方的这个序列转换成平衡二叉树进行存储。
步骤1:取出3,加入到平衡二叉树中
因为此刻我们的平衡二叉树为空的,所以我们将3作为平衡二叉树的根节点。此刻3的左右子树为空,所以3的平衡因子为0,此刻我们现有的二叉树是平衡的不需要调整。
步骤2:将2取出,插入到平衡二叉树中
因为2比3小,所以将2作为3的左孩子。因为2是叶子节点,所以2的平衡因子和深度都为零。而节点3左子树的深度为1,右子树为空,所以3的平衡因子=左子树的深度-右子树深度+1 = 1。从平衡因子中判断,此刻我们的二叉树是平衡二叉树。
步骤3:取出1插入的二叉树中
因为1比2要小,所以将1作为2的左孩子。此刻我们不难计算出节点3的平衡因子为2,可以看出以3为根节点的树以及不在平衡,我们需要对其进行调整。我们不难发现,因为王3的左孩子的左孩子上添加了一个节点,所以属于左左的情况,我们需要按照上一部分左左的情况处理。将该树根据左左的情况再度调整到平衡,具体如下所示:
步骤4:将4插入到平衡二叉树中
下方将4插入到之前的平衡二叉树中,插入后,二叉树仍然是平衡的,如下所示:
步骤5:将5插入到平衡二叉树中
将5插入到平衡二叉树中后,我们发现以3为结点的子树是因为此节点引起的最小不平衡二叉树。所以我们需要以3结点为基准进行调整。不难看出,此种情况为RR的情况,按照RR的情况进行调整即可,如下所示:
步骤6:将6取出插入到平衡二叉树中
结点6插入后,我们不难看出结点2是最小不平衡二叉树的结点,而且是RR的情况,我们需要对其通过RR的情况进行调整,如下所示:
步骤7:将7插入到平衡二叉树
结点7插入后,我们可以看出,最小不平衡二叉树的结点是5,所以我们要以5为调整结点,按照RR的情况对我们不平衡的二叉树进行调整。具体如下所示:
步骤8:将10插入到平衡二叉树
10插入到平衡二叉树上,没有引起不平衡,我们保持不变。
步骤9:将9插入到平衡二叉树
将9插入后,引起了新的不平。最小不平衡二叉树的根节点为7,不平衡的情况为RL, 所以我们可以根据RL情况进行调整,如下所示:
步骤10:将8插入
节点8插入后,引起了二叉树的不平衡,最小不平衡二叉树的节点为6。不平衡的情况为RL, 我们可以根据RL情况对不平衡的二叉树进行调整。
四、创建平衡二叉树的具体代码实现
上面的示意图聊的也挺足的了,接下来我们就要给出完整的平衡二叉树创建的代码了。平衡二叉树的插入和查找与二叉排序树的插入和查找类似,只不过平衡二叉树在插入元素后需要的查找该树在插入节点后是不是平衡,如果不平衡就要根据相应调整平衡的策略进行调整。如果进行调整在本篇博客的第一部分我们就已经详细的给出了。本部分我们详细的给出如果在节点插入后寻找最小不平衡二叉树的根节点。
1.寻找最小不平衡二叉树的根节点
开门见山,下方就是寻找最小不平衡二叉树的根节点的代码,代码比较简单。因为每一个节点都有一个指针指向其父节点,所以我们可以以插入的结点为基准,依次往父节点找,知道找到那个平衡因子不为-1,0或者1的节点为止。如果找到该结点我们就调用调整平衡的相关函数即可,下方就是该过程的具体实现。
2.确定不平衡的类型
找到不平衡节点后,在对其进行调整之前,我们需要确定具体是那种不平衡类型。下方这个代码段,就是根据不平衡节点来确定不平衡类型的。具体确定方式如下:
如果根节点的平衡因子为2,则说明肯定是根节点是左孩子引起的不平衡,于是乎我们确定了第一个
左。然后我们在查看根节点的左孩子的平衡因子,
如果为1,那么说明是在根节点的左孩子上添加了左孩子导致的不平衡,所以是左左的情况。同理,
如果是-1,那么说明是在根节点的左孩子上添加了右孩子,所以是左右的情况。如果根节点的平衡因子为-1,那么很显然是根节点的右孩子引起的不平衡。参考左左,左右的情况,如果根节点右孩子的平衡因子为1,那么为右左的情况,如果根节点的右孩子的平衡因子为-1,那么为右右的情况。
上述过程用代码来表示,就如下所示:
3.具体调整平衡方法的调用
确定完不平衡类型后,我们需要根据fixNoBalanceType()方法提供的不平衡类型来调用相应的调整平衡的方法,具体代码如下所示。在Switch-Case语句中调用的这些方法,我们在本篇博客的第二部分已经给出了。
平衡二叉树的删除方法在本篇博客中就不做过多赘述了,在删除一个结点后,我们要以该删除结点的父节点为准,往上查找不平衡的那个点,然后根据我们聊的不平衡的情况进行调整即可。我们在github上分享的代码包括平衡二叉树的结点删除的代码,具体请查看github上的代码实现。
五、测试用例
本篇博客的测试用例,我们就使用第三部分使用的测试用例。在创建完平衡二叉树后我们对平衡二叉树中的结点进行删除,然后查看二叉树是否依然平衡。
下方是插入结点没有调用调整平衡的功能所创建的二叉排序树,我们可以看一下其中序遍历的输出结果。从结果中我们就可以看出,这棵二叉排序树是不平衡的。
接下来我们就要启动我们二叉排序树调节平衡的功能,下方就是我们创建的平衡二叉树输出的结构,以及每次调整平衡的结点。
而下方的输出结果是删除某个结点后的输出结果,因为我们在删除结点后,对二叉树也进行了检查,如果不平衡我们要对其进行调节,输出结果如下所示;
上述代码的运行结果如下所示:
本篇博客所涉及的demo在github上的分享地址如下:
github分享地址:https://github.com/lizelu/DataStruct-Swift/tree/master/AVLTree