前言
Wiki:在计算机科学中,AVL树是最早被发明的自平衡二叉查找树。在AVL树中,任一节点对应的两棵子树的最大高度差为1,因此它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下的时间复杂度都是 O(logn)。增加和删除元素的操作则可能需要借由一次或多次树旋转,以实现树的重新平衡。AVL 树得名于它的发明者 G. M. Adelson-Velsky 和 Evgenii Landis,他们在1962年的论文《An algorithm for the organization of information》中公开了这一数据结构。
1 为什么要有平衡二叉树
二叉搜索树一定程度上可以提高搜索效率,但是当原序列有序时,例如序列 A = {1,2,3,4,5,6},构造二叉搜索树如图 1.1。依据此序列构造的二叉搜索树为右斜树,同时二叉树退化成单链表,搜索效率降低为 O(n)。
![91c5a720f9ae33dce49db7d14f7fbcab.png](https://i-blog.csdnimg.cn/blog_migrate/73d7b298a6b2047c7c2c75418fccd6a7.jpeg)
图 1.1
在此二叉搜索树中查找元素 6 需要查找 6 次。
二叉搜索树的查找效率取决于树的高度,因此保持树的高度最小,即可保证树的查找效率。同样的序列 A,将其改为图 1.2 的方式存储,查找元素 6 时只需比较 3 次,查找效率提升一倍。
![09fb28584aa96f6279f80ab03477f268.png](https://i-blog.csdnimg.cn/blog_migrate/492f31ac9195513649eaecf075b5a394.jpeg)
图 1.2
可以看出当节点数目一定,保持树的左右两端保持平衡,树的查找效率最高。
这种左右子树的高度相差不超过 1 的树为平衡二叉树。
2. 定义
平衡二叉查找树:简称平衡二叉树。由前苏联的数学家 Adelse-Velskil 和 Landis 在 1962 年提出的高度平衡的二叉树,根据科学家的英文名也称为 AVL 树。它具有如下几个性质:
- 可以是空树。
- 假如不是空树,任何一个节点的左子树与右子树都是平衡二叉树,并且高度之差的绝对值不超过 1。
平衡之意,如天平,即两边的分量大约相同。
例如图 2.1 不是平衡二叉树,因为节点 60 的左子树不是平衡二叉树。
![f3581539dcb2164092f45cfa922d5e2c.png](https://i-blog.csdnimg.cn/blog_migrate/1ab8f9dd99172cf90dc698d28e92517b.jpeg)
图 2.1
图 2.2 也不是平衡二叉树,因为虽然任何一个节点的左子树与右子树都是平衡二叉树,但高度之差已经超过 1 。
![5f5d2e73622d3c8b739ef4ccd5251ce2.png](https://i-blog.csdnimg.cn/blog_migrate/6b76844a2a5f3a2c0b3b35c2a7d6ea91.jpeg)
图 2.2
图 2.3 是平衡二叉树。
![022bb3ed2bc4ed14a4c8b1a6aa8b9803.png](https://i-blog.csdnimg.cn/blog_migrate/37bb37eaac4d576fe0530c8dbc26d2fc.jpeg)
图 2.3
3. 平衡因子
定义:某节点的左子树与右子树的高度(深度)差即为该节点的平衡因子(BF,Balance Factor),平衡二叉树中不存在平衡因子大于 1 的节点。在一棵平衡二叉树中,节点的平衡因子只能取 0 、1 或者 -1 ,分别对应着左右子树等高,左子树比较高,右子树比较高。
![93d46f7ffc028ecad1fb45728a84ebc1.png](https://i-blog.csdnimg.cn/blog_migrate/b8994b6244fdd983c3d50c3c8c4d1d20.jpeg)
图 3.1
![b06981c74715c3a14cbd9f90b735ea8e.png](https://i-blog.csdnimg.cn/blog_migrate/79cf4805eece740374d3de53ba685cfc.jpeg)
图 3.2
![a3745363bcc21491f5ce44e2908e61ae.png](https://i-blog.csdnimg.cn/blog_migrate/5d354a845d03d3d3c6d28f9cee154802.jpeg)
图 3.3
4. 节点结构
定义平衡二叉树的节点结构:
typedef struct AVLNode *Tree;typedef int ElementType;struct AVLNode{ int depth; //深度,这里计算每个节点的深度,通过深度的比较可得出是否平衡 Tree parent; //该节点的父节点 ElementType val; //节点值 Tree lchild; Tree rchild; AVLNode(int val=0) { parent = NULL; depth = 0; lchild = rchild = NULL; this->val=val; }};
5. AVL树插入时的失衡与调整
图 5.1 是一颗平衡二叉树
![6887e421f2a5d56bb3723e18b77243c6.png](https://i-blog.csdnimg.cn/blog_migrate/8f04e7c244dca9e1a3ff842d0f7c492f.jpeg)
图 5.1
在此平衡二叉树插入节点 99 ,树结构变为:
![76999fcd6af70fa6fff43fd6c1788bb3.gif](https://i-blog.csdnimg.cn/blog_migrate/81bf304d41fe947b60c024765b810c5e.gif)
动图 5.2
在动图 5.2 中,节点 66 的左子树高度为 1,右子树高度为 3,此时平衡因子为 -2,树失去平衡。
在动图 5.2 中,以节点 66 为父节点的那颗树就称为 最小失衡子树。
最小失衡子树:在新插入的节点向上查找,以第一个平衡因子的绝对值超过 1 的节点为根的子树称为最小不平衡子树。也就是说,一棵失衡的树,是有可能有多棵子树同时失衡的。而这个时候,我们只要调整最小的不平衡子树,就能够将不平衡的树调整为平衡的树。
平衡二叉树的失衡调整主要是通过旋转最小失衡子树来实现的。根据旋转的方向有两种处理方式,左旋 与 右旋 。
旋转的目的就是减少高度,通过降低整棵树的高度来平衡。哪边的树高,就把那边的树向上旋转。
5.1 左旋
![16fb35889eaf6f3e7b67cc80f6ddd68b.png](https://i-blog.csdnimg.cn/blog_migrate/c49b262c909c403c39140f3991cf33fe.jpeg)
图 5.1.1
以图 5.1.1 为例,加入新节点 99 后, 节点 66 的左子树高度为 1,右子树高度为 3,此时平衡因子为 -2。为保证树的平衡,此时需要对节点 66 做出旋转,因为右子树高度高于左子树,对节点进行左旋操作,流程如下:
(1)节点的右孩子替代此节点位置
(2)右孩子的左子树变为该节点的右子树
(3)节点本身变为右孩子的左子树
整个操作流程如动图 5.1.2 所示。
![cb0d17dee6a66b93be22de87ae79efe5.gif](https://i-blog.csdnimg.cn/blog_migrate/f98a090e506abd2fb600c8c9390d7030.gif)
动图 5.1.2
- 节点的右孩子替代此节点位置 —— 节点 66 的右孩子是节点 77 ,将节点 77 代替节点 66 的位置
- 右孩子的左子树变为该节点的右子树 —— 节点 77 的左子树为节点 75,将节点 75 挪到节点 66 的右子树位置
- 节点本身变为右孩子的左子树 —— 节点 66 变为了节点 77 的左子树
5.2 右旋
右旋操作与左旋类似,操作流程为:
(1)节点的左孩子代表此节点
(2)节点的左孩子的右子树变为节点的左子树
(3)将此节点作为左孩子节点的右子树。
![bbe2b8984e2eff393374f05c95c8c0b8.gif](https://i-blog.csdnimg.cn/blog_migrate/ce5cb69a213f8713956056dedc671108.gif)
动图 5.2.1
6. AVL树的四种插入节点方式
假设一颗 AVL 树的某个节点为 A,有四种操作会使 A 的左右子树高度差大于 1,从而破坏了原有 AVL 树的平衡性。平衡二叉树插入节点的情况分为以下四种:
![f0a002fd8c63a0dd26224bea708eb5ea.png](https://i-blog.csdnimg.cn/blog_migrate/4659be87f2a95938d6077bf8d6de5374.jpeg)
图 6.0
具体分析如下:
6.1 A的左孩子的左子树插入节点(LL)
只需要执行一次右旋即可。
![59709f59bc153564d40d8aaacd3c34a6.gif](https://i-blog.csdnimg.cn/blog_migrate/522ca13abc5111c2f5e5d44a46b56cfa.gif)
动图 6.1
实现代码如下:
//LL型调整函数//返回:新父节点Tree LL_rotate(Tree node){ //node为离操作节点最近的失衡的节点 Tree parent=NULL,son; //获取失衡节点的父节点 parent=node->parent; //获取失衡节点的左孩子 son=node->lchild; //设置son节点右孩子的父指针 if (son->rchild!=NULL) son->rchild->parent=node; //失衡节点的左孩子变更为son的右孩子 node->lchild=son->rchild; //更新失衡节点的高度信息 update_depth(node); //失衡节点变成son的右孩子 son->rchild=node; //设置son的父节点为原失衡节点的父节点 son->parent=parent; //如果失衡节点不是根节点,则开始更新父节点 if (parent!=NULL){ //如果父节点的左孩子是失衡节点,指向现在更新后的新孩子son if (parent->lchild==node){ parent->lchild=son; }else{ //父节点的右孩子是失衡节点 parent->rchild=son; } } //设置失衡节点的父亲 node->parent=son; //更新son节点的高度信息 update_depth(son); return son;}
6.2 A的右孩子的右子树插入节点(RR)
只需要执行一次左旋即可。
![1b09b170d816650ff5a48fe080a30502.gif](https://i-blog.csdnimg.cn/blog_migrate/cc3eb675812b0344e5105c75ec063935.gif)
动图 6.2
实现代码如下:
//RR型调整函数//返回新父节点Tree RR_rotate(Tree node){ //node为离操作节点最近的失衡的节点 Tree parent=NULL,son; //获取失衡节点的父节点 parent=node->parent; //获取失衡节点的右孩子 son=node->rchild; //设置son节点左孩子的父指针 if (son->lchild!=NULL){ son->lchild->parent=node; } //失衡节点的右孩子变更为son的左孩子 node->rchild=son->lchild; //更新失衡节点的高度信息 update_depth(node); //失衡节点变成son的左孩子 son->lchild=node; //设置son的父节点为原失衡节点的父节点 son->parent=parent; //如果失衡节点不是根节点,则开始更新父节点 if (parent!=NULL){ //如果父节点的左孩子是失衡节点,指向现在更新后的新孩子son if (parent->lchild==node){ parent->lchild=son; }else{ //父节点的右孩子是失衡节点 parent->rchild=son; } } //设置失衡节点的父亲 node->parent=son; //更新son节点的高度信息 update_depth(son); return son;}
6.3 A的左孩子的右子树插入节点(LR)
若 A 的左孩子节点 B 的右子树 E 插入节点 F ,导致节点 A 失衡,如图:
![8bfb06ff537d1832e3a1dac9f537a6ea.png](https://i-blog.csdnimg.cn/blog_migrate/d06f9b0bac78e08bc55e8e213f824efd.jpeg)
图 6.3
A 的平衡因子为 2 ,若仍按照右旋调整,则变化后的图形为这样:
![e3d64842157bfac6ebde3bc119ac4dc4.png](https://i-blog.csdnimg.cn/blog_migrate/27126f4f2c11cbc38755ca33ccd6a37e.jpeg)
图 6.3.1
经过右旋调整发现,调整后树仍然失衡,说明这种情况单纯的进行右旋操作不能使树重新平衡。那么这种插入方式需要执行两步操作,使得旋转之后为 原来根节点的左孩子的右孩子作为新的根节点。
(1)对失衡节点 A 的左孩子 B 进行左旋操作,即上述 RR 情形操作。
(2)对失衡节点 A 做右旋操作,即上述 LL 情形操作。
调整过程如下:
![b75a9767828c5a368dfead20ccfea43f.png](https://i-blog.csdnimg.cn/blog_migrate/3b0e340c7a256f2e966d3f382c45f122.jpeg)
图 6.3.2
![13a4a2eb6a6da96f534f051d8783f0fa.png](https://i-blog.csdnimg.cn/blog_migrate/3af464814965e9129f0e4db711004ba4.jpeg)
图 6.3.3
也就是说,经过这两步操作,使得 原来根节点的左孩子的右孩子 E 节点成为了新的根节点。
代码实现:
//LR型,先左旋转,再右旋转//返回:新父节点Tree LR_rotate(Tree node){ RR_rotate(node->lchild); return LL_rotate(node);}
6.4 A的右孩子的左子树插入节点(RL)
右孩子插入左节点的过程与左孩子插入右节点过程类似,也是需要执行两步操作,使得旋转之后为 原来根节点的右孩子的左孩子作为新的根节点。
(1)对失衡节点 A 的右孩子 C 进行右旋操作,即上述 LL 情形操作。
(2)对失衡节点 A 做左旋操作,即上述 RR 情形操作。
![1b3c4d94e55da1c1085412a858dde193.png](https://i-blog.csdnimg.cn/blog_migrate/dec4fa544b7cdf3bb2fc55cfea0e015b.jpeg)
图 6.4
![53bc28f340f92793d7b322cb83fcf641.png](https://i-blog.csdnimg.cn/blog_migrate/5ea9eabcfbd1dab56f9548d3a78e73a0.jpeg)
图 6.4.1
![8864f768741767ad93f9e3b00abb2eb4.png](https://i-blog.csdnimg.cn/blog_migrate/8595033c4044119f289c602d85646b52.jpeg)
图 6.4.2
也就是说,经过这两步操作,使得 原来根节点的右孩子的左孩子 D 节点成为了新的根节点。
代码实现:
//RL型,先右旋转,再左旋转//返回:新父节点Tree RL_rotate(Tree node){ LL_rotate(node->rchild); return RR_rotate(node);}
补充:
上述四种插入方式的代码实现的辅助代码如下:
//更新当前深度void update_depth(Tree node){ if (node==NULL){ return; }else{ int depth_Lchild=get_balance(node->lchild); //左孩子深度 int depth_Rchild=get_balance(node->rchild); //右孩子深度 node->depth=max(depth_Lchild,depth_Rchild)+1; }}//获取当前节点的深度int get_balance(Tree node){ if (node==NULL){ return 0; } return node->depth;}//返回当前平衡因子int is_balance(Tree node){ if (node==NULL){ return 0; }else{ return get_balance(node->lchild)-get_balance(node->rchild); }}
6.5 小总结
- 在所有的不平衡情况中,都是按照先 寻找最小不平衡树,然后 寻找所属的不平衡类别,再 根据 4 种类别进行固定化程序的操作。
- LL , LR ,RR ,RL其实已经为我们提供了最后哪个节点作为新的根指明了方向。如 LR 型最后的根节点为原来的根的左孩子的右孩子,RL 型最后的根节点为原来的根的右孩子的左孩子。只要记住这四种情况,可以很快地推导出所有的情况。
- 维护平衡二叉树,最麻烦的地方在于平衡因子的维护。建议读者们根据小吴提供的图片和动图,自己动手画一遍,这样可以更加感性的理解操作。
7. AVL树的四种删除节点方式
AVL 树和二叉查找树的删除操作情况一致,都分为四种情况:
(1)删除叶子节点
(2)删除的节点只有左子树
(3)删除的节点只有右子树
(4)删除的节点既有左子树又有右子树
只不过 AVL 树在删除节点后需要重新检查平衡性并修正,同时,删除操作与插入操作后的平衡修正区别在于,插入操作后只需要对插入栈中的弹出的第一个非平衡节点进行修正,而删除操作需要修正栈中的所有非平衡节点。
删除操作的大致步骤如下:
- 以前三种情况为基础尝试删除节点,并将访问节点入栈。
- 如果尝试删除成功,则依次检查栈顶节点的平衡状态,遇到非平衡节点,即进行旋转平衡,直到栈空。
- 如果尝试删除失败,证明是第四种情况。这时先找到被删除节点的右子树最小节点并删除它,将访问节点继续入栈。
- 再依次检查栈顶节点的平衡状态和修正直到栈空。
对于删除操作造成的非平衡状态的修正,可以这样理解:对左或者右子树的删除操作相当于对右或者左子树的插入操作,然后再对应上插入的四种情况选择相应的旋转就好了。
7.1 删除叶子节点
处理步骤:
①、将该节点直接从树中删除;
②、其父节点的子树高度的变化将导致父节点平衡因子的变化,通过向上检索并推算其父节点是否失衡;
③、如果其父节点未失衡,则继续向上检索推算其父节点的父节点是否失衡…如此反复②的判断,直到根节点 ;如果向上推算过程中发现了失衡的现象,则进行 ④ 的处理;
④、如果其父节点失衡,则判断是哪种失衡类型 [LL、LR、RR、RL] ,并对其进行相应的平衡化处理。如果平衡化处理结束后,发现与原来以父节点为根节点的树的高度发生变化,则继续进行 ② 的检索推算;如果与原来以父节点为根节点的高度一致时,则可说明父节点的父节点及祖先节点的平衡因子将不会有变化,因此可以退出处理;
![c3298a7bf116eba71e4a677445333c62.gif](https://i-blog.csdnimg.cn/blog_migrate/fe45016c185c47afd5b8dee51bf40c89.gif)
动图 7.1.1
具体数字演示:
![b888571d689bf07ad0a792880fa9f594.gif](https://i-blog.csdnimg.cn/blog_migrate/d6c295eaaf4c242ba479b333ffd8bc1e.gif)
动图 7.1
7.2 & 7.3 删除的节点只有左子树或右子树
处理步骤:
①、将左子树(右子树)替代原有节点 C 的位置;
②、节点 C 被删除后,则以 C 的父节点 B 为起始推算点,依此向上检索推算各节点(父、祖先)是否失衡;
③、如果其父节点未失衡,则继续向上检索推算其父节点 的父节点 是否失衡…如此反复 ② 的判断,直到根节点 ;如果向上推算过程中发现了失衡的现象,则进行 ④ 的处理;
④、如果其父节点失衡,则判断是哪种失衡类型 [LL、LR、RR、RL] ,并对其进行相应的平衡化处理。如果平衡化处理结束后,发现与原来以父节点为根节点的树的高度发生变化,则继续进行 ② 的检索推算;如果与原来以父节点为根节点的高度一致时,则可说明父节点的父节点及祖先节点的平衡因子将不会有变化,因此可以退出处理;
![86cf5d13feef084fee73c0a6a8883808.gif](https://i-blog.csdnimg.cn/blog_migrate/aff8173e436274f84c648266b5b52ca2.gif)
动图 7.2
7.4 删除的节点既有左子树又有右子树
处理步骤:
①、找到被删节点 B 和替代节点 BLR (节点 B 的前继节点或后继节点 —— 在此选择 前继);
②、将替代节点 BLR 的值赋给节点 B ,再把替代节点 BLR 的左孩子 BLRL 替换替代节点 BLR 的位置;
③、以 BLR 的父节点 BL 为起始推算点,依此向上检索推算父节点或祖先节点是否失衡;
④、如果其父节点未失衡,则继续向上检索推算其父节点的父节点是否失衡…如此反复③的判断,直到根节点;如果向上推算过程中发现了失衡的现象,则进行⑤的处理;
⑤、如果其父节点失衡,则判断是哪种失衡类型 [LL、LR、RR、RL] ,并对其进行相应的平衡化处理。如果平衡化处理结束后,发现与原来以父节点为根节点的树的高度发生变化,则继续进行 ② 的检索推算;如果与原来以父节点为根节点的高度一致时,则可说明父节点的父节点及祖先节点的平衡因子将不会有变化,因此可以退出处理;
![e8869f62819d7f43d727141abbf6d431.gif](https://i-blog.csdnimg.cn/blog_migrate/2d88872bf5e1c6edda4d1e5264150d68.gif)
动图 7.4
注:在这里,小吴并没有给出 AVL 的删除操作的代码,也没有给出平衡性修复的动画,因为我并不打算过多去讨论它,更复杂的删除操作过程将放在后续的 红黑树 中进行讨论。
总结
通过对 AVL 的插入操作和删除操作可以看出,平衡二叉树的优势在于不会出现普通二叉查找树的最差情况,即退化成链表结构,但为了保证高度平衡(对称),动态插入和删除的代价也随之增加。
AVL 的旋转问题看似复杂,但实际上如果你亲自用笔纸操作一下还是很好理解的。
/ 今日问题 /
已知我有 132 个好友,其中有 5 人关注了咪蒙。你可以通过 (批量)删除/添加好友并观察咪蒙好友关注数来推测谁关注了咪蒙。假设反复删除添加好友不会被揍,找到这 5 个好友需要进行多少次操作?