7.4 AVL树
在渐进意义下,AVL树可始终将其高度控制在O(logn)以内,从而保证每次查找、插入或删除操作,均可在O(logn)的时间内完成。
定义与性质
-
平衡因子
任一节点v的平衡因子(balance factor)定义为“其左、右子树的高度差”,即
balFac(v) = height(lc(v)) - height(rc(v))
所谓AVL树,即平衡因子受限的二叉搜索树——其中各节点平衡因子的绝对值均不超过1。
-
接口定义
基于BST定义的AVL树接口:
0001 #include "BST/BST.h" //基于BST实现AVL树 0002 template <typename T> class AVL : public BST<T> { //由BST派生AVL树模板类 0003 public: 0004 BinNodePosi<T> insert ( const T& e ); //插入(重写) 0005 bool remove ( const T& e ); //删除(重写) 0006 // BST::search()等其余接口可直接沿用 0007 };用于简化AVL树算法描述的宏:
0001 #define Balanced(x) ( stature( (x).lc ) == stature( (x).rc ) ) //理想平衡条件 0002 #define BalFac(x) ( stature( (x).lc ) - stature( (x).rc ) ) //平衡因子 0003 #define AvlBalanced(x) ( ( -2 < BalFac(x) ) && ( BalFac(x) < 2 ) ) //AVL平衡条件 -
平衡性
就渐进意义而言,AVL树的确是平衡的。
-
失衡与重平衡
因节点x的插入或删除而暂时失衡的节点,构成失衡节点集,记作UT(x)。请注意,若x为被摘除的节点,则UT(x)仅含单个节点;但若x为被引入的节点,则UT(x)可能包含多个节点。由以上实例,也可验证这一性质。
节点插入
-
失衡节点集
新引入节点x后,UT(x)中的节点都是x的祖先,且高度不低于x的祖父。以下,将其中的最深者记作g(x)。在x与g(x)之间的通路上,设p为g(x)的孩子,v为p的孩子。注意,既然g(x)不低于x的祖父,则p必是x的真祖先。
-
重平衡
首先,需要找到如上定义的g(x),可从x出发沿parent指针逐层上行并核对平衡因子,首次遇到的失衡祖先为g(x),这一过程只需O(logn)时间。
0001 /****************************************************************************************** 0002 * 在左、右孩子中取更高者 0003 * 在AVL平衡调整前,借此确定重构方案 0004 ******************************************************************************************/ 0005 #define tallerChild(x) ( \ 0006 stature( (x)->lc ) > stature( (x)->rc ) ? (x)->lc : ( /*左高*/ \ 0007 stature( (x)->lc ) < stature( (x)->rc ) ? (x)->rc : ( /*右高*/ \ 0008 IsLChild( * (x) ) ? (x)->lc : (x)->rc /*等高:与父亲x同侧者(zIg-zIg或zAg-zAg)优先*/ \ 0009 ) \ 0010 ) \ 0011 )这里并未显式地维护各节点的平衡因子,而是在需要时通过比较子树的高度直接计算。
以下,根据节点g(x)、p和v之间具体的联接方向,将采用不同的局部调整方案,分述如下:
-
单旋
v是p的右孩子,且p是g的右孩子:
该操作为zag(g(x))
不难验证,通过zig(g(x))可以处理对称的失衡。
-
双旋
节点v是p的左孩子,而p是g(x)的右孩子:
该操作为zig(p)和zag(g(x))
不难验证,通过zag(p)和zig(g(x))可以处理对称的情况。
-
高度复原
换而言之,在AVL树中插入新节点后,仅需不超过两次旋转,即可使整树恢复平衡。
-
实现
0001 template <typename T> BinNodePosi<T> AVL<T>::insert ( const T& e ) { //将关键码e插入AVL树中 0002 BinNodePosi<T> & x = search ( e ); if ( x ) return x; //确认目标节点不存在 0003 BinNodePosi<T> xx = x = new BinNode<T> ( e, _hot ); _size++; //创建新节点x 0004 // 此时,x的父亲_hot若增高,则其祖父有可能失衡 0005 for ( BinNodePosi<T> g = _hot; g; g = g->parent ) //从x之父出发向上,逐层检查各代祖先g 0006 if ( !AvlBalanced ( *g ) ) { //一旦发现g失衡,则(采用“3 + 4”算法)使之复衡,并将子树 0007 FromParentTo ( *g ) = rotateAt ( tallerChild ( tallerChild ( g ) ) ); //重新接入原树 0008 break; //局部子树复衡后,高度必然复原;其祖先亦必如此,故调整结束 0009 } else //否则(g仍平衡) 0010 updateHeight ( g ); //只需更新其高度(注意:即便g未失衡,高度亦可能增加) 0011 return xx; //返回新节点位置 0012 } //无论e是否存在于原树中,总有AVL::insert(e)->data == e -
效率
AVL树的节点插入操作可以在O(logn)时间内完成。
节点删除
-
失衡节点集
与插入操作十分不同,在摘除节点x后,以及随后的调整过程中,失衡节点集UT(x)始终至多只含一个节点。而且若该节点g(x)存在,其高度必与失衡前相同。
另外还有一点重要的差异是,g(x)有可能就是x的父亲。
-
重平衡
与插入操作同理,从_hot节点(7.2.6节)出发沿parent指针上行,经过O(logn)时间即 可确定g(x)位置。作为失衡节点的g(x),在不包含x的一侧,必有一个非空孩子p,且p的高度至少为1。于是,可按以下规则从p的两个孩子(其一可能为空)中选出节点v:若两个孩子不等高,则v取作其中的更高者;否则,优先取v与p同向者(亦即,v与p同为左孩子,或者同为右孩子)。
以下不妨假定失衡后g(x)的平衡因子为+2(为-2的情况完全对称)。根据祖孙三代节点g(x)、p和v的位置关系,通过以g(x)和p为轴的适当旋转,同样可以使得这一局部恢复平衡。
-
单旋
-
双旋
-
失衡传播
与插入操作不同,在删除节点之后,尽管也可通过单旋或双旋调整使局部子树恢复平衡,但就全局而言,依然可能再次失衡。
在摘除节点之后的调整过程中,这种由于低层失衡节点的重平衡而致使其高层祖先失衡的现象,称作“失衡传播”
-
实现
0001 template <typename T> bool AVL<T>::remove ( const T& e ) { //从AVL树中删除关键码e 0002 BinNodePosi<T> & x = search ( e ); if ( !x ) return false; //确认目标存在(留意_hot的设置) 0003 removeAt ( x, _hot ); _size--; //先按BST规则删除之(此后,原节点之父_hot及其祖先均可能失衡) 0004 for ( BinNodePosi<T> g = _hot; g; g = g->parent ) { //从_hot出发向上,逐层检查各代祖先g 0005 if ( !AvlBalanced ( *g ) ) //一旦发现g失衡,则(采用“3 + 4”算法)使之复衡,并将该子树联至 0006 g = FromParentTo ( *g ) = rotateAt ( tallerChild ( tallerChild ( g ) ) ); //原父亲 0007 updateHeight ( g ); //更新高度(注意:即便g未失衡或已恢复平衡,高度均可能降低) 0008 } //可能需做Omega(logn)次调整——无论是否做过调整,全树高度均可能降低 0009 return true; //删除成功 0010 } -
效率
综合各方面的消耗,AVL树的节点删除操作总体的时间复杂度依然是O(logn)。
统一重平衡算法 —— “3 + 4”重构
上述重平衡的方法,需要根据失衡节点及其孩子节点、孙子节点的相对位置关系,分别做单旋或双旋调整。按照这一思路直接实现调整算法,代码量大且流程繁杂,必然导致调试困难且容易出错。为此,本节将引入一种更为简明的统一处理方法。
无论对于插入或删除操作,新方法也同样需要从刚发生修改的位置x出发逆行而上,直至遇到最低的失衡节点g(x)。于是在g(x)更高一侧的子树内,其孩子节点p和孙子节点v必然存在,而且这一局部必然可以g(x)、p和v为界,分解为四棵子树——T0至T3
如下图所示,将这三个节点与四颗子树重新“组装”起来,恰好即是一颗AVL树!
代码:“3 + 4”重构:
0001 /******************************************************************************************
0002 * 按照“3 + 4”结构联接3个节点及其四棵子树,返回重组之后的局部子树根节点位置(即b)
0003 * 子树根节点与上层节点之间的双向联接,均须由上层调用者完成
0004 * 可用于AVL和RedBlack的局部平衡调整
0005 ******************************************************************************************/
0006 template <typename T> BinNodePosi<T> BST<T>::connect34 (
0007 BinNodePosi<T> a, BinNodePosi<T> b, BinNodePosi<T> c,
0008 BinNodePosi<T> T0, BinNodePosi<T> T1, BinNodePosi<T> T2, BinNodePosi<T> T3
0009 ) {
0010 a->lc = T0; if ( T0 ) T0->parent = a;
0011 a->rc = T1; if ( T1 ) T1->parent = a; updateHeight ( a );
0012 c->lc = T2; if ( T2 ) T2->parent = c;
0013 c->rc = T3; if ( T3 ) T3->parent = c; updateHeight ( c );
0014 b->lc = a; a->parent = b;
0015 b->rc = c; c->parent = b; updateHeight ( b );
0016 return b; //该子树新的根节点
0017 }
代码:AVL树的统一重平衡:
0001 /******************************************************************************************
0002 * BST节点旋转变换统一算法(3节点 + 4子树),返回调整之后局部子树根节点的位置
0003 * 注意:尽管子树根会正确指向上层节点(如果存在),但反向的联接须由上层函数完成
0004 ******************************************************************************************/
0005 template <typename T> BinNodePosi<T> BST<T>::rotateAt ( BinNodePosi<T> v ) { //v为非空孙辈节点
0006 BinNodePosi<T> p = v->parent; BinNodePosi<T> g = p->parent; //视v、p和g相对位置分四种情况
0007 if ( IsLChild ( *p ) ) /* zig */
0008 if ( IsLChild ( *v ) ) { /* zig-zig */
0009 p->parent = g->parent; //向上联接
0010 return connect34 ( v, p, g, v->lc, v->rc, p->rc, g->rc );
0011 } else { /* zig-zag */
0012 v->parent = g->parent; //向上联接
0013 return connect34 ( p, v, g, p->lc, v->lc, v->rc, g->rc );
0014 }
0015 else /* zag */
0016 if ( IsRChild ( *v ) ) { /* zag-zag */
0017 p->parent = g->parent; //向上联接
0018 return connect34 ( g, p, v, g->lc, p->lc, v->lc, v->rc );
0019 } else { /* zag-zig */
0020 v->parent = g->parent; //向上联接
0021 return connect34 ( g, v, p, g->lc, v->lc, v->rc, p->rc );
0022 }
0023 }
“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 14 天,点击查看活动详情”