1.AVL树介绍
二叉搜索树在某些极端情况下可能会退化,为了解决这个问题,引入了AVL树(平衡搜索二叉树中的一种)控制二叉搜索树的不平衡情况,插入一个新节点后,控制每一个节点的左右子树高度差的绝对值不超过1
之所以控制绝对值不超过1而不是不超过0是因为只有在满二叉树的情况下才可以满足每一个节点的左右子树高度差的绝对值不超过0,为了满足普遍性,选择绝对值不超过1
一棵AVL树具有以下的特点:
-
每一个节点的左右子树都是AVL树
-
左右子树高度差的绝对值(通过平衡因子判断)不超过1
绝对值不超过1,包括-1、1和0
以右子树高度减左子树高度为例,下面是含有每一个节点的高度的AVL树示意图
2.AVL树平衡因子更新分析
在AVL树中,平衡因子更新只有两种情况:1. 增加 2. 减少,但是需要考虑什么时候增加什么时候减少
-
当插入节点在左子树时,平衡因子减少,例如插入在8节点的左子树
2.当插入节点在右子树时,平衡因子增加,例如插入在8的右子树
接着考虑插入节点后,哪些平衡因子需要改变
1.如果插入节点在8的左子树,那么更新时只会更新8节点的平衡因子,因为8初始时是1,插入在左子树平衡因子减小,而对于节点7来说,并没有影响到其平衡因子,因为7的右子树总体的高度没有发生变化,而对于根节点的左子树以及7节点的左子树来说没有一点影响
2.如果按照下图的插入方式,插入节点为红色节点,此时会更新其所有祖先节点
3. 如果插入节点在8的右子树,那么更新时8节点的平衡因子变为了2,因为8初始时是1,插入在右子树平衡因子增加,因为AVL树规定每一个节点的左右子树高度差的绝对值不能超过1,所以此时8节点需要进行额外处理使其恢复AVL树结构
综上所述,存在三种情况:
-
当插入节点的父亲节点更新为0时:插入节点前父亲节点平衡因子为1或者-1(一边高一边矮),为1时,右子树高,插入位置为左子树;为-1时,左子树高,插入位置为右子树(插入在矮的一边),此时只需要更新父亲节点的平衡因子,因为总高度并没有发生变化。例如8节点的左子树插入节点,树的总高度还是4
2.当插入节点的父亲节点更新为1或者-1时:插入节点前父亲节点平衡因子为0(左右子树高度相等),当更新为1时,插入位置为右子树,右子树变高,起始为-1时,插入位置为左子树,左子树变高(其中一方变高),此时需要更新所有祖先节点的平衡因子,因为插入的节点导致了树的总高度发生变化,例如例2插入红色节点树的总高度由4变为5
3.当插入节点的父亲节点更新为2或者-2时:插入节点前父亲节点平衡因子为1或者-1(左右子树存在一方高一方矮),当更新为2时,原父亲节点平衡因子为1,并且插入位置在平衡因子为1的父亲节点的右子树,例如在节点8右子树插入新节点;当更新为-2时,原父亲节点平衡因子为-1,插入位置平衡因子为-1的父亲节点的右子树(高的一方继续变高,矮的一方没有改变),此时需要进行旋转处理将平衡因子为2或者-2的节点对应的树进行调整使其恢复AVL树结构
3.AVL树插入时旋转与平衡因子更新
左单旋
当插入的节点在平衡因子为1的父亲节点的右子树的右子树上时,此时父亲节点的平衡因子更新为2,其右孩子节点的平衡因子分别为1和0时需要进行左单旋,例如下面的一种具体情况:
为了便于分析,将插入位置进行抽象化,a、b和c分别是高度h>=0
的AVL子树,如下图所示
此时因为父亲节点10的平衡因子变为了2,需要进行左旋,左旋需要进行的步骤如下:
平衡因子计算:
h+1(新增节点所在层数)-1=1为20节点的平衡因子
h+1+1(20节点所在层数)-h=2为10节点的平衡因子
1.将20的左孩子给10节点作为其右孩子
2.将10降下作为20的左孩子
-
20节点作为本棵子树的根节点
-
更新10和20节点的平衡因子为0
结合变量parent
、subR
和subRL
:
// 执行左旋操作,通常用于平衡AVL树
void RotateL(Node* parent)
{
// 右子节点
Node* subR = parent->_right;
// 右子节点的左子节点
Node* subRL = subR->_left;
// 将 parent 的右子节点更新为 subR 的左子节点
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
// 保存 parent 的父节点
Node* parentParent = parent->_parent;
// 将 subR 的左子节点更新为 parent
subR->_left = parent;
parent->_parent = subR;
// 如果 parent 是根节点,更新根节点为 subR
if (parentParent == nullptr)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
// 更新 parentParent 的左或右子节点为 subR
if (parent == parentParent->_left)
{
parentParent->_left = subR;
}
else
{
parentParent->_right = subR;
}
subR->_parent = parentParent;
}
// 更新节点的平衡因子(假设平衡因子 bf 的值设置为 0)
parent->_bf = subR->_bf = 0;
}
右单旋
当插入的节点在平衡因子为-1的父亲节点的左子树的左子树上时,此时父亲节点的平衡因子更新为-2,其左孩子节点的平衡因子分别为-1和0时需要进行右单旋,例如下面的一种具体情况:
为了便于分析,将插入位置进行抽象化,a、b和c分别是高度h>=0
的AVL子树,如下图所示:
此时因为父亲节点10的平衡因子变为了-2,需要进行右旋,右旋需要进行的步骤如下:
平衡因子计算:
h-(h+1(新增节点所在层数))=-1为20节点的平衡因子
h-(h+1+1(20节点所在层数))=-2为10节点的平衡因子
1.将5的右孩子作为10的左孩子
将10作为5的右孩子
-
将20作为本棵子树的根节点
-
更新10和20的平衡因子为0
结合变量parent
、subL
和subLR
:
// 执行右旋操作,通常用于平衡AVL树
void RotateR(Node* parent)
{
// 左子节点
Node* subL = parent->_left;
// 左子节点的右子节点
Node* subLR = subL->_right;
// 将 parent 的左子节点更新为 subL 的右子节点
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
// 保存 parent 的父节点
Node* parentParent = parent->_parent;
// 将 subL 的右子节点更新为 parent
subL->_right = parent;
parent->_parent = subL;
// 如果 parent 是根节点,更新根节点为 subL
if (parentParent == nullptr)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
// 更新 parentParent 的左或右子节点为 subL
if (parent == parentParent->_left)
{
parentParent->_left = subL;
}
else
{
parentParent->_right = subL;
}
subL->_parent = parentParent;
}
subL->_bf = parent->_bf = 0;
}
左右单旋
当插入的节点在平衡因子为-1的父亲节点的左子树的右子树上时,此时父亲节点的平衡因子更新为-2,其右孩子节点的平衡因子分别为1和0时需要先进行左单旋再进行右单旋,例如下面的一种具体情况:
为了便于分析,将插入位置进行抽象化
a、b和c分别是高度h=0
的AVL子树,如下图所示
在当前情况下,b位置的节点即为新增节点,a和c位置此时均没有节点,10节点的平衡因子更新为-2,10节点的左孩子节点的平衡因子更新为1
平衡因子计算:
5节点的平衡因子:h+1(新增节点所在层数)-h=1
10节点的平衡因子:h-(h+1+1(5节点所在层数))=-2
a、b、c和d分别是高度h>0
的AVL子树,如下图所示:
h>0
在上图的情况下代表已经至少存在一个节点,即5的右子树开始有一个节点存在,所以6位置没有节点时用h-1
代替
此时插入节点的位置有两种:
(a) 在b位置插入,此时6节点的平衡因子变为-1,5节点的平衡因子变为1,10节点的平衡因子变为-2
平衡因子的计算:
6节点的平衡因子:h-1-h(新增节点所在层数)=-1
5节点的平衡因子:h+1(6节点所在层数)-h=1
10节点的平衡因子:h-(h+1+1)=-2
(b) 在d位置插入,此时6节点的平衡因子变为-1,5节点的平衡因子变为1,10节点的平衡因子变为-2
平衡因子的计算:
6节点的平衡因子:h+1-h(新增节点所在层数)=1
5节点的平衡因子:h+1(6节点所在层数)-h=1
10节点的平衡因子:h-(h+1+1)=-2
尽管左右单旋有两种主要情况,但是实际上影响到的平衡因子的更新,主要思路还是先左旋再右旋,为了更好观察效果,以第二种情况中的第一种情况为例分析先左旋再右旋的步骤:
左旋(将多条路径更新化为一条路径更新)
1.将6节点的左孩子作为5节点的右孩子
2,将5节点作为6节点的左孩子
3.将6节点作为本棵树的根节点,链接到10节点的左孩子(因为开始的父亲节点5是10节点的左孩子)
2.右旋(更新单一路径)
1.将6节点的右孩子作为10节点的左孩子
2.将10节点作为6节点的右孩子
-
将6节点作为本棵树的根节点
-
更新平衡因子
当树旋转结束后需要更新对应的平衡因子,此时需要考虑到两种主要情况,即h=0
和h>0
,结合变量parent
、subL
和subLR
:
1.h=0
,左右双旋结束后如下图所示:
2.h>0
1.插入位置在b,左右双旋结束后如下图所示:
2.插入位置在d,左右双旋结束后如下图所示
代码实现(左右双旋)
// 执行左-右双旋操作,用于平衡AVL树
void rotateLR(node* parent)
{
// 记录旋转前的左子节点和左子节点的右子节点
node* subL = parent->_left;
node* subLR = subL->_right;
// 记录 subLR 的平衡因子
int bf = subLR->_bf;
// 1. 执行左-右双旋操作
// 先对左子节点执行左旋操作
rotateLeft(parent->_left);
// 然后对 parent 执行右旋操作
rotateRight(parent);
// 根据 subLR 的平衡因子更新旋转后的节点的平衡因子
if (bf == 0)
{
// 平衡因子为 0,旋转后各节点平衡因子也设置为 0
subL->_bf = parent->_bf = 0;
}
else if (bf == -1) // 插入在 subLR 的左子树
{
// 插入导致 subLR 的右子树变长,更新相应的平衡因子
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 1;
}
else if (bf == 1) // 插入在 subLR 的右子树
{
// 插入导致 subLR 的左子树变长,更新相应的平衡因子
subL->_bf = -1;
subLR->_bf = 0;
parent->_bf = 0;
}
else
{
// 不应出现的平衡因子值,触发断言
assert(false);
}
}
右左单旋(跟上述一样:代码实现)
/ 执行右-左双旋操作,用于平衡AVL树
void RotateRL(Node* parent)
{
// 右子节点
Node* subR = parent->_right;
// 右子节点的左子节点
Node* subRL = subR->_left;
// 获取 subRL 的平衡因子
int bf = subRL->_bf;
// 先对右子节点执行右旋操作
RotateR(parent->_right);
// 然后对 parent 执行左旋操作
RotateL(parent);
// 根据 subRL 的平衡因子调整旋转后的节点的平衡因子
if (bf == 0)
{
// 平衡因子为 0,旋转后各节点平衡因子也设置为 0
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = 0;
}
else if (bf == 1)
{
// 平衡因子为 1,旋转后 parent 的平衡因子为 -1
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1)
{
// 平衡因子为 -1,旋转后 subR 的平衡因子为 1,subRL 的平衡因子为 0
subR->_bf = 1;
subRL->_bf = 0;
parent->_bf = 0;
}
else
{
// 不应出现的平衡因子值,触发断言
assert(false);
}
}
AVL树的分析
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log_2 (N)。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此如果需要一种查询高效且有序的数据结构,而且数 据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合