目录
一、什么是AVL树
AVL树就是二叉搜索树的进一步的优化,二叉搜索树虽可以缩短查找的效率,但是当数据有序或接近有序二叉搜索树变成单支树,在查找的过程中其效率会变得更低下。所以有一种可以达到自平衡二叉搜索树(AVL),它在每次插入或删除节点时会通过平衡因子(高度差)来进行旋转操作保持树的平衡。AVL树的名称来自它的发明者G. M. Adelson-Velsky和E. M. Landis。
AVL树的特点:
- AVL树的平衡受到平衡因子(高度差)的影响,对于任意节点,其左子树和右子树的高度差不超过1。如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在O(log n),搜索时间复杂度O(log n)
- 当向AVL树中插入或删除节点时,就会存在树的平衡性被破坏的可能。所以,为了恢复平衡,AVL树使用四种旋转操作:左旋、右旋、左右旋和右左旋。通过这些旋转操作,AVL树可以在O(log n)时间内完成插入和删除操作,并保持树的平衡。
- 因为AVL树保持严格的平衡,因此其在查找、插入和删除操作上的时间复杂度都是O(log n),使得它成为一种高效的数据结构。
二、AVL树的作用
- 提供高效的查找操作:AVL树的平衡性保证了在最坏情况下,查找操作的时间复杂度为O(log n),其中n是树中节点的数量。这使得AVL树在需要频繁查找元素的场景下非常有用,例如数据库索引、字典等。
- 支持有序遍历:由于AVL树是一种二叉搜索树,其节点按照特定的顺序进行排列。因此,通过对AVL树进行中序遍历,可以按照升序或降序获取树中的所有元素。这使得AVL树在需要按顺序处理数据的场景下很有用,例如范围查询、排序等。
- 动态数据集的维护:AVL树的自平衡性适用于动态数据集的维护。当插入或删除节点时,AVL树会通过旋转操作来保持平衡,从而保证树的高度始终保持在O(log n)的范围内。这使得AVL树在需要频繁地插入和删除元素的场景下表现出色,例如实时的数据更新、动态排序等。
- 实现其他数据结构:AVL树也可以作为其他数据结构的基础,例如集合、映射、优先队列等。通过在AVL树上实现相应的操作,可以实现这些数据结构,并且保证其在插入、删除和查找等操作上的高效性和平衡性。
三、树节点的定义
template<class K, class V>
struct AVLTreeNode
{
AVLTreeNode<K, V>* _left; //左子节点
AVLTreeNode<K, V>* _right; //右子节点
AVLTreeNode<K, V>* _parent; //父亲节点
pair<K, V> _kv; // 用于存储节点值
int _bf; // 平衡因子
AVLTreeNode(const pair<K, V>& kv)//初始化构造
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _bf(0)
{}
}
typedef AVLTreeNode<K, V> Node;
四、节点的插入
AVL树的插入主要分为两点:
1.根据二叉搜索树的性质进行插入新的节点
二叉搜索树性质:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分为二叉搜索树
2.调整节点的平衡因子
调整规则:(parent表示的是插入后的节点的父亲节点)
1、新增在右,父节点的平衡因子parent->bf++;新增在左,父节点的平衡因子parent->bf--;
2、更新后,如果parent->bf== 1 或-1,说明parent插入前的平衡因子是0,左右子树高度相等,插入后有一边高,parent高度变了,所以需要继续更新上面节点的 bf
3、更新后,如果parent->bf ==0,说明parent插入前的平衡因子是1 或 -1,左右子树一边高一边低,插入后两边一样高,插入填上了矮了那边,parent所在子树高度不变,不需要继续往上更新
4、更新后,如果parent-> bf== 2 或 -2,说明parent插入前的平衡因子是1 或 -1,是平衡临界值,插入后变成2 或 -2,打破了平衡,parent所在子树需要根据实际情况进行旋转处理。
5、更新后,如果parent->bf > 2 或 <-2的值,则说明插入前就不是AVL树,需要去检查之前操作的问题。
插入代码讲解:
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)//如果是空树,插入的节点就是根节点
{
_root = new Node(kv);
return true;
}
//parent用于记录cur节点的父亲节点,防止在插入新节点时丢失
Node* parent = nullptr;
Node* cur = _root;
// 遍历找到插入新节点的位置,parent记录该位置,cur节点用于探索后面的节点或存储新节点便于插入
while (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作为新节点,通过parent与该树进行连接,完成节点插入
cur = new Node(kv);
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;
// 控制平衡
// 更新平衡因子
//根据上面的调整规则更新平衡因子
while (parent)
{
//规则1
if (cur == parent->_right)
{
parent->_bf++;
}
else
{
parent->_bf--;
}
//规则3
if (parent->_bf == 0)
{
break;
}
//规则2
else if (abs(parent->_bf) == 1)
{
parent = parent->_parent;
cur = cur->_parent;
}
//规则4
else if (abs(parent->_bf) == 2)
{
// 说明parent所在子树已经不平衡了,需要旋转处理
if (parent->_bf == 2 && cur->_bf == 1)
{
RotateL(parent); //左单旋
}
else if ((parent->_bf == -2 && cur->_bf == -1))
{
RotateR(parent); //右单旋
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
RotateLR(parent); // 左右双旋
}
else if (parent->_bf == 2 && cur->_bf == -1)
{
RotateRL(parent); // 右左双旋
}
else
{
assert(false);
}
break;
}
//规则5不存在的可能
else
{
assert(false);
}
}
return true;
}
五、旋转
1.左单旋
旋转原因:
在插入新节点后,父节点parent->bf==2,子节点cur->bf==1。也就是在插入前,该节点bf=1,插入后bf=2,是在该节点的右子树进行插入。如下图:
subR:parent的右子节点
subRL:parent的右子节点的左节点
旋转后subRL的父亲节点,要由原来的subR变为parent节点。subR节点,就变为新的parent节点
左单旋代码:
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
//将原来的subRL节点的父亲节点指向为parent
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
//记录当前parent节点的父节点,保证旋转后,能与主树连接上
Node* ppNode = parent->_parent;
//将subR变为新的parent节点
subR->_left = parent;
parent->_parent = subR;
//将旋转后的子树与原来的主树进行连接
if (_root == parent)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
//判断该子树是原来主树的左子树还是右子树,在进行连接
if (ppNode->_left == parent)
{
ppNode->_left = subR;
}
else
{
ppNode->_right = subR;
}
subR->_parent = ppNode;
}
}
2.右单旋
旋转原因:
parent->bf==-2,cur->bf==-1
在parent的左子树左节点上进行插入
右旋代码:
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
//将subLR与parent相连接
parent->_left = subLR;
if (subLR)
{
subLR->_parent = parent;
}
Node* ppNode = parent->_parent;
//subL变为新的parent节点
subL->_right = parent;
parent->_parent = subL;
//将旋转后的子树与主树进行连接
if (_root == parent)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (ppNode->_left == parent)
{
ppNode->_left = subL;
}
else
{
ppNode->_right = subL;
}
subL->_parent = ppNode;
}
subL->_bf = parent->_bf = 0;
}
3.左右双旋
旋转原因:
parent->bf == -2 && cur->bf == 1,在parent的左子树的右子树上进行插入,破坏了平衡因子。
需要先进行左旋,再进行右旋,再进行调整平衡因子。
左右双旋代码 :
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
//先左旋、再右旋,可以套用上面已实现的方法
RotateL(parent->_left);
RotateR(parent);
subLR->_bf = 0;
//调整平衡因子
//有三种情况
if (bf == 1)
{
parent->_bf = 0;
subL->_bf = -1;
}
else if (bf == -1)
{
parent->_bf = 1;
subL->_bf = 0;
}
else if (bf == 0)
{
parent->_bf = 0;
subL->_bf = 0;
}
else
{
assert(false);
}
}
4.右左双旋
旋转原因:
parent->bf == 2 && cur->bf == -1。插入节点在右子树的左子树上
需要先进行右旋,再进行左旋,再调整平衡因子
右左旋代码:
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
//先右旋,再左旋
RotateR(parent->_right);
RotateL(parent);
//再调整平衡因子
subRL->_bf = 0;
if (bf == 1)
{
subR->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1)
{
subR->_bf = 1;
parent->_bf = 0;
}
else if (bf == 0)
{
parent->_bf = 0;
subR->_bf = 0;
}
else
{
assert(false);
}
}
基于以上就是AVL树的概念和旋转的原理和代码的实现。