引言:
在计算机科学中,AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为一,所以它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下都是O(log n)。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。AVL树得名于它的发明者 G.M. Adelson-Velsky 和 E.M. Landis,他们在 1962 年的论文 "An algorithm for the organization of information" 中发表了它。
理解:
AVL树其实就是普通二叉排序树的升级版,只是增加了一个维护操作,使得这颗二叉树相对更平衡,也就是保证其每个节点的左右子树高度差小于等于1(使其不至于在有序数据下退化成一条链),这样达到每次操作都能在O(logn)的时间内完成。
所以AVL树其实是很好理解的,但写起来的确麻烦,不过写熟了也就没什么了
关于AVL树的一些操作解析:
- 维护(旋转)
- 插入
- 删除
- 查询rank
- 查询前驱
- 查询后继
- 以及一些set能实现的功能
Part 1:维护(旋转)
维护是AVl树的关键操作,所以在这里先讲,在后面的讲解中就直接用了。
引言:
维护就是在插入和删除操作后,由于改变了树的高度,使得这颗树可能变得不平衡了,所以就需要改变不平衡的子树的结构来使这棵树重新达到平衡的操作。
我们先定义一个节点的高度为:该节点到以其为根的子树的叶子节点的最大深度
至于具体怎么操作呢?我们举个列子来看:
首先,使树不平衡的节点可以具体分为4类:
(这里先讲在插入后的维护,删除的将在后面的删除板块讲解)
1. 变动节点在根节点的左子树的左子树上;(后面简称为:左左)
2. 变动节点在根节点的左子树的右子树上;(后面简称为:左右)
3. 变动节点在根节点的右子树的右子树上;(后面简称为:右右)
4. 变动节点在根节点的右子树的左子树上;(后面简称为:右左)
注:此处所说的根节点均指由叶子节点往上所不平衡的起一个节点
对于每一个情况都有不同的操作也对应分为4类:
- 右旋
- 左旋+右旋
- 左旋
- 右旋+左旋
先从最简单,也是最基础的右旋和左旋讲起(也就是分别解决“左左”和“右右”的方法)
Case1:左左(右旋)
由于是插入在左子树的左儿子上,所以可以知道,一定是左儿子的高度大于了右儿子的高度,所引起的不平衡。这时我们就要降低左儿子的的高度,提升右儿子的高度,来使其重新达到平衡。
因为在二叉排序树上,一个节点的左儿子比该节点小,而其右儿子则比它大;所以我们可以通过改变根节点,即让当前根节点的左儿子来做根节点,而当前根节点则变为其左儿子的右儿子的方法来使其重新达到平衡。
Code:
inline void pushup(int u){T[u].h = max(T[T[u].lson].h, T[T[u].rson].h) + 1;}
inline int Right(int u){ //(右旋)
int tmp = T[u].lson;
T[u].lson = T[tmp].rson;
T[tmp].rson = u;
pushup(u);
pushup(tmp);
return tmp;
}
Case2:右右(左旋)
同上:让当前根节点的右儿子来做根节点,而当前根节点则做其右儿子的左儿子。
Code:
inline void pushup(int u){T[u].h = max(T[T[u].lson].h, T[T[u].rson].h) + 1;}
inline int Left(int u){
int tmp = T[u].rson;
T[u].rson = T[tmp].lson;
T[tmp].lson = u;
pushup(u);
pushup(tmp);
return tmp;
}
左旋和右旋看完了,就该来看一下“左,右旋”组合的了
其实,组合旋转来处理“左右”和“右左”问题的原理就是把,在内部的节点先转到外面去从而转化成“左左”和“右右”的问题来解决的。
左右:
右左:
Code:
左右
inline int Left_Right(int u){
T[u].lson = Left(T[u].lson);
return Right(u);
}
右左
inline int Right_Left(int u){
T[u].rson = Right(T[u].rson);
return Left(u);
}
综合的维护操作:
inline void maintain(int &u){
pushup(u);
if(T[T[u].lson].h == T[T[u].rson].h+2){
int tmp = T[u].lson;
if(T[T[tmp].lson].h == T[T[u].rson].h+1) u = Right(u);
else if(T[T[tmp].rson].h == T[T[u].rson].h+1) u = Left_Right(u);
}
else if(T[T[u].lson].h+2 == T[T[u].rson].h){
int tmp = T[u].rson;
if(T[T[tmp].rson].h == T[T[u].lson].h+1) u = Left(u);
else if(T[T[tmp].lson].h == T[T[u].lson].h+1) u = Right_Left(u);
}
pushup(u);
}
插入:
在进行插入操作时,我们首先需要为自己所维护的节点信息设定一个优先级,这样在插入时,将插入数据与当前节点数据进行比较,从而判断是向该节点的左儿子插入还是向右儿子插入。像这样反复比较直至到达一个空节点,我们就将插入信息赋给这个空节点,然后返回,并进行维护。
值得提一下的是,虽然我们普遍规定:左儿子小于当前节点,右儿子大于当前节点。但这并不意味着无论什么样的题都是这样一尘不变,有时,根据题目要求,适当改变也会有出乎意料的结果。
int Insert(int u, int v){
if(u) tp = min(tp, ABS(T[u].v - v));
if(!u){
cnt ++;
T[cnt].v = T[cnt].sum = v;
T[cnt].h = 1;
return cnt;
}
if(v < T[u].v) T[u].lson = Insert(T[u].lson, v);
else if(v > T[u].v) T[u].rson = Insert(T[u].rson, v);
maintain(u);
return u;
}
删除:
在进行删除操作时,我们可以简单地处理需要删除节点没有儿子或有一个儿子的情况:这时候就相当于把他直接删去,或删去他并把他的儿子直接与他的父亲相连。我们之所以可以这样简单地进行操作是因为,这样操作不会使得树的结构发生改变即大小关系混乱的情况。
但如果需要删除节点有两个儿子,我们就不能这样办了,因为不论你将哪个儿子直接与被删结点的父亲相连,都会面临另以一个儿子为根的子树不论接在哪里都会使大小关系发生混乱,如要维护,就只要将这颗子树上的点全部拆开一个个的从新插入。这样无疑带来了我们所无法接受的高昂时间复杂度。
所以我们在删除有两个儿子的节点时,普遍采取下列操作:
- 首先,找到该节点左子树中最右边的节点即小于当前节点的最大值
- 其次,我们用这个节点来替代删除节点
- 最后,我们递归删除这个找到的节点
注:因为其是左右边的节点,故:其一定没有右儿子,那么这个节点就满足了第一类简单删除的要求,这样就完成了删除操作
inline int Find_maxu(int u){
while(T[u].rson) u = T[u].rson;
return T[u].v;
}
int del(int u, int v){
if(T[u].v == v){
if(!T[u].lson || !T[u].rson) return T[u].lson + T[u].rson;
else{
int tmp = Find_maxu(T[u].lson);
T[u].v = tmp;
T[u].lson = del(T[u].lson, tmp);
maintain(u);
return u;
}
}
if(v < T[u].v) T[u].lson = del(T[u].lson, v);
else T[u].rson = del(T[u].rson, v);
maintain(u);
return u;
}
实际应用:
其实掌握了AVL的旋转操作,并形成了自己的模板,我们在实际应用中就不难发现:对于不同的题,我们要思考的就是如何进行AVL树上的节点信息的维护与需要维护什么信息。
所以这样看来,对于普遍的AVL的应用就是考验OIer在数据信息简洁,快速维护上的功夫。
笼统的来说,记住模板了,对于不同的题我们所需要更改的东西就是需要维护的不同信息了。
在这里我就不举例题了,有兴趣的人可以去各大OJ上找点简单的模板题刷一下。
最后提供一个简单的模板(维护的是在动态删除中的和,也有前驱和后继):
struct AVL_Tree{
int rt, cnt;
struct node{
int v, sum;
int h, lson, rson;
}T[MAXN << 1];
AVL_Tree(){}
inline void pushup(int u){
T[u].h = max(T[T[u].lson].h, T[T[u].rson].h) + 1;
T[u].sum = T[T[u].lson].sum + T[T[u].rson].sum + T[u].v;
}
inline int Right(int u){
int tmp = T[u].lson;
T[u].lson = T[tmp].rson;
T[tmp].rson = u;
pushup(u);
pushup(tmp);
return tmp;
}
inline int Left(int u){
int tmp = T[u].rson;
T[u].rson = T[tmp].lson;
T[tmp].lson = u;
pushup(u);
pushup(tmp);
return tmp;
}
inline int Left_Right(int u){
T[u].lson = Left(T[u].lson);
return Right(u);
}
inline int Right_Left(int u){
T[u].rson = Right(T[u].rson);
return Left(u);
}
inline void maintain(int &u){
pushup(u);
if(T[T[u].lson].h == T[T[u].rson].h + 2){
int tmp = T[u].lson;
if(T[T[tmp].lson].h == T[T[u].rson].h + 1) u = Right(u);
else if(T[T[tmp].rson].h == T[T[u].rson].h + 1) u = Left_Right(u);
}
else if(T[T[u].lson].h + 2 == T[T[u].rson].h){
int tmp = T[u].rson;
if(T[T[tmp].rson].h == T[T[u].lson].h + 1) u = Left(u);
else if(T[T[tmp].lson].h == T[T[u].lson].h + 1) u = Right_Left(u);
}
pushup(u);
}
int Insert(int u, int v){
if(!u){
cnt ++;
T[cnt].v = T[cnt].sum = v;
T[cnt].h = 1;
return cnt;
}
if(v < T[u].v) T[u].lson = Insert(T[u].lson, v);
else if(v > T[u].v) T[u].rson = Insert(T[u].rson, v);
maintain(u);
return u;
}
void INSERT(int v){rt = Insert(rt,v);}
inline int Find_maxu(int u){
while(T[u].rson)
u = T[u].rson;
return T[u].v;
}
int del(int u, int v){
if(T[u].v == v){
if(!T[u].lson || !T[u].rson) return T[u].lson + T[u].rson;
else{
int tmp = Find_maxu(T[u].lson);
T[u].v = tmp;
T[u].lson = del(T[u].lson, tmp);
maintain(u);
return u;
}
}
if(v < T[u].v) T[u].lson = del(T[u].lson, v);
else T[u].rson = del(T[u].rson, v);
maintain(u);
return u;
}
void DELETE(int v){rt = del(rt, v);}
int LOWER(int v){
int u = rt, lower = -1;
while(u){
if(v <= T[u].v) u = T[u].lson;
else lower = T[u].v, u = T[u].rson;
}
return lower;
}
int UPPER(int v){
int u = rt, upper = -1;
while(u){
if(v < T[u].v) upper = T[u].v, u = T[u].lson;
else u = T[u].rson;
}
return upper;
}
void CLEAR(){
memset(T, 0, sizeof T);
rt = cnt = 0;
}
};