(d4)AVL树:(3+4)-重构

官方信息技术老师·18 页·深入(追求细节与边界)·0 次浏览·2 天前
AVL树平衡重构二叉树

AVL树:(3+4)-重构

看完你能亲手画出四种失衡的(3+4)重构并理解其统一本质

按 空格/→ 演示下一步

1 / 18 页

全部页面点击任意一页,跳回舞台从这页播放

AVL树平衡重构二叉树

AVL树:(3+4)-重构

看完你能亲手画出四种失衡的(3+4)重构并理解其统一本质

1第 1 页 · AVL树:(3+4)-重构

AVL树失衡的四种情况

上一节我们用 (3+4)-重构统一处理所有失衡。但失衡的形状本身只有四种——都由失衡节点往下走两步的方向决定。

LL型
左孩子的左子树高,单次右旋修复
RR型
右孩子的右子树高,单次左旋修复
LR型
左孩子的右子树高,先左旋后右旋
RL型
右孩子的左子树高,先右旋后左旋
祖孙节点
所有旋转只动失衡节点、孩子、孙子三个位置
路口两次拐弯对应 →失衡路径四型

每次左或右,两次拐弯 = 2×2 = 四种组合,对应四种失衡

2第 2 页 · AVL树失衡的四种情况

为什么需要3+4重构

前页说过失衡有四种,对应四种旋转——LL/RR用单旋、LR/RL用双旋。但仔细看,这四套手法其实是同一件事的四个影子。3+4把它们收编成一套统一操作。

单/双旋四套手法
LL和RR用单旋、LR和RL用双旋,需分别记忆
3+4抽象统一视角
聚焦失衡点向上的3个节点与挂载的4棵子树
一套覆盖四种情况
不论失衡类型,按同一套重接流程即可
乐高拆装对应 →3+4重构

3块积木对应3节点,统一卡口对应4子树重接

3第 3 页 · 为什么需要3+4重构

失衡节点与祖孙三代

上一页我们看到四种失衡形态,但要动手 (3+4) 重构,得先从插入点往上找到「当事人」——失衡的祖父 g、它的孩子 p,以及 p 的孩子 v。

g 失衡祖父
从插入点向上找,第一个 |BF|>1 的祖先节点
p 中间父亲
g 的孩子,位于 g 到插入节点的路径上
v 当前孩子
p 朝插入点方向的孩子,插入点必在 v 子树内
同一条路径
g、p、v 三者串在同一条根到叶的路径上
跷跷板失衡对应 →g→p→v 失衡链

一端加 v 后,支点 g 失去平衡,p 是中间传力的座位

4第 4 页 · 失衡节点与祖孙三代

四种情况的节点标识对比

四种失衡横向并排;每型都是 g→p→v 三代,边上 L/R 标出该层走向。

图解渲染中…
g失衡节点:从插入点向上首个 ±2 的祖先pg 在插入路径方向的孩子vp 在插入路径方向的孩子,常是插入点L边上字母标记该层孩子为左或右
5第 5 页 · 四种情况的节点标识对比

中序遍历序列的规律

上一页我们圈出了 g、p、x 三个节点。重构把它们挪到哪?答案藏在「中序遍历」这条不变的线上。

中序不变性
无论怎么旋转,这三个节点在中序序列中的相对次序永远不变
中间值为新根
大小居中的那个键值,重构后必然坐上新子树的根节点位
有序性保住
保住中序 = 保住 BST 的有序查找能力
三个人重新排队对应 →中序相对位置不变

换队形不换顺序:谁在前谁在后不变,只调站位距离

6第 6 页 · 中序遍历序列的规律

3+4重构的整体框架

三节点、四棵子树的中序排列与重新连接

3+4重构的整体框架
三节点、四棵子树的中序排列与重新连接
7第 7 页 · 3+4重构的整体框架

三种重构形态的图解

LL/LR/RR/RL四种情况对应的中序排列结果

三种重构形态的图解
LL/LR/RR/RL四种情况对应的中序排列结果
8第 8 页 · 三种重构形态的图解

3+4重构的六步执行流程

六步把失衡子树原地重构成平衡子树,每步都不可省。

1
定位祖节点
从插入点向上找第一个失衡祖先 g
2
确认父子
沿失衡路径向下找子 p、孙 c,凑齐三代
3
选定新根
g、p、c 按中序排序,中间者升级为新根 b
4
拆出四子树
三节点向外伸出的 4 棵子树按中序记为 T0~T3
5
重接父子
b 与剩下两节点按中序连成父子关系
6
挂回四子树
T0~T3 按中序挂到 b 及两子的左右孩子位
9第 9 页 · 3+4重构的六步执行流程

平衡因子的修复过程

