目录
一、AVL树概念
1.二叉搜索树的缺点
我们知道 map/multimap/set/multiset 这些容器的底层都按照二叉搜索树实现,但是二叉搜索树的缺点在于,假如向树中插入的元素有序或者接近有序时,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),相当于在顺序表中搜索元素,效率低下。所以map/multimap/set/multiset的底层结构对二叉搜索树做了处理,采用平衡树来实现。
2.AVL树的概念
如何避免二叉树搜索树会退化成单支树的缺点呢?
向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。为什么高度差的绝对值不超过1而不是0呢?因为如果高度差的绝对值不超过0,那么二叉树就变成满二叉树了,因此绝对值不能超过1。这就引入了平衡二叉树的概念:
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
(1)它的左右子树都是AVL树
(2)左右子树高度之差(简称平衡因子=右子树高度-左子树高度)的绝对值不超过1(-1/0/1)
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 O(),搜索时间复杂度O(
)。
二、AVL树定义
由于要实现AVL树的增删改查,所以定义AVL树的节点,就需要定义parent,否则插入节点时,不知道要链接到树里面哪个节点下面。
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;// balance factor
AVLTreeNode(const pair<K, V>& kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
,_bf(0)
{}
};
三、AVL树插入
1.插入节点
插入节点需要先判断树是否为空:
(1)若为空,让该节点作为根节点
(2)若不为空,分3种情况:
①key比当前节点小,向左走
②key比当前节点大,向右走
③相等,插入失败
如果没找到节点,那么需要插入新节点
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->_left;
}
else if (cur->_kv.first < kv.first)// 往右找
{
parent = cur;
cur = cur->_right;
}
else
{
return false; // 值相等
}
}
cur = new Node(kv);
if (parent->_kv.first > kv.first)
parent->_left = cur;
else
parent->_right = cur;
cur->_parent = parent; // 反向链接父亲
// 控制平衡,更新平衡因子
// ...
return true;
}
2.控制平衡
(1)更新平衡因子
一个节点的平衡因子是否需要更新,取决于它的左右子树的高度是否发生变化。如果插入节点后,它的父节点到根节点路径上的部分节点的平衡因子发生改变,那么需要对这些节点进行更新,以保持树的平衡。因此
①如果新增节点是父亲的左子树(cur == parent->left),那么parent->_bf--
②如果新增节点是父亲的右子树(cur == parent->right),那么parent->_bf++
更新后:
什么决定了是否要继续往上更新爷爷节点,取决于parent所在的子树高度是否变化?变了继续更新,不变则不再更新。
a、parent->bf == 1 || parent->bf == -1 --》parent所在的子树变了,继续更新,为什么?
=》说明插入前parent->bf == 0,说明插入前左右两边高度相等,现在有一边高1,说明 parent 一边高一边低,高度变了。
b、parent->bf == 2 || parent->bf == -2 --》parent所在的子树不平衡,需要处理这颗子树(旋 转处理)。
c、 parent->bf == 0, parent所在的子树高度不变,不用继续网上更新。这一次插入结束,为 什么呢?
=》说明插入前是parent->bf == 1 or -1,插入之前一边高,一边低,插入节点填上矮的边, 它的高度不变。
// 控制平衡,更新平衡因子
while (parent)
{
if (parent->_bf == 1 || parent->_bf == -1)
{
// 继续更新
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 ()
{
}
}
else
{
assert(false); // 插入前这棵树已经有了问题
}
}
(2)旋转
旋转处理有4种:右单旋、左单旋、右左单旋、左右单旋
①左单旋
新节点插入到较高右子树的右侧,即右右-----左单旋
插入新节点前,AVL树是平衡的,新节点插入到60的右子树,那么60的右子树增加了一层,导致以30为根的二叉树不平衡。为了让30平衡,只能让30的右子树的高度减小一层,并把60的左子树的高度增加一层。
因此,要把60的右子树往上提,把30转下来,因为30比60小,只能把30放在60的左子树,60的左子树比60小,比30大,因此只能把60的左子树放在30的右子树。再更新节点平衡因子。
抽象图:
需要考虑的情况:
(1)60的左孩子可能存在,也可能不存在
(2)30可能是根节点,也可能是子树;如果是根节点,旋转后,要更新根节点。如果是子树,可能是左子树也可能是右子树,就把30原来的父亲的左或右指向60。
// 左单旋
void RotateL(Node* parent)
{
Node* childR = parent->_right;
Node* childRL = childR->_left;
parent->_right = childRL;
if (childRL)
childRL->_parent = parent; // 更新父亲
Node* ppnode = parent->_parent;
childR->_left = parent;
parent->_parent = childR; // 更新父亲
// 是一颗单独的树
if (ppnode == nullptr)
{
_root = childR;
_root->_parent = nullptr;
}
else
{
// 是一颗子树,判断是左子树还是右子树
if (ppnode->_left == parent)
{
ppnode->_left = childR;
}
else
{
ppnode->_right = childR;
}
childR->_parent = ppnode;// 更新父亲
}
parent->_bf = childR->_bf = 0; // 更新平衡因子
}
具象图:
h=0的情况:
例子:20变成10的左子树,10的左子树为空,不考虑
h=1的情况:
例子:10变成20的左子树,20的左子树变成10的右子树
h=2的情况:
例子:10变成20的左子树,20的左子树变成10的右子树
h == ...多种情况就不一一列举了。
①右单旋
将新节点插入到较高左子树的左侧,即左左-----右单旋
插入新节点前,AVL树是平衡的,新节点插入到30的左子树,那么30的左子树增加了一层,导致以60为根的二叉树不平衡。为了让30平衡,只能让30的左子树的高度减小一层,并把60的右子树的高度增加一层。
因此,要把30的左子树往上提,把60转下来,因为60比30大,只能把60放在30的右子树,30的右子树比30大,比60小,因此只能把30的右子树放在60的左子树。再更新节点平衡因子。
抽象图:需要考虑的情况:
(1)30的右孩子可能存在,也可能不存在
(2)60可能是根节点,也可能是子树;如果是根节点,旋转后,要更新根节点。如果是子树,可能是左子树也可能是右子树,就把60原来的父亲的左或右指向30。
// 右单旋
void RotateR(Node* parent)
{
Node* childL = parent->_left;
Node* childLR = childL->_right;
parent->_left = childLR;
if (childLR)
childLR->_parent = parent; // 更新父亲
Node* ppnode = parent->_parent;
childL->_right = parent;
parent->_parent = childL; // 更新父亲
// 是一颗单独的树
if (parent == _root)
{
_root = childL;
_root->_parent = nullptr;
}
else
{
// 是一颗子树,判断是左子树还是右子树
if (ppnode->_left == parent)
{
ppnode->_left = childL;
}
else
{
ppnode->_right = childL;
}
childL->_parent = ppnode;// 更新父亲
}
parent->_bf = childL->_bf = 0; // 更新平衡因子
}
具象图:模板就不画了,根据左单旋照葫芦画瓢,举样例
h=0的情况:
20变成10的左子树,10的左子树为空,不用考虑
h=1的情况:
20变成10的右子树,10的右子树12变成20的左子树
h=2的情况:
20变成10的左子树,10的右子树12变成20的左子树
③左右双旋
新节点插入较高左子树的右侧---左右:先左单旋再右单旋
插入新节点前,AVL树是平衡的,新节点插入到60的左子树,那么60的左子树增加了一层,导致以90为根的二叉树不平衡。为了让90平衡,只能让90的左子树的高度减小一层。
现在90左子树的高度是h+1,但是现在不能把30进行右单旋,因为要把30右单旋,那么60和90都必须处于30的右子树,这办不到。因此要分为两步:
(1)先把30左单旋
(2)再把60右单旋
void RotateLR(Node* parent)
{
Node* childL = parent->_left;
Node* childLR = childL->_right;
int bf = childLR->_bf;
//旋转之前,保存childLR的平衡因子,旋转完成之后,需要根据该平衡因子来调整其他节点的平衡因子
RotateL(parent->_left);
RotateR(parent);
if (bf == 1)
{
parent->_bf = 0;
childLR->_bf = 0;
childL->_bf = -1;
}
else if (bf == -1)
{
parent->_bf = -1;
childLR->_bf = 0;
childL->_bf = 0;
}
else if (bf == 0)
{
parent->_bf = 0;
childLR->_bf = 0;
childL->_bf = 0;
}
else
{
assert(false);
}
}
④右左双旋
新节点插入较高右子树的左侧---右左:先右单旋再左单旋
插入新节点前,AVL树是平衡的,新节点插入到60的右子树,那么60的右子树增加了一层,导致以30为根的二叉树不平衡。为了让30平衡,只能让30的右子树的高度减小一层。
现在30右子树的高度是h+1,但是现在不能把60进行左单旋,因为要把60左单旋,那么30和90都必须处于60的左子树,这办不到。因此要分为两步:
(1)先把90右单旋
(2)再把30左单旋
//右左单旋
void RotateRL(Node* parent)
{
Node* childR = parent->_right;
Node* childRL = childR->_right;
int bf = childLR->_bf;
//旋转之前,保存childLR的平衡因子,旋转完成之后,需要根据该平衡因子来调整其他节点的平衡因子
RotateR(parent->_right);
RotateL(parent);
if (bf == 1)
{
parent->_bf = 0;
childLR->_bf = -1;
childL->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 1;
childLR->_bf = 0;
childL->_bf = 0;
}
else if (bf == 0)
{
parent->_bf = 0;
childLR->_bf = 0;
childL->_bf = 0;
}
else
{
assert(false);
}
}
四、AVL树高度
计算树高度肯定要递归计算:
(1)计算左右子树的高度
(2)谁的高度大,那AVL树的高度就为较高子树的高度+1
int _Height(Node* root)
{
if (root == nullptr)
return 0;
int leftH = _Height(root->_left);
int rightH = _Height(root->_right);
return leftH > rightH ? leftH + 1 : rightH + 1;
}
五、判断是否为AVL树
检查树是否是AVL树:
如何检验自己的 AVL 树是否合法? 答案是通过平衡因子检查
平衡因子 反映的是 左右子树高度之差,计算出 左右子树高度之差 与当前节点的 平衡因子 进行比对,如果发现不同,则说明 AVL 树 非法
或者如果当前节点的 平衡因子 取值范围不在 [-1, 1] 内,也可以判断 非法
(1)获取左右子树高度
(2)根据左右子树高度计算平衡因子
(3)当平衡因子<=2 || -2时就是平衡的
bool _IsBalance(Node* root)
{
if (root == nullptr)
return true;
int leftH = _Height(root->_left);
int rightH = _Height(root->_right);
if (rightH - leftH != root->_bf)
{
cout << root->_kv.first << "节点平衡因子异常" << endl;
return false;
}
// 递归所有子树判断是否平衡
return abs(leftH - rightH) < 2
&& _IsBalance(root->_left)
&& _IsBalance(root->_right);
}
六、AVL树的性能
AVL
树是一棵 绝对平衡 的二叉树,对高度的控制极为苛刻,稍微有点退化的趋势,都要被旋转调整,这样做的好处是 严格控制了查询的时间,查询速度极快,约为 logN