文章目录
- 1. AVL树的概念
- 2. AVL树结点的定义
- 3. AVL 树的插入
- 3.1 关于平衡因子
- 3.2 插入代码
- 4. AVL 树的旋转逻辑
- 4.1 不需要旋转
- 4.2 左旋
- 4.3 右旋
- 4.4 双旋
- 4.4.1 先右后左单旋(RL 旋转)
- 4.4.2 先左后右单旋(LR 旋转)
- 4.5 完整插入代码(插入+旋转)
- 5. AVL 树的验证
- 5.1 中序遍历打印和计算树的高度
- 5.2 验证
- 5.3 数据测试
二叉搜索树虽可以缩短查找的效率,但 如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。
所以就发明了 AVL 树。
1. AVL树的概念
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
- 它的左右子树都是 AVL 树
- 左右子树高度之差(简称平衡因子)的绝对值不超过 1(1/0/-1)
结点是一个个插入的,有些情况无法做到高度差等于 0(如:2 个节点等偶数个结点)
平衡因子 = 右子树高度 − 左子树高度 平衡因子=右子树高度-左子树高度 平衡因子=右子树高度−左子树高度(我们这里是如此实现)
平衡因子并不是必须的,它只是一种控制方式,帮助我们更便捷地控制树
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 O ( l o g 2 n ) O(log_2 n) O(log2n),搜索时间复杂度为 O ( l o g 2 n ) O(log_2 n) O(log2n)。
2. AVL树结点的定义
template<class K, class V>
struct AVLTreeNode
{
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
int _bf;//balance factor 平衡因子
pair <K, V> _kv;
AVLTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
, _kv(kv)
{}
};
3. AVL 树的插入
p
是 pParent
,c
是 pCur
- =】哦怕【、按二叉搜索树规则插入(复用代)
- 更新平衡因子
- 插入结点会影响哪些结点的平衡因子呢?——新增结点的部分祖先
- 更新原则:
c
是p
的左边,p->bf--
c
是p
的右边,p->bf++
是否继续更新取决于p
的高度是否变化,是否会影响爷爷结点
3.1 关于平衡因子
pCur
插入后,pParent
的平衡因子一定需要调整,在插入之前,pParent
的平衡因子分为三种情况:-1,0, 1, 分以下两种情况:
- 如果
pCur
插入到pParent
的左侧,只需给pParent的平衡因子-1
即可 - 如果
pCur
插入到pParent
的右侧,只需给pParent的平衡因子+1
即可
检查平衡因子
-
更新后,p->bf ==0,p 所在的子树高度不变,不会影响爷爷。说明更新前,p 的 bf 是 1 或者-1,p 的矮的那边插入了节点,左右均衡了,p 的高度不变,不会影响爷爷——更新结束
-
更新后,p->bf==1/-1,p 所在的子树的高度变了,会影响爷爷说明更新前,p 的 bf 是 0,p 的有一边插入,p 变得不均衡,但是不违反规则, p 的高度变了,会影响爷爷,需要往上检查一下——继续往上更新(往上结点走更新规则)
-
更新后,p->bf==2/-2, 说明 p 所在的子树违反了平衡规则——处理 ->旋转
结束条件
c
更新到root
位置- 更新后,p->bf ==0 结束
- 旋转后结束
旋转让p
所在的子树高度回到插入之前的状态,不会对上层的bf
有影响
3.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)//更新parent和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;
}
}
//更新结点后,开辟出新结点,并使parent结点指向cur
//因为在指向cur前的parent是树末端结点,指向nullptr
cur = new Node(kv);
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;//容易忽略
//更新cur里的parent指向——可以理解为二叉搜索树是树状的双向链表
//更新双亲的平衡因子
while (parent)
{
//继续向上更新(循环)
//直到根节点或遇到一个平衡因子为0的节点为止
if (cur == parent->_left)//新结点在当前的parent(那一树)左边插入,父节点的平衡因子--
{
parent->_bf--;
}
else//新结点在当前的parent(那一树)右边插入,父节点的平衡因子++
{
parent->_bf++;
}
if (parent->_bf == 0)//插入之前parent的平衡因子为正负1,插入后被调整成0,此时满足AVL树的性质,插入成功
{
break;
}
else if (parent->_bf == 1 || parent->_bf == -1)
{
//如果parent的平衡因子为正负1,
// 说明插入前parent的平衡因子一定为0,插入后被更新成正负1,
// 此时以parent为根的树的高度增加,需要继续向上更新
cur = cur->_parent;
parent = parent->_parent;
}
//检查是否需要旋转
else if (parent->_bf == 2 || parent->_bf == -2)
{
//如果parent的平衡因子为正负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
{
RotateRL(parent);
}
break;
}
else//插入之前AVL树就有问题
{
assert(false);
}
}
//不需要旋转,或者已经通过旋转恢复树的平衡,那么插入就完成了
return true;
}
4. AVL 树的旋转逻辑
下面动画演示了不断将节点插入 AVL 树时的情况,并且演示了左旋(Left Rotation)、右旋(Right Rotation)、右左旋转(Right-Left Rotation)、左右旋转(Left-Right Rotation)以及带子树的右旋(Right Rotation with children)
当插入一个新结点后当前 parent 平衡因子为 2/-2,这时候需要旋转调整,将以 parent 为根的这棵树调整为 AVL 树。
这时候还要分四种情况:
- 新结点插入到左子树的左侧。这时候我们需要右单旋。
- 新结点插入到右子树的右侧。这时候我们需要左单旋。
- 新结点插入到右子树的左侧。这时候我们需要先右单旋再左单旋。
- 新结点插入到左子树的右侧。这时候我们需要先左单旋再右单旋。
旋转目的
- 保持搜索规则
- 当前树从不平衡旋转为平衡
- 降低树的高度
旋转有 2 个作用
- 让左右子树均衡
- 同时使高度下降(或者保持旋转前高度)
4.1 不需要旋转
每个结点的平衡因子都在允许的范围内(1/0/-1)
4.2 左旋
新结点插入到右子树的右侧。这时候我们需要左单旋。
- 不仅要动两个结点,还要考虑
parent
subRL
可能为空——60节点
的左孩子可能存在,也可能不存在- 如果
parent结点
在一棵子树则要跟父亲结点进行链接,如果不是子树,subR
可能要跟根进行链接
30可能是根节点,也可能是子树- 如果是根节点,旋转完成后,要更新根节点
- 如果是子树,可能是parent的父节点的左子树,也可能是右子树
- 平衡因子的更新——旋转结束后,平衡因子需要调整的就两个结点,一个是起始parent结点,一个是起始parent结点的右结点(subR),其他结点的左右高度差没有变化。这两个结点调整后平衡因子都变为0
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
//开始重新指向
parent->_right = subRL;
if (subRL)//subRL有可能是空树
//如果该树(结点)存在,则重新指向其父节点
subRL->_parent = parent;
subR->_left = parent;
//还有subR的parent还未处理
Node* ppnode = parent->_parent;
parent->_parent = subR;
if (parent == _root)
{
_root = subR;
subR->_parent = nullptr;
}
else//parent结点是在子树里,所以才需要ppnode,也就是爷爷结点
{
//subR链接上之前的爷爷结点
if (ppnode->_left == parent)
{
ppnode->_left = subR;
}
else
{
ppnode->_right = subR;
}
subR->_parent = ppnode;
}
parent->_bf = 0;
subR->_bf = 0;
}
4.3 右旋
新结点插入到左子树的左侧。这时候我们需要右单旋。
- 不仅要动两个结点,还要考虑
parent
subRL
可能为空——60节点
的左孩子可能存在,也可能不存在- 如果
parent结点
在一棵子树则要跟父亲结点进行链接,如果不是子树,subR
可能要跟根进行链接
30 可能是根节点,也可能是子树- 如果是根节点,旋转完成后,要更新根节点
- 如果是子树,可能是 parent 的父节点的左子树,也可能是右子树
- 平衡因子的更新——旋转结束后,平衡因子需要调整的就两个结点,一个是起始 parent 结点,一个是起始 parent 结点的左结点(subL),其他结点的左右高度差没有变化。这两个结点调整后平衡因子都变为 0
可以和 RotateL
进行比对,代码整体相似,细节除外
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
{
subLR->_parent = parent;
}
subL->_right = parent;
Node* ppnode = parent->_parent;
parent->_parent = subL;
if (parent == _root)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = subL;
}
else
{
ppnode->_right = subL;
}
subL->_parent = ppnode;
}
subL->_bf = 0;
parent->_bf = 0;
}
4.4 双旋
4.4.1 先右后左单旋(RL 旋转)
新结点插入到右子树的左侧。这时候我们需要先右单旋再左单旋。
注:它 subRL
是分开来(便于细分情况),原本就是一个整体
总的来看,中间值(如:60) 最终会作为(子树的)根,同时 平衡因子
为 0
这里我们看到,parent 的右子树的左侧插入新结点后导致 parent 不平衡,我们的策略是:先对 90 进行左单旋,再对 30 进行右单旋。
这里我们需要注意的是:我们可以调用上面已经写好的左单旋和右单旋的函数,但是,左单旋和右单旋的函数只对它们的 parent 结点及其左结点或者右结点的平衡因子作出了调整,并且都调整为了 0。我们观察上述抽象图,我们调整后,会有三个结点的平衡因子需要作出调整(结点 30、60、90)!
考虑以下三种情况:
-
新结点插入的是 60 的左子树:插入后三个结点的平衡因子为 30 (bf==-2),90 (bf==-1),60 (bf==-1),调整后的三个平衡因子为 30 (bf==0),90 (bf==1),60 (bf==0)。
-
新结点插入的是 60 的右子树:插入后三个结点的平衡因子为 30 (bf==-2),90 (bf==-1),60 (bf==1),调整后的三个平衡因子为 30 (bf==-1),90 (bf==0),60 (bf==0)。
-
新结点插入的是一棵原本仅有 2 个结点的 AVL 树:插入后的三个结点的平衡因子为 30 (bf==2),90 (bf==-1),60 (bf==0),调整后的三个平衡因子为 30 (bf==0),90 (bf==0),60 (bf==0)。
-
根据旋转前
subRL->_bf
来判断新的结点在哪里插入 -
并以此来确定如何更新
parent
和subR
的平衡因子
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;//记录旋转之前该结点的bf
RotateR(subR);
RotateL(parent);
subRL->_bf = 0;
if (bf == 1)//在c插入
{
parent->_bf = -1;
subR->_bf = 0;
}
else if (bf == -1)//在b插入
{
parent->_bf = 0;
subR->_bf = -1;
}
else if (bf == 0)//60作为被插入的结点
{
parent->_bf = 0;
subR->_bf = 0;
}
else
{
assert(false);
}
}
4.4.2 先左后右单旋(LR 旋转)
新结点插入到左子树的右侧。这时候我们需要先单左旋再右单旋。
parent 的左子树的右侧插入新结点后导致 parent 不平衡,我们的策略是:先对 30 进行左单旋,再对 90 进行右单旋。
这里我们需要注意的是:我们可以调用上面已经写好的左单旋和右单旋的函数,但是,左单旋和右单旋的函数只对它们的 parent 结点及其左结点或者右结点的平衡因子作出了调整,并且都调整为了 0。我们观察上述抽象图,我们调整后,会有三个结点的平衡因子需要作出调整(结点 30、60、90)!
考虑以下三种情况:
-
新结点插入的是 60 的左子树:插入后三个结点的平衡因子为 90 (bf==-2),30 (bf==1),60 (bf==-1),调整后的三个平衡因子为 90 (bf==1),30 (bf==0),60 (bf==0)。
-
新结点插入的是 60 的右子树):插入后三个结点的平衡因子为 90 (bf==-2),30 (bf==1),60 (bf==1),调整后的三个平衡因子为 90 (bf==0),30 (bf==-1),60 (bf==0)。
-
新结点插入的是一棵原本仅有 2 个结点的 AVL 树:插入后的三个结点的平衡因子为 90 (bf==-2),30 (bf==1),60 (bf==0),调整后的三个平衡因子为 90 (bf==0),30 (bf==0),60 (bf==0)。
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(parent->_left);
RotateR(parent);
//旋转结束,重置bf
if (bf == -1)
{
subLR->_bf = 0;
subL->_bf = 0;
parent->_bf = 1;
}
else if (bf == 1)
{
subLR->_bf = 0;
subL->_bf = -1;
parent->_bf = 0;
}
else if (bf == 0)
{
subLR->_bf = 0;
subL->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
4.5 完整插入代码(插入+旋转)
bool Insert(const pair<K, V>& kv)
{
//和二叉搜索树的插入一样的逻辑,复用
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)//更新parent和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;
}
}
//更新结点后,开辟出新结点,并使parent结点指向cur
//因为在指向cur前的parent是树末端结点,指向nullptr
cur = new Node(kv);
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;//容易忽略
//更新cur里的parent指向——可以理解为二叉搜索树是树状的双向链表
//更新双亲的平衡因子
while (parent)
{
if (cur == parent->_left)//新结点在左边插入,父节点的平衡因子--
{
parent->_bf--;
}
else//新结点在右边插入,父节点的平衡因子++
{
parent->_bf++;
}
if (parent->_bf == 0)//插入之前parent的平衡因子为正负1,插入后被调整成0,此时满足AVL树的性质,插入成功
{
break;
}
else if (parent->_bf == 1 || parent->_bf == -1)
{
//如果parent的平衡因子为正负1,
// 说明插入前parent的平衡因子一定为0,插入后被更新成正负1,
// 此时以parent为根的树的高度增加,需要继续向上更新
cur = cur->_parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
//如果pParent的平衡因子为正负2,
// 则pParent的平衡因子违反平衡树的性质,需要对其进行旋转处理
//旋转处理
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
{
RotateRL(parent);
}
break;
}
else//插入之前AVL树就有问题
{
assert(false);
}
}
return true;
}
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL)//subRL有可能是空树
subRL->_parent = parent;
subR->_left = parent;
//还有subR的parent还未处理
Node* ppnode = parent->_parent;
parent->_parent = subR;
if (parent == _root)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = subR;
}
else
{
ppnode->_right = subR;
}
subR->_parent = ppnode;
}
parent->_bf = 0;
subR->_bf = 0;
}
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
{
subLR->_parent = parent;
}
subL->_right = parent;
Node* ppnode = parent->_parent;
parent->_parent = subL;
if (parent == _root)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = subL;
}
else
{
ppnode->_right = subL;
}
subL->_parent = ppnode;
}
subL->_bf = 0;
parent->_bf = 0;
}
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(parent->_left);
RotateR(parent);
//旋转结束,重置bf
if (bf == -1)
{
subLR->_bf = 0;
subL->_bf = 0;
parent->_bf = 1;
}
else if (bf == 1)
{
subLR->_bf = 0;
subL->_bf = -1;
parent->_bf = 0;
}
else if (bf == 0)
{
subLR->_bf = 0;
subL->_bf = 0;
parent->_bf = 0;
}
else
{
assert (false);
}
}
void RotateRL (Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR (subR);
RotateL (parent);
subRL->_bf = 0;
if (bf == 1)//在 c 插入
{
parent->_bf = -1;
subR->_bf = 0;
}
else if (bf == -1)//在 b 插入
{
parent->_bf = 0;
subR->_bf = -1;
}
else if (bf == 0)//60 作为被插入的结点
{
parent->_bf = 0;
subR->_bf = 0;
}
else
{
assert (false);
}
}
5. AVL 树的验证
5.1 中序遍历打印和计算树的高度
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_kv.first << "[" << root->_bf << "]" << endl;
_InOrder(root->_right);
}
void InOrder()
{
_InOrder(_root);
}
int Height()
{
return _Height(_root);
}
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;
}
5.2 验证
- 验证其为二叉搜索树
如果中序遍历可得到一个有序的序列,就说明为二叉搜索树 - 验证其为平衡树
- 每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
- 节点的平衡因子是否计算正确
在这里有两种方式:前序和(后序+引用参数),前者易于理解,后者高效且值得细品
//前序
bool _IsBalance(Node* root)
{
if (root == nullptr)
{
return true;
}
int leftHeight = Height(root->_left);
int rightHeight = Height(root->_right);
if (abs(rightHeight - leftHeight) >= 2)
{
cout << root->_kv.first << "不平衡" << endl;
return false;
}
if (rightHeight - leftHeight != root->_bf)
{
cout << root->_kv.first << "平衡因子异常" << endl;
return false;
}
return _IsBalance(root->_left) && _IsBalance(root->_right);
}
bool IsBalance()
{
return _IsBalance(_root);
}
- 基础情况:
如果 root 为 nullptr(即树为空),那么它自然是平衡的,所以返回 true。 - 计算左右子树的高度:
使用_Height 函数计算左子树和右子树的高度。 - 检查高度差:
计算左右子树的高度差(abs (leftHeight - rightHeight))
。
如果这个高度差大于或等于 2,说明树不是 AVL 树,所以打印出当前根节点的值(可能是为了调试目的),并返回 false。 - 检查平衡因子:
AVL 树的每个节点通常都有一个平衡因子(BF),它是右子树高度减去左子树高度的结果。
接下来,代码检查右子树高度减去左子树高度得到的差值是否等于当前节点的平衡因子(root->_bf)。
如果不等,说明平衡因子不正确,可能是在之前的旋转或插入/删除操作中出现了错误,所以打印出当前根节点的值(为了调试)并返回 false。 - 递归检查子树:
最后,代码递归地检查左子树和右子树是否都是 AVL 树。只有当两个子树都是 AVL 树时,当前树才是 AVL 树。
优点就是简单直观。
但是,这方法缺点很明显
- 重复计算高度:每次检查节点的平衡状态时,都需要调用
Height
函数,导致子树的高度被多次重复计算,效率较低。 - 效率低:由于高度的重复计算,导致时间复杂度较高,是 O ( n 2 ) O(n^2) O(n2)。
以下方法没有用到上述的 Height()
函数
//后序
bool _IsBalance(Node* root, int& height)
//用了后序,高度还是重复计数了,所以增加了一个引用参数
{
if (root == nullptr)
{
height = 0;
return true;
}
int leftHeight = 0,rightHeight = 0;
//递归调用 `_IsBalance` 函数,分别检查左子树和右子树的平衡性,同时计算左右子树的高度。
//如果左子树或右子树不平衡,直接返回 `false`。
if (!_IsBalance(root->_left,leftHeight) || !_IsBalance(root->_right, rightHeight))
{
return false;
}
//检查当前节点的平衡性:
//如果左右子树高度差的绝对值大于等于 2,输出当前节点的键值并返回 `false` 表示不平衡。
if (abs(rightHeight - leftHeight) >= 2)
{
cout << root->_kv.first << "不平衡" << endl;
return false;
}
//如果左右子树高度差不等于当前节点的平衡因子 `_bf`,输出当前节点的键值并返回 `false` 表示平衡因子异常。
if (rightHeight - leftHeight != root->_bf)
{
cout << root->_kv.first << "平衡因子异常" << endl;
return false;
}
height = leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
//计算当前节点的高度并通过引用参数 `height` 返回。
return true;
}
bool IsBalance()
{
int height = 0;
return _IsBalance(_root,height);
}
优点:
- 避免重复计算:通过引用参数
height
,避免了对子树高度的重复计算。例如,在遍历左子树和右子树时,会计算子树的高度并通过引用参数传递给父节点,父节点可以直接使用这些高度值,而不需要再次计算。 - 后序遍历:该算法采用后序遍历的方式,确保在处理当前节点之前已经处理完左右子树,从而保证在返回当前节点的高度时,左右子树的高度已经计算完成。
- 高效性:时间复杂度是 O(n),因为每个节点只被遍历一次,每个节点的高度也只被计算一次。
- 平衡因子检查:不仅检查高度差是否在允许范围内,还检查每个节点的平衡因子
_bf
是否正确,确保 AVL 树的完整性。
5.3 数据测试
void TestAVLTree1()
{
int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
AVLTree<int, int> t;
for (auto e : a)
{
if (e == 14)
{
int x = 0;
}
t.Insert(make_pair(e, e));//它会自动推导模板参数的类型
}
t.InOrder();
cout << t.IsBalance() << endl;
}
返回 1
即为确实是 AVL 树
关于调试
1、先看是插入谁导致出现的问题
2、打条件断点,画出插入前的树(打印、日志)
3、单步跟踪,对比图一一分析细节原因