[TOC]

AVL 树的介绍和模拟实现

1. AVL树的简介

AVL树也是二叉搜索树的一种,但是是经过优化的,AVL树的效率比一般的二叉搜索树高很多,但是略逊红黑树一点点。至于什么是二叉搜索树呢?我忘了写了,后期补上,嘻嘻。简单来说二叉搜索树看名字就知道是一颗树,是用来搜索的。它有自己的特殊规则,就是,每个节点都要满足 左子树 < 根 < 右子树。满足这个条件就可以说这颗树是一个二叉搜索树,当然如果反过来的话,也可以叫二叉搜索树。

二叉搜索树的查找、插入和删除效率取决于树高,时间复杂度可以写成 O(h)。当树比较平衡时,树高 h 约为 logN,操作效率就是 O(logN);但是二叉搜索树也是有缺陷的,就是在原始数据有序时容易退化成链表,树高变成 O(N),搜索效率也会变回 O(N)。

为了解决这个问题呢?AVL树和红黑树其实都是来解决这个问题的,只不过两个树的规则不同。顺带一提,AVL 是两位苏联科学家的名字,他们发明了 AVL 树,这比红黑树要早一些。话说回来,AVL树的规则就是在二叉搜索树的基础上,再加一个条件:任意节点的左右子树高度差绝对值最大为 1。如果左右高度相差大于 1 了,就要经过旋转来修改。

2. AVL树的模拟实现

1.平衡因子的引入

刚才提到,我们要如何来维护左右高度相差为 1 呢?这里我们引入了一个平衡因子。本文约定平衡因子为 右子树高度减去左子树高度,这样就可以更好地去维护左右子树的高度差。平衡因子也不是必须要使用的,也有的AVL树实现会直接维护节点高度;接下来描述的AVL树实现使用平衡因子。

当插入一个新节点的时候,不止影响它父亲的平衡因子,还会影响这条路径上祖先节点的平衡因子,需要分情况来调整。

2.插入新节点

一个AVL树,在插入一个新节点的时候,需要根据这个节点父亲的平衡因子来调整树的结构。

按照本文“右高减左高”的约定:节点插入在左边,平衡因子就 --;节点插入在右边,平衡因子就 ++

  1. 如果插入后父亲的平衡因子是 0,也就是说插入之前父亲的平衡因子是 1 或者 -1。说明这次插入让这个子树更平衡了,子树高度不再增加,也就不用继续向上判断和修改。
  2. 如果插入后父亲的平衡因子是 1 或 -1,也就是说插入之前父亲的平衡因子是 0。说明这个子树高度增加了,所以需要沿着这条路径继续向上调整祖先节点的平衡因子。
  3. 如果父亲节点平衡因子已经变成了 2 或 -2,那么就需要通过旋转来调整。

3. 旋转

之前我们提到过的,调整AVL树的办法就是旋转。旋转分为四种:右单旋、左单旋、左右双旋和右左双旋。下面平衡因子的结论主要针对插入新节点导致的失衡;删除场景的平衡因子调整会更复杂,不能直接套用。

右旋

考虑旋转的时候就体现出画图的重要性了,但是,单单画图还不够,这里画细节图的话会很头大,所以这里最好是画抽象图。什么意思呢?如下图:

image-20260320222931262

我们看到这个数,就是画的一个抽象图,把 子树 a , b , c 的高度抽象为 h 。 这样的话 10 这个节点的平衡因子就是 -1 , 左子树上再插入一个节点就要旋转。 假设插入再 a 这个子树上,那么 a 这个树的高度就变成 h + 1 ,如下图:

image-20260320223327247

这样的话, 10 节点的平衡因子就变成 -2 了,这个时候就需要调整了。但是,我们看到,这个 h 是可以取很多值的。

当 h = 1 的时候,这里其实 b 子树有一种情况, c 子树有一种情况 , a 子树(插入后)有 2 种情况;

这样看是不是还好,那么当 h = 2 呢?

image-20260321221700741

当 h = 2 的时候, a, b ,c 树就是大约这样的形状, b 子树有 3 种情况 , c 子树也有 3 种情况,所以在 a 的插入点就有 4 * 3 种情况 。所以,整个 就有 108 情况。

看起来是不是还好。

那么当 n = 3 呢?

image-20260321221924537

这就是 a b c 子树的大体情况就是这样。所以, b 子树 和 c 子树 就有 C(4 ,4) + C(4,3) + C(4,2) + C(4,1) 种情况。 a 子树也有很多种情况。 总之就是,这样如果考虑细节的话,要考虑的情况是列举不完的。所以,我们在这里画了抽象图,换句话说,这些情况的处理方式都是差不多的。

好了好了,我们话再说回来,右旋是怎么回事呢?

image-20260320223327247

我们看到,10 的平衡因子变为了 -2 ,所以,10 就是旋转点。 旋转本质上就是一种降高度嘛,

我们可以, 把 10 变成 5 的右子树, 把 5 的右子树变成 10 的左子树。

也就是变成这样:

image-20260321223431572

原谅我画的图丑爆了,但是我感觉还是听清晰的,嘻嘻。 我们可以看到这个树经过右旋以后,所有的节点的平衡因子都变成了 0 。

左旋

左旋的情况和右旋正好是反过来的。插入前是这样的,

image-20260322090813905

