1. 约束
-
对于节点 vvv,我们定义其平衡因子为:vvv 的左、右子树的高度差。
-
AVL 树是一棵二叉搜索树,但其引入了额外的约束以保证其平衡性:树中各节点的平衡因子的绝对值不得超过 111。
-
设一共有 nnn 个节点,AVL 树始终可以将树的高度控制在 O(logn)O(\log n)O(logn) 以内,且每次搜索、插入和删除操作均可在 O(logn)O(\log n)O(logn) 时间内完成。
2. 插入
往 AVL 树中插入新节点可能会增加树的高度,从而导致其所在的子树失衡。
(1)首先按照二叉搜索树的插入方式插入新节点 xxx,;
(2)然后从 xxx 开始,沿着父指针逆行而上,直至找到第一个失衡的祖先节点(最低的失衡祖先),将其记为 ggg;
(3)找到以 ggg 为根的子树中最高的子树,将其根记为 ppp;
(4)找到以 ppp 为根的子树中最高的子树,将其根记为 vvv;可见新节点 xxx 一定位于子树 ppp、vvv 中(xxx 可能等于 vvv);
根据 g,p,vg,p,vg,p,v 的方向,可以分为四种情况:
(1)ppp 是 ggg 的左孩子,且 vvv 是 ppp 的左孩子;
(2)ppp 是 ggg 的左孩子,且 vvv 是 ppp 的右孩子;
(3)ppp 是 ggg 的右孩子,且 vvv 是 ppp 的左孩子;
(4)ppp 是 ggg 的右孩子,且 vvv 是 ppp 的右孩子;
其中,(1)和(4)是对称的情况,可以通过单旋操作来实现重平衡;(2)和(3)是对称的情况,可以通过双旋操作来实现重平衡。
情况(4)的重平衡操作如下所示:
虚线连接的两个灰色方框表示其中一个不为空(xxx 节点),另一个为空。
以 ggg 为轴进行 zag 旋转(设 ggg 的父节点为 pgpgpg):
(1)将 ggg 逆时针往下拉,使之成为 ppp 的新的左孩子;
(2)将 ppp 的旧的左孩子过继给 ggg,使之成为 ggg 的新的右孩子;
(3)更新 ppp 的父节点为 pgpgpg、pgpgpg 的子节点为 ppp。
情况(3)的重平衡操作如下所示:
虚线连接的两个灰色方框表示其中一个不为空(xxx 节点),另一个为空。
以 ppp 为轴进行 zig 旋转:
(1)将 ppp 顺时针往下拉,使之成为 vvv 的新的右孩子;
(2)将 vvv 的旧的右孩子过继给 ppp,使之成为 ppp 的新的左孩子;
(3)更新 vvv 的父节点为 ggg、ggg 的子节点为 vvv。
经过上述操作后,情况(3)即变为了与情况(4)等价的情况。
由上可见,AVL 树的插入操作最多只需两次旋转操作即可使整棵树恢复平衡。
3. 删除
从 AVL 树中删除节点可能会降低树的高度,从而引发失衡情况。
(1)首先按照二叉搜索树的删除方式删除节点 xxx;
(2)然后从 xxx 的父节点开始,沿着父指针逆行而上,直至找到第一个失衡的祖先节点(最低的失衡祖先),将其记为 ggg;
(3)找到以 ggg 为根的子树中最高的子树,将其根记为 ppp;
(4)找到以 ppp 为根的子树中最高的子树,将其根记为 vvv;如果 ppp 的左右子树等高,且 ppp 是 ggg 的左孩子,则令 vvv 为 ppp 的左孩子,否则令 vvv 为 ppp 的右孩子;可见节点 xxx 一定不位于子树 ppp、vvv 中;
与插入操作类似,根据 g,p,vg,p,vg,p,v 的方向,可以分为四种情况:
(1)ppp 是 ggg 的左孩子,且 vvv 是 ppp 的左孩子;
(2)ppp 是 ggg 的左孩子,且 vvv 是 ppp 的右孩子;
(3)ppp 是 ggg 的右孩子,且 vvv 是 ppp 的左孩子;
(4)ppp 是 ggg 的右孩子,且 vvv 是 ppp 的右孩子;
其中,(1)和(4)是对称的情况,可以通过单旋操作来实现重平衡;(2)和(3)是对称的情况,可以通过双旋操作来实现重平衡。
情况(1)的重平衡操作如下所示:
虚线连接的两个灰色方框不能同时为空,没有虚线连接的灰色方框可为空或非空。
以 ggg 为轴进行 zig 旋转(设 ggg 的父节点为 pgpgpg):
(1)将 ggg 顺时针往下拉,使之成为 ppp 的新的右孩子;
(2)将 ppp 的旧的右孩子过继给 ggg,使之成为 ggg 的新的左孩子;
(3)更新 ppp 的父节点为 pgpgpg、pgpgpg 的子节点为 ppp。
情况(2)的重平衡操作如下所示:
虚线连接的两个灰色方框不能同时为空。
以 ppp 为轴进行 zag 旋转:
(1)将 ppp 逆时针往下拉,使之成为 vvv 的新的左孩子;
(2)将 vvv 的旧的左孩子过继给 ppp,使之成为 ppp 的新的右孩子;
(3)更新 vvv 的父节点为 ggg、ggg 的子节点为 vvv。
经过上述操作后,情况(2)即变为了与情况(1)等价的情况。
与插入操作不同的是,删除操作的重平衡过程只能保证失衡的子树恢复平衡,但此重平衡操作可能降低子树的高度,从而导致失衡向上传播(如,子树 ggg 属于原先高度更小的一个分支,重平衡之后 ggg 所在的子树高度又降低了),即无法保证整棵树保持平衡。
为此,需要继续从 ggg 开始,沿着父指针逆行而上,判断祖先是否失衡,如果出现失衡情况,则继续使用上述操作将子树恢复平衡,否则即可成功返回。
由此可见,相比于插入操作而言,AVL 树的删除操作可能需要更多次数的旋转操作,最坏情况下需要多达 O(logn)O(\log n)O(logn) 次的旋转。
4. 统一重平衡算法
统一重平衡算法可以实现单旋和双旋操作。
其基本思想是:对重平衡之前的失衡子树进行中序遍历所得结果,和重平衡之后所得子树的中序遍历结果是一致的。
如,
(a)的中序遍历结果为:{T0,p,T1,v,T2,g,T3}\{ T_0, p, T_1, v, T_2, g, T_3 \}{T0,p,T1,v,T2,g,T3}
(b)的中序遍历结果为:{T0,p,T1,v,T2,g,T3}\{ T_0, p, T_1, v, T_2, g, T_3 \}{T0,p,T1,v,T2,g,T3}
统一重平衡操作涉及到了三个节点及对应的四棵子树,其不再进行旋转操作,而是直接进行子树的组装:{((T0,p,T1),v,(T2,g,T3))}\{ ((T_0, p, T_1), v, (T_2, g, T_3)) \}{((T0,p,T1),v,(T2,g,T3))},圆括号括住的内容表示一棵子树,中间为树根,两边分别为树根的左、右子树。其 Python 实现大致如下:
def connect34(a, b, c, T0, T1, T2, T3):
a.leftChild = T0
a.rightChild = T1
c.leftChild = T2
c.rightChild = T3
b.leftChild = a
b.rightChild = c
if T0:
T0.parent = a
if T1:
T1.parent = a
if T2:
T2.parent = c
if T3:
T3.parent = c
a.parent = b
c.parent = b
如,(a)到(c)的重平衡过程为:
connect34(p, v, g, p.leftChild, v.leftChild, v.rightChild, g.rightChild)