【C++】AVL树
目录
- 前言
- 平衡二叉树的定义
- AVL树的插入
- AVL树插入的大致过程
- 更新平衡因子
- 调整最小不平衡因子
- 左单旋
- 右单旋
- 左右双旋
- 右左双旋
- AVL树的删除
- AVL树的查找

前言
前面我们在数据结构中学习了树,以及二叉树,还有二叉排序树,这节来学习平衡二叉树。
数据结构专题学习:数据结构学习
C++专题学习:深入学习C++
平衡二叉树的定义
在使用二叉排序树时,二叉排序树的查找效率取决于树的高度,
当构造二叉排序树的输入序列是有序
的时候,它就会形成只有左子树(从大到小输入)或者只有右子树(从小到大输入)的单支树,此时二叉排序树的性能显著变坏
。
因此,为了避免树的高度增长过快,降低二叉排序树的性能,规定在插入和删除节点时,要保证任意节点的左、右子树高度差的绝对值不超过1,将这样的树就称为平衡二叉树,即AVL树。
它给树中的每个节点都定义了一个平衡因子(bf)。
节点的平衡因子=左子树高度-右子树高度。
(当然也有右子树高度-左子树高度的,只要不违背高度差的绝对值不超过一即可)
平衡二叉树具有以下几个性质:
- AVL树是一棵二叉排序树
- AVL树的左右子树也都是AVL树
- AVL树节点的平衡因子的值只能为-1,0,1
AVL树如下图所示:
AVL树是一个三叉链结构,不仅有指向左右孩子节点的指针,还有指向父节点的指针,AVL树的代码定义如下:
template<class K, class V>
struct AVLTreeNode
{
pair<K, V> _kv;
//std::pair是C++标准库中的一个模板类,它能把两个不同类型的值组合成一个单元,这两个值分别由 first 和 second 成员访问。
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
int _bf; // balance factor
AVLTreeNode(const pair<K, V>& kv)
:_kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
,_bf(0)
{}
};
AVL树的插入
AVL树插入的大致过程
- 在插入值时会按照二叉排序树的插入规则进行插入。
- 插入新节点后,会影响祖先节点的高度,即影响祖先的平衡因子,因此要检查其插入路径上的节点是否因为此次操作而导致了不平衡。
- 如果导致了不平衡,也就是平衡因子变为了2或-2,此时就要调整这棵子树,进行旋转,从而调节平衡因子。
从新插入的节点开始往上找,找到第一个不平衡的节点,调整以该节点为根的最小不平衡子树。
- 如果更新平衡因子没有出现问题,则插入结束。
更新平衡因子
根据节点的平衡因子=左子树高度-右子树高度。
在插入节点后,对节点进行更新平衡因子。
代码如下:
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
//如果要插入节点的值大于cur节点的值,则往右走
if (cur->_kv.first < kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_kv.first > kv.first)//否则往左走
{
parent = cur;
cur = cur->_left;
}
else
{
//找到插入节点的位置
return false;
}
}
cur = new Node(kv);
//判断插在parent节点的左边还是右边
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;
// 更新平衡因子
while (parent)
{
if (cur == parent->_left)
parent->_bf++;
else
parent->_bf--;
if (parent->_bf == 0)
{
break;
}
else if (parent->_bf == 1 || parent->_bf == -1)
{
// 继续往上更新
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
// 不平衡了,旋转处理
if (parent->_bf == 2 && cur->_bf == 1)
{
//左子树高了,要右旋
RotateR(parent);
}
else if (parent->_bf == -2 && cur->_bf == -1)
{
//右子树高了,要左旋
RotateL(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1)
{
//左孩子的右子树高了,先左旋再右旋
RotateLR(parent);
}
else
{
//右孩子的左子树高了,先右旋再左旋
RotateRL(parent);
}
break;
}
else
{
assert(false);
}
}
return true;
}
调整最小不平衡因子
为了既能保证搜索树的规则,又能降低树的高度,调整平衡因子,总共有四种旋转操作:左单旋/右单旋/左右双旋/右左双旋。下面依次来解释。
首先,在调整之前,我们先要找到第一个不平衡的节点,从上面的调整平衡因子就可以知道。然后以该节点作为根节点,它所对应的子树就是我们要调整的最小不平衡子树。如下图:
因为==只要把最小的这棵不平衡子树让它恢复之后,它的祖先节点的平衡因子也都会恢复,==因此在插入一个新节点导致不平衡之后,我们只需要调整最小的这棵子树,就可以让其它节点也恢复平衡。
左单旋
当在节点A的右孩子的右子树上插入了一个新节点后,导致了A的平衡因子由-1减至-2,导致以A为节点的子树失去平衡,此时就需要一次左单旋操作。如下图所示:
图中:
H代表各个子树的高度都为H,至于为什么都为H,而不是H-1或者H+1,大家可以自行带入,找平衡因子就可以知道了。
RR插入,指的是在右孩子的右子树上插入了一个新节点导致树的不平衡。
BL:B的左孩子,BR:B的右孩子,AL:A的左孩子
二叉排序树的特性:左子树节点值 < 根节点值 < 右子树节点值
左单旋的过程为:将A的右孩子B左上旋转代替A成为根节点,将A节点左下旋转成为B的左孩子,而B的原左子树成为A的右子树。
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if(subRL)//如果subRL,即右孩子的左子树不为空,则更新它的父节点
subRL->_parent = parent;
//找到原来父节点的父节点
Node* parentParent = parent->_parent;
//将父节点更新到原来右孩子的左边
subR->_left = parent;
parent->_parent = subR;
if (parentParent == nullptr)//如果原来父节点就是根节点,则更新根节点
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (parent == parentParent->_left)
{
parentParent->_left = subR;
}
else
{
parentParent->_right = subR;
}
subR->_parent = parentParent;
}
//更新平衡因子。
parent->_bf = subR->_bf = 0;
}
可能有些复杂,学会大致思路也行,大致思路如下(设节点A是f,节点B是p,节点A的父节点是gf):
- f->left = p -> right;
- p->right = f;
- gf->left/right = p;
右单旋
当在节点A的左孩子的左子树上插入了一个新节点后,导致了A的平衡因子由1增至2,导致以A为节点的子树失去平衡,此时就需要一次右单旋操作。如下图所示:
右单旋的过程为:将A的左孩子B向右上旋转代替A成为根结点,将A右下旋转成为B的右孩子,而B的右子树变为A的左子树。
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
Node* parentParent = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (parentParent == nullptr)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (parent == parentParent->_left)
{
parentParent->_left = subL;
}
else
{
parentParent->_right = subL;
}
subL->_parent = parentParent;
}
parent->_bf = subL->_bf = 0;
}
大致思路如下(设节点A时f,节点B是p,节点A的父节点是gf):
- f->right = p->left;
- p->left = f;
- gf->left/right = p;
左右双旋
当在节点A的左孩子的右子树上插入了一个新节点后,导致了A的平衡因子由1增至2,导致以A为节点的子树失去平衡,此时就需要两次旋转操作,先左旋一次,再进行右旋。如下图所示:
左右双旋的过程为:先将A的左孩子B的右子树的根节点C向左上旋转提升到B的位置,然后把C向右上提升到A的位置。
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(parent->_left);
RotateR(parent);
if (bf == 0)
{
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 0;
}
else if (bf == 1)
{
subL->_bf = 0;
subLR->_bf = -1;
parent->_bf = 0;
}
else if (bf == -1)
{
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 1;
}
else
assert(false);
}
右左双旋
当在节点A的右孩子的左子树上插入了一个新节点后,导致了A的平衡因子由-1减至-2,导致以A为节点的子树失去平衡,此时就需要两次旋转操作,先右旋转一次,再进行左旋。如下图所示:
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(parent->_right);
RotateL(parent);
if (bf == 0)
{
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = 0;
}
else if (bf == 1)
{
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1)
{
subR->_bf = 1;
subRL->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
AVL树的删除
AVL树的删除有以下几个步骤:
-
用二叉排序树的方法对节点w执行删除操作。
-
若导致了不平衡,则
从节点w开始向上回溯,找到第一个不平衡的节点z
(最小不平衡子树),找到节点z的高度最高的孩子y
(即"个头"最高的儿子),再找到节点y的高度最高的孩子x
(即"个头"最高的孙子)。 -
对以z为根的子树进行平衡调整:
①孙子在LL,则进行右单旋。
②孙子在LR,则进行左右双旋。
③孙子在RR,则进行左单旋。
④孙子在RL,则进行右左双旋。 -
若调整后子树高度减1,则可能需要对z的祖先节点进行平衡调整直至回溯到根节点。
二叉排序树的删除操作可参考下图:
删除操作如下图:
AVL树的查找
在AVL树上查找的过程与BST数相同。因此查找过程中,进行关键字比较次数不超过树的深度
。假设nh表示深度为h的AVL树中含有的最少节点数。显然有n0=1,n1=1,n2=2并且有nh=nh-2+nh-1+1,如下图所示,则可以依次推出深度为h的AVL树含有的最少节点。含有n个节点的AVL树的最大深度为O(log~2~n),因此平均查找效率为O(log~2~n)。
查找的代码如下:
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
cur = cur->_right;
}
else if (cur->_key > key)
{
cur = cur->_left;
}
else
{
return cur;
}
}
return nullptr;
}
小tips:
当关键字以有序的顺序插入初始为空的AVL树中,若关键字个数为n=2h-1时,该二叉树一定是一棵满二叉树。
感谢大家观看,如果大家喜欢,希望大家一键三连支持一下,如有表述不正确,也欢迎大家批评指正。本篇还涵盖了王道的内容与图片。