=========================================================================
个人主页点击直达:小白不是程序媛
C++系列专栏:C++干货铺
代码仓库:Gitee
=========================================================================
目录
前言
AVL树
AVL树的概念
AVL树结点的定义
AVL树的插入
寻找插入结点的位置
修改平衡因子
AVL树的旋转
右单旋
左单旋
先右旋再左旋
先左旋再右旋
AVL树的验证
AVL树的删除(了解)
AVL树的性能
前面对map/multimap/set/multiset进行了简单的介绍,在其文档介绍中发现,这几个容器有个
共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中
插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此
map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查
找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii
和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
- 它的左右子树都是AVL树
- 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
template
struct AVLTreeNode
{
AVLTreeNode* _left;
//指向右孩子
AVLTreeNode* _right;
//指向左孩子
AVLTreeNode* _parent;
//键值对
pair _kv;
//平衡因子
int _bf;
//构造函数
AVLTreeNode(const pair& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _bf(0)
{}
};
AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么
AVL树的插入过程可以分为三步:
1. 按照二叉搜索树的方式插入新节点
2. 调整节点的平衡因子 3. 将插入后不符合的子树进行旋转调整
AVL树是基于搜索二叉树的,搜索二叉树的原则是左小右大。根据这个规则可以找到插入结点的位置。
注:搜索二叉树是没有两个相同的结点的。
//根节点是否为空,如果为空开辟新的结点
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
{
//AVL树是基于搜索二叉树的不会存在相等的情况
return false;
}
}
//找到要插入的位置进行插入
//创建新的结点
cur = new Node(kv);
if (parent->_kv.first < kv.first)
{
//大于放右边
parent -> _right = cur;
cur->_parent = parent;
}
else
{
//小于放左边
parent->_left = cur;
cur->_parent = parent;
}
无论是在哪里插入结点二叉树的高度一定会发生变化高度发生变化平衡因子也会发生变化,因此要对平衡因子进行修改。
while (parent)
{
//当新增结点为左子树时相当于左子树高度加1
//减数不变,被减数增加,结果减小
if (cur == parent->_left)
{
parent->_bf--;
}
else
{
//当新增结点为右子树时相当于右子树高度加1
//减数增加,被减数不变,结果增大
parent->_bf++;
}
//添加后左右子树高度相等
//对父节点往上的结点的平衡因子都没有影响
if (parent->_bf == 0)
{
break;
}
//添加节点后对树的高度发生变化
//继续向上修改平衡
//对新增结点的父节点的父节点的平衡因子进行修改
else if (parent->_bf == 1 || parent->_bf == -1)
{
cur = parent;
parent = parent->_parent;
}
}
结点的插入必然会影响父代及以上结点的平衡因子,从插入结点的父节点往上修改平衡因子,一定会出现平衡因子异常(超过1/0/-1),这时候就要进行旋转操作了。根据插入节点的位置不同,AVL树的旋转分为四种。
else if (parent->_bf == 2 || parent->_bf == -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)
{
//先右旋在左璇
RotateRL(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
//先左璇在右璇
RotateLR(parent);
}
新节点插入较高左子树的左侧
抽象图
//右单璇
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 (parent == _root)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
//更新父亲节点为旋转结点
if (parent == parentParent->_left)
{
parentParent->_left = subL;
}
else
{
parentParent->_right = subL;
}
//更新旋转结点的父亲结点,此时选装结点彻底为父亲结点
subL->_parent = parentParent;
}
//更新平衡因子
subL->_bf = parent->_bf = 0;
}
新节点插入较高右子树的右侧
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
subR->_left = parent;
Node* parentParent = parent->_parent;
parent->_parent = subR;
if (subRL)
{
subRL->_parent == nullptr;
}
if (parent == _root)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (parent == parentParent->_left)
{
parentParent->_left = subR;
}
else if (parent == parentParent->_right)
{
parentParent->_right = subR;
}
subR->_parent = parentParent;
}
parent->_bf = subR->_bf = 0;
}
新节点插入较高右子树的左侧
抽象图
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(parent->_right);
RotateL(parent);
if (bf == 0)
{
// subRL自己就是新增
parent->_bf = subR->_bf = subRL->_bf = 0;
}
else if (bf == -1)
{
// subRL的左子树新增
parent->_bf = 0;
subRL->_bf = 0;
subR->_bf = 1;
}
else if (bf == 1)
{
// subRL的右子树新增
parent->_bf = -1;
subRL->_bf = 0;
subR->_bf = 0;
}
else
{
assert(false);
}
}
新节点插入较高左子树的右侧
void RotateLR(Node* parent)
{
Node* cur = parent->_left;
Node* curright = cur->_right;
int bf = curright->_bf;
RotateL(cur);
RotateR(parent);
if (bf == 0)
{
parent->_bf = cur->_bf = curright->_bf = 0;
}
else if (bf == 1)
{
parent->_bf = 0;
cur->_bf = -1;
curright->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 1;
cur->_bf = 0;
curright->_bf = 0;
}
}
AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:
如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_kv.first << " ";
_InOrder(root->_right);
}
- 每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
- 节点的平衡因子是否计算正确
int _Height(Node* root)
{
if (root == nullptr)
return 0;
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
bool _IsBalance(Node* root)
{
if (root == nullptr)
{
return true;
}
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
if (rightHeight - leftHeight != root->_bf)
{
cout << root->_kv.first << "平衡因子异常" << endl;
return false;
}
return abs(rightHeight - leftHeight) < 2
&& _IsBalance(root->_left)
&& _IsBalance(root->_right);
}
因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不
错与删除不同的时,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。
具体实现大家可参考《算法导论》或《数据结构-用面向对象方法与C++描述》殷人昆版。
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这
样可以保证查询时高效的时间复杂度,即O(log2(n))。但是如果要对AVL树做一些结构修改的操
作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,
有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数
据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。
今天的对数据结构中基于二叉搜索树的AVL树的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,个人主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是我前进的动力!