还是插入在 a 子树上,变成这样:

image-20260322090905809

然后左旋:把 10 变成 5 的左子树, 5 的左子树 变成 10 的右子树,

image-20260322091155817

可以看到, 5 变成了 根, 所有节点的平衡因子都变成了 0 。 这就是左旋。

左右双旋

之前提到了左旋和右旋,那么什么是左右双旋呢?其实就是插入的地方不同。仔细看一下刚才的两个情况,我们插入之后 , 还没有旋转的时候, 10 的平衡因子分别是 -2 和 2 , 5 这个节点的平衡因子分别是 -1 和 1;

换句话说, 右旋的情况,是左边高的左边高,因为 10 和 5 的平衡因子都是 负数 ,也就代表着左边高嘛。

左旋的情况是,是右边高的右边高, 因为 10 和 5 的平衡因子都是 正数 。

如果,我们改变一下插入的位置,不插入在 a 子树, 插入在 b 子树的话, 右旋的情况就变成了 左边高的右边高,对应平衡因子 10 是 -2 , 5 是 1 ,如图:

image-20260322092349553

因为要用到 5 的右子树的下一个节点,我就又具体画了一个,出来, 插入在 8 这个子树上, 5 的平衡因子就变成了 1 。如果是这样的话 ,单靠 右旋是解决不了问题的。可以话话试试看,还是以 10 为旋转点,右旋一次, 变成这样:

image-20260322094005402

可发现,如果只右旋一次的话,就变成这样,依然没有解决问题,说实话再转转也转不出来,有兴趣可以试试。

那么这时候怎么办呢? 就需要左右双旋了: 先以 5 为旋转点 左旋一次 , 然后在以 10 为旋转点,右旋一次 ,总共旋转两次。如图:

image-20260322094459350

看到左旋 5 一次后的结果,是不是就很清晰了,这是不是又变成了 左边高的左边高情况,再来一次右旋就好了,完成之后,就变成了这样,说实话这个就三个节点的看着有点抽象啊,我也是偷个懒,我只画的关键的几个节点。

image-20260322101100425

那么平衡因子呢?我们来画一个稍微聚体一些的:

image-20260322101542308

这是左旋一次后的具体的结果,然后再右旋,

image-20260322101703933

可以看到,左右双旋之后,整个树就变成了这样,也就是说,把 8 变成了整棵树的根,注意啊,这整个树也可以是子树。 来看平衡因子, 8 的平衡因子是 0 , 10 的平衡因子也是 0 , 5 的平衡因子是 -1 。

明白过程以后,就可以直接看看结论了,结论就是, 8 这个 节点变成 根, 8 的左子树给了 5 , 8 的右子树 给了 10; 8 的左子树高度是 h - 1, 所以 5 的平衡因子 是 -1 。 8 的右子树是 h , 所以 10 的平衡因子是 0; 8 最后转完了以后平衡因子也是 0 。

右左双旋

左右双旋看明白了,那么右左双旋也就完全没有问题了,反过来就可以。

当插入后,出现右边高的左边高的情况,如图:

image-20260322103320053

然后,先以 5 为旋转点右旋,再以 10 为 旋转点左旋,

这里我就直接用结论了,

image-20260322103642337

然后,可以看到 , 8 的 平衡因子是 0 , 10 的平衡因子是 0, 5 的平衡因子是 1。

4.AVL树的实现

AVL树 也是 一种 二叉搜索树 , 是一个三叉链的结构,再加一个平衡因子就好了,这里实现的是一个 Key Value 的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template<class K, class V>
struct AVLTreeNode
{
std::pair<K, V> _kv;

AVLTreeNode* _parent;
AVLTreeNode* _left;
AVLTreeNode* _right;

// 本文约定:右子树高度 - 左子树高度
int _bf;

AVLTreeNode(const std::pair<K, V>& kv)
: _kv(kv)
, _parent(nullptr)
, _left(nullptr)
, _right(nullptr)
, _bf(0)
{}
};

好了,这就是节点的状态了。下一个就开始写 AVL树了。因为确实太多了,我就自己实现了一下,然后放到码云里面了。

这里主要提一下,要注意的几个坑,

插入的时候,要注意链接节点的父亲,这个是在图上看不出来的,说实话。

然后就是空节点或者根结点的问题。别的暂时想不起来了。

3.AVL树的验证和删除概览

如何检查一个AVL是不是 AVL树呢?答案吗就是利用树高,递归左子树高度,然后递归右子树高度,然后检查一下就好了,整一个 diff 变量, 作为右子树和左子树的高度差,然后检查一下大小,然后检查一下当前节点的平衡因子。

至于 AVL树的删除,这里先不实现,只简单介绍一下。AVL 删除通常先按二叉搜索树的替代删除法完成删除,再从受影响节点向上调整平衡因子。

  1. 如果 parent 的平衡因子变成 0,说明子树高度变了,需要继续往上检查;这和插入时“平衡因子变成 0 就停止”的情况正好相反。
  2. 如果 parent 的平衡因子变成 1 或 -1,说明当前子树高度没有继续减少,可以结束向上调整。
  3. 删除导致失衡时还需要根据旋转前后节点的平衡因子,判断旋转后是否继续向上调整;这部分比插入复杂,后面单独整理完整代码。

本篇水水的也结束。