【C++】AVL树
1、平衡因子
AVL树是一种自平衡的二叉搜索树,它通过在每个节点中维护一个平衡因子来保持树的平衡。平衡因子是指节点的左子树高度减去右子树高度的结果。
AVL树的基本原理是通过对插入和删除操作进行平衡调整来保持树的平衡性。当插入或删除一个节点后,如果导致某些节点的平衡因子超过范围(通常是 -1、0 或 1),就需要进行旋转操作来调整树的结构,使得每个节点的平衡因子保持在合理的范围内。
平衡因子的定义和作用如下:
- 平衡因子:平衡因子是指节点的左子树高度减去右子树高度的结果。公式为
balanceFactor = height(leftSubtree) - height(rightSubtree)
。 - 作用:平衡因子用于判断节点的平衡状态。当平衡因子为 -1、0 或 1 时,表示节点是平衡的;当平衡因子超过这些范围时,表示节点不平衡,需要进行平衡调整。
通过保持树中每个节点的平衡因子在合理的范围内,AVL树能够保持较为平衡的结构,从而提供了快速的搜索、插入和删除操作,保证了树的高度始终保持在 O(log n) 的水平。
2、节点结构
template<class K, class V>
class 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)
{}
};
这段代码定义了一个模板类 AVLTreeNode
,用于表示 AVL 树的节点。节点包含以下成员:
_left
:左子节点的指针。_right
:右子节点的指针。_parent
:父节点的指针。_kv
:键值对,用于存储节点的键和值。_bf
:平衡因子,用于记录节点的平衡状态。
在构造函数中,节点被初始化为指定的键值对,并将左子节点、右子节点、父节点和平衡因子初始化为默认值。
这个节点结构将用于构建 AVL 树,其中每个节点都包含了键值对以及与其他节点的关联关系和平衡信息。
3、平衡调整
3.1 左旋调整
void RotateL(Node* parent)
{
Node* subR = parent->_right;
subRL = subR->_left;
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
Node* ppnode = parent->_parent;
subR->_left = parent;
parent->_parent = subR;
if (ppnode == nullptr)
{
_root = subR;
_root->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = subR;
}
else
{
ppnode->_right = subR;
}
subR->_parent = ppnode;
}
parent->_bf = subR->_bf = 0;
}
这是一段实现左旋转的代码。下面是代码中各个步骤的解释:
Node* subR = parent->_right;
- 保存父节点的右子节点为 subR。subRL = subR->_left;
- 保存 subR 的左子节点为 subRL。parent->_right = subRL;
- 将 subRL 设置为 parent 的右子节点。if (subRL) subRL->_parent = parent;
- 如果 subRL 存在,则将其父节点设置为 parent。Node* ppnode = parent->_parent;
- 保存 parent 的父节点为 ppnode。subR->_left = parent;
- 将 parent 设置为 subR 的左子节点。parent->_parent = subR;
- 将 parent 的父节点设置为 subR。if (ppnode == nullptr) { ... } else { ... }
- 根据 ppnode 的情况进行处理。- 在适当的情况下,将 subR 设置为根节点或将其链接到 ppnode 上。
parent->_bf = subR->_bf = 0;
- 将 parent 和 subR 的平衡因子设置为 0,表示已经平衡。
这段代码实现了将以 parent 为根的子树进行左旋转的操作,旋转后能够保持 AVL 树的平衡性。
3.2 右旋调整
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
Node* ppnode = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (parent == _root)
{
_root = subL;
_root->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = subL;
}
else
{
ppnode->_right = subL;
}
subL->_parent = ppnode;
}
subL->_bf = parent->_bf = 0;
}
这段代码实现了右旋转操作。下面是代码中各个步骤的解释:
Node* subL = parent->_left;
- 保存父节点的左子节点为 subL。Node* subLR = subL->_right;
- 保存 subL 的右子节点为 subLR。parent->_left = subLR;
- 将 subLR 设置为 parent 的左子节点。if (subLR) subLR->_parent = parent;
- 如果 subLR 存在,则将其父节点设置为 parent。Node* ppnode = parent->_parent;
- 保存 parent 的父节点为 ppnode。subL->_right = parent;
- 将 parent 设置为 subL 的右子节点。parent->_parent = subL;
- 将 parent 的父节点设置为 subL。if (parent == _root) { ... } else { ... }
- 根据 parent 是否为根节点进行处理。- 在适当的情况下,将 subL 设置为根节点或将其链接到 ppnode 上。
subL->_bf = parent->_bf = 0;
- 将 subL 和 parent 的平衡因子设置为 0,表示已经平衡。
这段代码实现了将以 parent 为根的子树进行右旋转的操作,旋转后能够保持 AVL 树的平衡性。
3.3 左右旋
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(parent->_left);
RotateR(parent);
if (bf == 1)
{
parent->_bf = 0;
subLR->_bf = 0;
subL->_bf = -1;
}
else if (bf == -1)
{
parent->_bf = 1;
subLR->_bf = 0;
subL->_bf = 0;
}
else if (bf == 0)
{
parent->_bf = 0;
subLR->_bf = 0;
subL->_bf = 0;
}
else
{
assert(false);
}
}
这段代码实现了左右旋转操作(LR旋转)。下面是代码中各个步骤的解释:
Node* subL = parent->_left;
- 保存父节点的左子节点为 subL。Node* subLR = subL->_right;
- 保存 subL 的右子节点为 subLR。int bf = subLR->_bf;
- 保存 subLR 的平衡因子为 bf。- 调用左旋转操作
RotateL(parent->_left);
,将 subL 进行左旋转。 - 调用右旋转操作
RotateR(parent);
,将 parent 进行右旋转。 - 根据 bf 的值,更新旋转后节点的平衡因子。
- 如果 bf 为 1,表示左旋转前 subL 的右子树高度大于左子树高度,需要进行调整,将 parent、subLR 和 subL 的平衡因子更新为使树保持平衡的值。
- 如果 bf 为 -1,表示左旋转前 subL 的右子树高度小于左子树高度,需要进行调整,将 parent、subLR 和 subL 的平衡因子更新为使树保持平衡的值。
- 如果 bf 为 0,表示左旋转前 subL 的左子树和右子树高度相等,不需要调整平衡因子。
- 如果 bf 不是上述情况,则出现错误。
这段代码实现了在 AVL 树中进行左右旋转的操作,旋转后能够保持 AVL 树的平衡性。
3.4 右左旋
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(parent->_right);
RotateL(parent);
if (bf == 1)
{
parent->_bf = -1;
subRL->_bf = 0;
subR->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 0;
subRL->_bf = 0;
subR->_bf = 1;
}
else if (bf == 0)
{
parent->_bf = 0;
subRL->_bf = 0;
subR->_bf = 0;
}
else
{
assert(false);
}
}
原理同左右旋
4、插入操作
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
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 = new Node(kv);
if (parent->_kv.first > kv.first)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;
// 更新平衡因子
while (parent)
{
if (cur == parent->_right)
{
parent->_bf++;
}
else
{
parent->_bf--;
}
if (parent->_bf == 1 || parent->_bf == -1)
{
// 继续更新
parent = parent->_parent;
cur = cur->_parent;
}
else if (parent->_bf == 0)
{
break;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
// 需要旋转处理 -- 1、让这颗子树平衡 2、降低这颗子树的高度
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;
}
else
{
assert(false);
}
}
return true;
}
这是在 AVL 树中插入节点的函数。它首先检查根节点是否为空,如果为空,则将新节点作为根节点插入。否则,根据键值大小逐级比较,找到新节点应该插入的位置。
在插入节点后,代码通过迭代向上更新节点的平衡因子。如果节点的平衡因子为 1 或 -1,则继续向上更新。如果节点的平衡因子为 0,则表示该子树高度没有变化,插入操作完成。
当节点的平衡因子为 2 或 -2 时,说明该子树需要进行旋转操作来保持平衡。根据旋转操作的不同情况,调用相应的旋转函数:左旋转(RotateL)、右旋转(RotateR)、左右旋转(RotateLR)、右左旋转(RotateRL)。
旋转操作将树的结构进行调整,使得子树保持平衡,并降低子树的高度。最后,函数返回插入成功的标志。
这段代码实现了在 AVL 树中插入节点并自动调整平衡的功能。
5、删除操作
bool Remove(const pair<K, V>& kv)
{
Node* parent = nullptr;
Node* cur = _root;
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
{
break; // 找到要删除的节点
}
}
if (cur == nullptr)
{
return false; // 没有找到要删除的节点
}
// 节点删除分三种情况:无子节点、有一个子节点、有两个子节点
if (cur->_left == nullptr && cur->_right == nullptr)
{
// 无子节点,直接删除
if (cur == _root)
{
_root = nullptr;
}
else if (parent->_left == cur)
{
parent->_left = nullptr;
}
else
{
parent->_right = nullptr;
}
delete cur;
}
else if (cur->_left && cur->_right)
{
// 有两个子节点,找到右子树中的最小节点替代删除节点
Node* minRight = cur->_right;
parent = cur;
while (minRight->_left)
{
parent = minRight;
minRight = minRight->_left;
}
cur->_kv = minRight->_kv; // 替换键值对
cur = minRight; // 将删除操作转换为删除最小节点
// 继续执行后面的删除逻辑
}
else
{
// 有一个子节点,直接将子节点替代删除节点
Node* child = (cur->_left != nullptr) ? cur->_left : cur->_right;
if (cur == _root)
{
_root = child;
}
else if (parent->_left == cur)
{
parent->_left = child;
}
else
{
parent->_right = child;
}
child->_parent = parent;
delete cur;
}
// 更新平衡因子并进行平衡调整
while (parent)
{
if (cur == parent->_right)
{
parent->_bf--;
}
else
{
parent->_bf++;
}
if (parent->_bf == 1 || parent->_bf == -1)
{
// 继续更新
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 0)
{
break;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
// 需要旋转处理 -- 1、让这颗子树平衡 2、降低这颗子树的高度
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;
}
else
{
assert(false);
}
}
return true;
}
这段代码实现了删除AVL树中指定键值对的节点,并根据删除操作进行平衡因子的更新和平衡调整。
首先,代码通过遍历找到要删除的节点,并确定其父节点。然后,根据节点的子节点情况进行删除操作:
- 如果要删除的节点是叶子节点(即没有左右子节点),直接删除该节点。
- 如果要删除的节点有两个子节点,需要找到右子树中的最小节点(即右子树中最左侧的节点),将该最小节点的键值对替换到要删除的节点上,然后将删除操作转换为删除最小节点。
- 如果要删除的节点只有一个子节点,直接将子节点替代删除节点。
删除节点后,代码进入平衡因子的更新和平衡调整过程:
- 从删除节点的父节点开始,沿着路径向上更新每个节点的平衡因子。如果删除节点在父节点的右子树上,则父节点的平衡因子减1;如果删除节点在父节点的左子树上,则父节点的平衡因子加1。
- 如果更新过程中的某个节点的平衡因子为1或-1,继续向上更新。
- 如果更新过程中的某个节点的平衡因子为0,说明该节点的子树高度没有发生变化,不需要继续向上更新,退出循环。
- 如果更新过程中的某个节点的平衡因子为2或-2,说明该节点的子树发生了不平衡,需要进行旋转操作来恢复平衡。
- 根据不同的情况进行左旋、右旋、左右旋或右左旋等操作,使得子树恢复平衡并降低高度。
- 旋转操作完成后,退出循环。
这段代码中的旋转操作和旋转函数(如RotateL
、RotateR
、RotateLR
、RotateRL
)的实现需要根据具体情况进行适当的修改和调整。