沿着流程看:记录原高度→3+4重构→验证新高度恰好少一层,bf才算真正修好。

图解渲染中…
a1失衡链条上的祖g、父p、子cd1按中序排列的四个子树片段f1bf = 左高 - 右高,由e1的结果决定g1重构后高度必须等于原高减一,证明失衡彻底消除
10第 10 页 · 平衡因子的修复过程

rotateAt()函数的设计思路

六步重构走完后,我们要把这套操作封装成一个函数,调用一次就完成整个修复。不同失衡情况处理逻辑不同,但入口应该统一——这就是 rotateAt() 的设计目标。

唯一参数
只传入失衡节点 g,p 和 n 由函数内部沿树向上探查得到
内部判定
根据 g、p、n 的左右孩子方向,自动归类为 LL/LR/RL/RR
返回新根
返回重构后子树的根节点,由调用者接回原树
指针重接
调整 n、p、g 与外部父节点、四个子树间的 parent 指针
高度修复
自底向上重新计算三个节点的高度(平衡因子)
智能维修工对应 →rotateAt 函数

接到失衡节点像接到报修单,自己判断故障类型再动手修

rotateAt(g)n\text{rotateAt}(g) \to n'
11第 11 页 · rotateAt()函数的设计思路

rotateAt()的伪代码实现

cpp

从 v 沿 parent 找到 p、g,用两层 h 方向判定 LL/LR/RR/RL,再把三代和四棵子树交给 connect34。

代码高亮加载中…

两层 h 判断先定 p 相对 g 的方向,再定 v 相对 p 的方向;四个 return 按 LL、LR、RR、RL 传入 3+4 的七个参数。

12第 12 页 · rotateAt()的伪代码实现

h方向的判断逻辑图解

沿箭头判断:先看g→p方向定支线,再读p→n方向h决定单/双旋。

图解渲染中…
a1首个失衡祖先节点b1g→p连线方向决定支线d1p→n方向即he1同向单旋,异向双旋
13第 13 页 · h方向的判断逻辑图解

连接t1~t4的指针操作

cpp

connect34() 的核心:六行指针赋值覆盖所有失衡情况。

代码高亮加载中…

前 4 行把四棵子树按中序挂到 a、c 下;后 2 行让 b 接管 a 和 c 成为新子树根——这就是 (3+4) 重构的统一形式。

14第 14 页 · 连接t1~t4的指针操作

3+4重构 vs 传统单旋/双旋

四种失衡,传统旋转要写四套逻辑;3+4 一套搞定。边界与复用的差异在哪儿?

传统单旋/双旋
  • LL/RR/LR/RL 四种情况各自写代码
  • 旋转后平衡因子要逐情况修复
  • 每个旋转函数单独处理边界
3+4重构
  • rotateAt() 一套函数覆盖全部四种
  • 中间节点左右子树高决定新平衡因子
  • 统一从 t1~t4 接指针,边界一次处理
学旋转原理选传统,写工程代码选3+4:统一模板消重复,中序承接一次解决边界。
15第 15 页 · 3+4重构 vs 传统单旋/双旋

时间复杂度分析

上一页我们连接了 t1~t4、调整了三个节点——表面看操作不少,实际却只花常数时间。为什么?我们来数清楚到底做了多少事。

常数个节点被触及
只有g、p、v这3个节点参与调整
子树整体搬迁
t1~t4作为整块重新连接,内部不访问
指针修改有上界
至多十余次指针赋值,与节点数n无关
不向下递归
重构只在这一层完成,不进入子树
整箱搬家对应 →3+4重构

只挪4个箱子的位置,箱内物品从不重排

16第 16 页 · 时间复杂度分析

3+4重构核心理解测验

点击作答

在(3+4)重构中,g是失衡节点,p是g更高的孩子,v是p更高的孩子。若p是g的右孩子、v是p的左孩子,应执行什么旋转?

17第 17 页 · 3+4重构核心理解测验

课后思考

三道开放题,先自己想三分钟,再点开参考答案对照思路。

1为什么3+4重构在四种失衡情况下都只动祖孙三代这一个局部?

参考答案失衡高度只受这代子树影响,向上传递即可;重构恢复高度后祖先无需调整。这是AVL'局部修复'的核心保证。

2如果要在3+4重构里同时记录'重构前的子树高度'用于回溯,该如何扩展rotateAt()?

参考答案可在rotateAt()前保存三个节点原高度到临时变量,重连后用真值重算。高度由max(左高,右高)+1推导,不必额外存储。

3如果允许h方向上的子树为空(即节点只有两个),三种重构形态还够吗?

参考答案够。t1~t4只是'槽位',是否为空指针不影响形态枚举,但需增加空指针判断,避免悬挂访问。

18第 18 页 · 课后思考