持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第30天,点击查看活动详情
大家好呀,我是平衡蛋~
在上一个阶段,我带大家玩转了二叉搜索树,那在这个阶段我来和大家一起搞一下平衡二叉树。
如果你对平衡二叉树一点儿也不了解的话,你可能会感觉我从二叉搜索树转折到平衡二叉树有点生硬。
咱往下看就知道了~
平衡二叉树怎么来的
在这呢,我就先透漏一下平衡二叉树的半个概念,顺便也是解答上面说的平衡二叉树和二叉搜索树的关系:
其实叭,平衡二叉树也是一棵二叉搜索树。
那现在可能又有一个疑问,为啥有了二叉搜索树还要脱裤子放屁整出个平衡二叉树?
那就得从盘古开天辟地,呃,从二叉搜索树的一个特殊情况说起...
如果你看过我二叉搜索树的实战文章,在分析递归题解时间复杂度的时候我一般会说:xx 大小取决于二叉搜索树的高度,二叉搜索树最坏情况下的高度为 n。
问题就出在这个高度为 n 上。
比如对于序列 root = [1,2,3,4],正常情况下,我们构造的应该是下面这样的:
但总有不正常的时候,比如在老年痴呆的的情况下构造出的二叉搜索树可能是下面这样的:
你看这像啥样子,好好一棵二叉搜索树非得 cos 个”单链表“。
这就造成了,本来我查找一个节点的时间复杂度度是 O(logn),现在硬生生的成了 O(n)。
比如我正常情况下找 4 这个节点,找 2 次就够,而在 cos 情况下,我得找 4 次,这种现象就是二叉搜索树的退化。
从上面两张不同二叉搜索树的图可以看出,树的高度决定了它的查找效率,那我们是不是可以这么考虑:当树的高度最小时,就能够保证树的查找效率。
连我都能考虑出来的东西,显然大佬们早就解决了,当然了也没很早,也就区区...60 年叭~
1962 年,两个数学家 AV + L 提出了平衡二叉树的概念。
哎呀妈呀,终于套上了,至于平衡二叉树到底是个什么东西,我们下...面分解。
平衡二叉树
我在上一节中说了平衡二叉树的半个概念,现在来把它补充完整。
平衡二叉树,是一种二叉排序树,其中对于每个节点的左右子树高度相差小于等于 1。
后半句点出了平衡二叉树中”平衡“的实际指向,即”高度平衡“!
把二叉树左右子树的高度看成是左右子树的重量,把这两个重量放在天平上,尽可能保证这个天平是平衡的。
那怎么确定什么程度是”尽可能的平衡“呢?这里就引出一个概念:平衡因子(BF,Balance Factor)。
它的计算公式:平衡因子 = 左子树的高度 - 右子树的高度。
那对于平衡二叉树来说,它概念中”每个节点的左右子树高度相差小于等于 1“,这么来看,平衡二叉树的平衡因子只可能是 1,0,-1。
也可以这么说,只要你的平衡因子的绝对值 > 1,那你就不是平衡二叉树。
由此,我们在判断一棵二叉树是不是平衡二叉树,2 个条件:
- 这棵二叉树是二叉搜索树。
- 这棵二叉树的每个节点的 BF 绝对值 < 1。
比如下图:
这就不是一棵平衡二叉树,它首先就不满足条件 1:是一棵二叉搜索树。
比如下图:
这也不是一棵平衡二叉树,它虽然满足条件 1 是一棵二叉搜索树,但是不满足条件 2,对于节点 3 来说,它的 BF = 2。
比如下图:
这就是一棵平衡二叉树,它同时满足条件 1 和条件 2。
平衡二叉树就通过这种“尽可能的平衡”,让树的高度保持在 logn,这是相对最小的树高,也正因为是维持了这个树高,对于一棵有 N 个节点的平衡二叉树来说,它每一个操作的时间复杂度都可以维持在 O(logn) 。
对于平衡二叉树的操作主要有 3 种:
- 查找操作
- 插入操作
- 删除操作
查找操作我就不在这多说了,和二叉搜索树的查找一个套路。
下面我重点来讲下平衡二叉树的插入和删除操作,这个一定要仔细看,重点看!
插入操作
对于一棵平衡二叉树来说,插入一个新的节点可能会让平衡二叉树【失衡】。
比如对于下面这棵平衡二叉树:
当插入节点 7 的时候,成了下面这样:
此时节点 5 的 BF = -2,这就造成了平衡二叉树的失衡。
这里就会产生一个概念,叫做【最小不平衡子树】,即距离插入节点最近的,且平衡因子绝对值大于 1 的节点为根的子树。
在上图中,距离插入节点 7 最近的 BF 的绝对值大于 1 的节点就是节点 5,所以以节点 5 为根节点的二叉树就是最小不平衡子树。
那平衡二叉树失衡了,就要想办法让它重新平衡,这种失衡的调整主要就是通过调整最小不平衡子树来实现重新平衡。
这里的调整靠的是【旋转】,通过旋转让二叉树重新成为新的平衡二叉树。
根据旋转的方向不同,主要分为 2 种:
- 左旋
- 右旋
左旋
左旋操作主要是把最小不平衡子树的右孩子作为根进行旋转操作,具体步骤为:
- “根节点的右孩子”作为“新的根节点”
- “根节点的右孩子的左子树”(如果有)变为“根节点的右子树”
- “根节点”变为“根节点的右孩子”的左子树
可能看着比较绕...
比如对于下图:
最小不平衡子树的根节点为 5,根节点的右孩子为 6,根节点的右孩子的左子树为 null,所以经过左旋,变成了下面的样子:
右旋
与左旋对称的就是右旋,右旋是把最小不平衡子树的左孩子作为根进行旋转操作,具体步骤为:
- “根节点的左孩子“作为“新的根节点”
- ”根节点的左孩子的右子树“(如果有)变为”根节点的左子树“
- “根节点”变为“根节点的左孩子”的右子树
比如对于下图:
最小不平衡子树的根节点为 3,根节点的左孩子为 2,根节点的左孩子的右子树为 null,所以经过右旋,变成了下面这个样子:
不同插入节点的方式的不同旋转
对于平衡二叉树的节点 node 来说,有 4 种插入方式:
- 插入左孩子的左子树(LL 型)
- 插入右孩子的右子树(RR 型)
- 插入左孩子的右子树(LR 型)
- 插入右孩子的左子树(RL 型)
这些插入方式可能会造成 node 的 BF 绝对值大于 1,从而打破了原平衡二叉树的平衡。
针对这些不同的插入方式有着不同的旋转办法,下面我们一一来看。
LL 型失衡
针对插入左孩子的左子树失衡,只需要执行一次【右旋】即可。
插入节点 1,造成失衡,这时候最小不平衡子树的根节点为 3,直接进行一次右旋操作。
RR 型失衡
对于针对插入右孩子的右子树失衡,只需要执行一次【左旋】即可。
插入节点 7,造成失衡,这个时候最小不平衡子树的根节点为 5,直接进行一次左旋操作。
LR 型失衡
插入左孩子的右子树失衡,这就不是 1 步能解决的事了...
这个时候需要 2 步:
- 对最小不平衡子树根节点的左孩子左旋
- 最小不平衡子树右旋
插入节点 2,造成失衡,最小不平衡子树的根节点为 3。
第 1 步,此时最小不平衡子树根节点的左孩子为节点 1,将以节点 1 为根节点的子树左旋。
第 2 步,将最小不平衡子树右旋。
RL 型失衡
当然,插入右孩子的左子树失衡,也不是 1 步能解决的事,同样也需要 2 步:
- 对最小不平衡子树根节点的右孩子右旋
- 最小不平衡子树左旋
插入节点 6,造成失衡,最小不平衡子树的根节点为 5。
第 1 步,此时最小不平衡子树根节点的右孩子为节点 7,将以节点 7 为根节点的子树右旋。
第 2 步,将最小不平衡子树左旋。
那这 4 种情况我是都讲完了,情况就是这么些情况,说麻烦其实也不麻烦,就是绕点儿。
趁看到现在还明白点,赶紧自己动手画几遍。
删除操作
对于一棵平衡二叉树来说,它的删除情况和二叉搜索树一样,同样是分 3 种情况:
- 删除节点为叶子节点
- 删除的节点只有左子树或者右子树
- 删除的节点既有左子树又有右子树。
稍微有点区别的是删除完节点后,重新判断一下是否依然平衡,如果失衡,旋转操作就走起来。
这里需要注意的是,删除节点以后,可能存在多个不平衡的节点。
平衡二叉树因为删除节点判断失衡,我们可以换一种想法来理解:
- 删除的是左子树的节点,那左子树的高度 -1,这可以想成,左子树高度不变,右子树高度 +1,即相当于在右子树插入了一个新的节点。
- 同理,删除的是右子树的节点,那右子树的高度 -1,这也可以想成,右子树高度不变,左子树高度 + 1,相当于在左子树上插入了一个新的节点。
下面我简单介绍一下 3 种情况,至于更细节的内容我们在后面的实战文章中继续。
情况 1:删除节点为二叉树的叶子节点
第 1 步 删除:
通过平衡二叉树的查找操作找到节点,然后直接删掉就好了。
第 2 步 旋转:
从删除节点的父节点开始判断是否失衡,如果失衡,则判断是 LL、RR、LR、RL 中的哪种失衡,根据上面讲的调整方式进行旋转。
如果不失衡,就继续向父节点的父节点继续判断...
情况 2 :删除的节点只有左子树或者右子树
第 1 步 删除:
直接用删除节点的父节点指向它的子树即可。
第 2 步 旋转:
从删除节点的父节点开始判断是否失衡,如果失衡,则判断是 LL、RR、LR、RL 中的哪种失衡,根据上面讲的调整方式进行旋转。
如果不失衡,就继续向父节点的父节点继续判断...
情况 3:删除的节点既有左子树又有右子树
第 1 步 删除:
这个就稍微复杂点了,就是将要删除节点 node 位置上替换成左子树最右边的节点或者右子树最左边的节点。
即左子树的最大值或者右子树的最小值。
第 2 步 旋转:
删除以后的调整操作,还是与情况一和情况二的旋转操作一样调整。
平衡二叉树入门基础到这就讲完啦,哎呀妈呀,感觉写了好久,肩膀都写硬了...
平衡二叉树这玩意怎么说呢,看着挺难的,实际就是稍微复杂点,就是一步一步的拆出来看,就还好。
相信我在文章中已经讲的很明白了,认真看完的同学一定看懂了,如果你要说你没看懂,那...就再看一遍~
接下来就是通过实战题来巩固啦~
我是蛋蛋,我们下次见啦!