文章目录
- 1. AVL 树概念
- 2. AVL 树节点的定义
- 3. AVL树的插入
- 4. AVL树的旋转
- 4.1 新节点插入较高左子树的左侧---左左:右单旋
- 4.2 新节点插入较高右子树的右侧---右右:左单旋
- 4.3 新节点插入较高左子树的右侧---左右:先左单旋再右单旋
- 4.4 新节点插入较高右子树的左侧---右左:先右单旋再左单旋
- 5. AVL树的性能
- 6. AVL树的面试题
1. AVL 树概念
🍎① 二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化
为单支树
,查
找元素相当于在顺序表
中搜索元素,效率低下
。
🍎② 因此,两位俄罗斯的数学家G.M.A
delson-V
elskii和E.M.L
andis (为什么叫 AVL
树呢 ? ----- 是从这两位科学家的姓名来的) 在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1
(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
🍎③ 一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
- Ⅰ、它的左右子树都是
AVL
树;
Ⅱ、左右子树高度之差(简称平衡因子)的绝对值不超过1
(即:-1、0、1);
注意:
🐧a. 平衡因子不是 AVL树必须需要的,它只是 AVL 树的一种实现方式,平衡因子不是必须要维护的,在操作时也可以直接通过高度函数来算,只不过比较麻烦;
🐧b. 平衡因子 = 右子树的高度 - 左子树的高度。
结论: 如果一棵二叉搜索树是高度平衡的,它就是
AVL
树。如果它有n个结点,其高度可保持在 O ( l o g 2 n ) O(log_2 n) O(log2n) ,搜索时间复杂度O( l o g 2 n log_2 n log2n)。(注意,当 n = 3亿 的时候,二叉树的高度还不到 30,所以极大的提高了搜索效率)
2. AVL 树节点的定义
template<class K, class V>
struct AVLTreeNode {
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent; // AVL树要定义节点的父亲,因为更新需要更新祖先的平衡因子
pair<K, V> _kv;
int _bf; // 该节点的平衡因子
//构造函数,以便初始化
AVLTreeNode(const pair<K, V>& kv)
: _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _bf(0);
{}
};
3. AVL树的插入
🍎① AVL
树就是在二叉搜索树的基础上引入了平衡因子
,因此AVL
树也可以看成是二叉搜索树
。那么
AVL树的插入过程可以分为两步:
🐧Ⅰ. 按照二叉搜索树
的方式插入新节点
🐧Ⅱ. 调整插入节点以及该节点的祖先节点的平衡因子
a. 插入父节点的左边,父节点的平衡因子
-1
;
b. 插入父节点的右边,父节点的平衡因子+1
;
c. 父亲的平衡因子 == 0
的时候,表示父亲所在子树的高度不变,不再需要往上更新,插入结束
;
d. 父亲平衡因子 == 1 or -1
,父亲所在子树高度变了,继续往上更新;
e. 父亲平衡因子== 2 or -2
,父亲所在的子树已经不平衡了,需要旋转处理
;
注意: ❗更新平衡因子的结束条件,要么是当前更新的父亲平衡因子等于0
,要么是当前更新节点是根节点,根节点的父亲的平衡因子是不存在的(因为此时父亲节点为空),即为结束条件。
4. AVL树的旋转
🍎 如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:
4.1 新节点插入较高左子树的左侧—左左:右单旋
🍎 ①
右单旋
:必须要 单纯的满足都是左子树比右子树高的情况 ,不能出现右子树比左子树高的情况;
🍎 ②什么情况下使用右单旋呢 ?
//右单旋
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
// 先稳定老大
parent->_left = subLR;
// 认新主人
if (subLR)
subLR->_parent = parent;
// 将老大变成老二
subL->_right = parent;
// 因为 parent 可能是还有父节点的情况的
Node* ppNode = parent->_parent;
parent->_parent = subL;
if (parent == _root)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
//判断 parent 是 ppNode的左孩子还是右孩子
if (ppNode->_left == parent)
{
ppNode->_left = subL;
}
else
{
ppNode->_right = subL;
}
subL->_parent = ppNode;
}
// 将其平衡因子改变
parent->_bf = subL->_bf = 0;
}
4.2 新节点插入较高右子树的右侧—右右:左单旋
🍎 ① 什么情况下使用左单旋呢 ?
🍎 ② 注意:❗下面这种情况就不是左单旋,因为它不是单纯的右边高(它有左边高的情况
)
- 以下是左单旋的例子:
//左单旋
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
// 先稳定老大,给他派士兵
parent->_right = subRL;
if (subRL != nullptr)
subRL->_parent = parent;
// 老大变成老二
subR->_left = parent;
parent->_parent = subR;
Node* ppNode = parent->_parent;
if (parent == _root)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
// 判断 parent 节点是在 ppNode的右节点还是左节点
if (ppNode->_right == parent)
{
ppNode->_right = subR;
}
else
{
ppNode->_left = subR;
}
subR->_parent = ppNode;
}
parent->_bf = subR->_bf = 0;
}
4.3 新节点插入较高左子树的右侧—左右:先左单旋再右单旋
🍎 ① 大概的思路是将该二叉树变成满足完全
右单旋
的情况。
🍎 ② 什么情况下使用左右双旋呢 ?
// 先进行左单旋,再右单旋
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
// 旋转之前,保存subLR的平衡因子,旋转完成之后,需要根据该平衡因子来调整其他节点的平衡因子
int bf = subLR->_bf;
// 先对30进行左单旋
RotateL(parent->_left);
// 再对90进行右单旋
RotateR(parent);
// // 旋转之前,60的平衡因子可能是-1/0/1,旋转完成之后,根据情况对其他节点的平衡因子进行调整
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.4 新节点插入较高右子树的左侧—右左:先右单旋再左单旋
🍎 ① 什么情况下使用右左双旋呢 ?
// 右左单旋
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)
{
subR->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1)
{
parent->_bf = 0;
subR->_bf = 1;
}
else
{
parent->_bf = 0;
subR->_bf = 0;
}
}
5. AVL树的性能
🐧🐧🐧 AVL
树是一棵绝对平衡的二叉搜索树
,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即
l
o
g
2
(
N
)
log_2 (N)
log2(N)。
🍎🍎🍎 但是如果要对AVL树做一些结构修改
的操作,性能非常低下,比如:插入
(插入的时候 new 出节点也很消耗时间)时要维护其绝对平衡,旋转的次数比较多
,更差的是在删除时,有可能一直要让旋转持续到根的位置。
因此:如果需要一种查询高效
且有序的数据结构,而且数据的个数为静态的
(即不会改变,意思就是不会插入新数据),可以考虑AVL树,但一个结构经常修改,就不太适合。
6. AVL树的面试题
- 🍎① AVL树
插入
或者删除
的时候,其旋转情况?
🐧Ⅰ、插入时,AVL树最多只需要旋转两次。
🐧Ⅱ、删除操作时,可能不止旋转两次,可能需要旋转多次
,子树旋转后,其高度降低了一层,其上层可能也需要跟着旋转。