直击AVL树要害:插入过程详解(大厂面试常考:建议收藏)
前言
AVL树是数据结构中效率比较高的结构,应用非常广泛,相比于顺序表和链表,在一亿个单词查找一个单词平均需要一亿次,AVL树只需要查找几十次,就能快速高效地在一亿个单词获得想要的单词。在实现AVL树数据结构的时候,我们会遇到很多的难题,这一节,我会就AVL树中插入数据过程所会遇到的难题进行详细分析。
本节重点
一、什么是AVL树?
二、AVL树的性质
三、AVL树结点的类型
四、查找
五、重头戏:插入(分类讨论)
一、什么是AVL树?
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序,二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。
因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
二、AVL树的性质
- 它的左右子树都是AVL树
- 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
- 这里需要注意,空树也属于AVL树,所以在实现AVL树的接口的时候,需要记得对空树的情况进行判断
示意图
上图就是一棵AVL树
三、AVL树结点的类型(K_Val模型)
//结点类型
template <class K,class V>
struct 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)
{
//新节点由于没有左右子树,因此左右高度肯定是平衡的,那么平衡因子自然就是0
}
};
- 三叉链模型就是有三个指针,方便在使用的过程中可以很直接地找到当前结点的左孩子和右孩子和父亲结点
- K_Val模型:使用比Key模型好,其中的数据域是使用一个
pair<K,V>
类型,pair<K,V>
本质是一个结构体,里面包含有first(key)
和second(value)
,应用比较广泛,value
是一个与key
有一定关系的数据
四、查找
查找操作和二叉搜索树非常相似
代码示例
bool Find(const pair<K,V>& kv)//插入一个kv类型的数据
{
//const:对kv进行保护,防止kv被改变
//&:减少拷贝
Node* cur = _root;
while (cur)
{
if (cur->_kv.first < kv.first)
{
cur = cur->_right;
}
else if (cur->_kv.first > kv.first)
{
cur = cur->_left;
}
else
{
return true;
}
}
return false;
}
注意:
- 这里可以不对树进行判空操作,因为,如果树为空,那么
_root = nullptr
,则cur = nullptr
,那么下面的循环就不会进去,函数会直接返回false
- 在比较的过程中不是比较
kv
的大小,而是比较kv
中的first
,因为kv
中的first
才是每个结点的关键字
五、重头戏:插入(分类讨论)
- 按照二叉搜索树的原则进行插入
if (_root == nullptr)
{
//根为空:空树
_root = new Node(kv);
_root->_bf = 0;
return true;
}
//非空:找到插入的位置
//(利用二叉搜索树中讲到的典型的遍历方式:双指针法)
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (cur->_kv.first < kv.first)//注意比较的是kv中的first
{
parent = cur;
cur = cur->_right;
}
else if (cur->_kv.first > kv.first)//注意比较的是kv中的first
{
parent = cur;
cur = cur->_left;
}
else
{
//数据已经存在,不支持插入
return false;
}
}
//到这里,说明找到了插入的位置,就是cur
cur = new Node(kv);
cur->_bf = 0;
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;
在继续下面的操作之前,首先需要学会几种常见的情况及对应的旋转方式
情况一:当新插入的结点的位置是在较高左树的左侧时,采用右旋
如图
在右旋的过程中需要记录几个重要的结点:ppNode
,subL
,subLR
(如图)
一般情况的旋转过程:(右旋)
分析:
- 将b给60结点的左边
- 将60结点给30的右边
这里需要注意两个问题:
- 当前的
parent
可能为根节点,也可能为子树中的结点subLR
可能为空,可能不为空
代码实现
void RotateR(Node* parent)
{
//1.记录关键的结点
Node* ppNode = parent->_parent;
Node* subL = parent->_left;
Node* subLR = subL->_right;
//2.处理parent和subLR的关系
parent->_left = subLR;
if (subLR)//这里一定要判断subLR是否为空,如果为空则不需要处理
//否则,就对空指针指向的内容进行非法访问了
{
subLR->_parent = nullptr;
}
//3.处理parent和subL的关系
subL->_right = parent;
parent->_parent = subL;
//4.在处理subL->_parent的时候,需要判断原来的parent是否为根
if (ppNode == nullptr)
{
_root = subL;
_root->_parent = nullptr;
//_root->_parent = ppNode;此时ppNode = nullptr
}
else
{
// 5.判断原来的parent是ppNode的左孩子还是右孩子
if (ppNode->_left == parent)
{
ppNode->_left = subL;
}
else
{
ppNode->_right = subL;
}
subl->_parent = ppNode;
}
}
平衡因子的更新
parent->_bf = subL->_bf = 0;
情况二:当新插入的结点的位置是在较高右树的右侧时,采用左旋(如图)
一般情况的左旋
分析:
- 将b给30的右边
- 将30给60的左边
在左旋的过程中需要记录几个重要的结点:ppNode
,subR
,subRL
这里需要注意两个问题:
- 当前的
parent
可能为根节点,也可能为子树中的结点subRL
可能为空,可能不为空
代码实现
void RotateL(Node* parent)
{
//1.记录关键结点
Node* ppNode = parent->_parent;
Node* subR = parent->_right;
Node* subRL = subR->_left;
//2. 处理parent和subRL的关系
parent->_right = subRL;
if (subRL)
{
subRL->_parent = parent;
}
//3. 处理parent和subR的关系
subR->_left = parent;
parent->_parent = subR;
//在更新subR->_parent时,需要判断原来的parent是否为根
if (ppNode == nullptr)
{
//如果为根,则需要更新根的情况
_root = subR;
_root->_parent = nullptr;
//_root->_parent = ppNode;
}
else
{
if (ppNode->_left == parent)
{
ppNode->_left = subR;
}
else
{
ppNode->_right = subR;
}
subR->_parent = ppNode;
}
}
平衡因子的更新
parent->_bf = subR->_bf = 0;
情况三:在较高的左子树的右侧插入新节点(双旋:左旋+右旋)
这种情况则需要先对左子树进行左旋,转化成左子树比右子树高的情况,再对整棵树进行右旋,这里主要是要对subLR->_bf
的情况进行分类以更新后续的平衡因子
- 向上面的图这样的情况,
subLR
为新插入的结点,则其平衡因子为0 subLR
不是新插入的结点,新插入的结点的位置是subLR
的左边,subLR->_bf = -1
subLR
不是新插入的结点,新插入的结点的位置是subLR
的右边,subLR->_bf = 1
一般情况的旋转过程:
代码实现
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(subL);
RotateR(parent);
//更新平衡因子
if (bf == 0)
{
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 1;
subL->_bf = 0;
subLR->_bf = 0;
}
else if (bf == 1)
{
parent->_bf = 0;
subL->_bf = -1;
subLR->_bf = 0;
}
else
{
assert(false);
}
}
情况四:在较高的右子树的左侧插入新节点(双旋:右旋+左旋)
- 最简单的情况:
subRL
不存在,在该位置插入新节点,插入后subRL->_bf = 0
subRL
不是新插入的结点,新插入的结点的位置是subRL
的左边,subRL->_bf = -1
subRL
不是新插入的结点,新插入的结点的位置是subRL
的右边,subRL->_bf = 1
处理方法:先对右子树进行右旋,转化为整棵树的右子树比左子树高的情况,再对整棵树进行左旋
代码实现
void RotateRL(Node* parent)
{
//记录关键的结点
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
//旋转
RotateR(subR);
RotateL(parent);
//分类讨论
if (bf == 0)
{
parent->_bf = 0;
subR->_bf = 0;
subRL->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 0;
subRL->_bf = 0;
subR->_bf = 1;
}
else if (bf == 1)
{
parent->_bf = -1;
subR->_bf = 0;
subRL->_bf = 0;
}
else
{
assert(false);
}
}
调整
代码实现与分析
while (parent)
{
//调整平衡因子
if (cur == parent->_left)
{
//插入左树,平衡因子--
parent->_bf--;
}
else
{
//插入右树,平衡因子++
parent->_bf++;
}
if (parent->_bf == 0)
{
//高度不变,不需要进行调整,退出循环
break;
}
else if (parent->_bf == -1 || parent->_bf == 1)
{
//出现1或者-1,只有可能是0变成1或者0变成-1
//也就是说原来的左右高度是一样的,平衡因子为0
//插入左树后,平衡因子变成-1
//插入右树后,平衡因子变成1
//需要继续向上进行调整
cur = parent;
parent = cur->_parent;
}
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)
{
RotateLR(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1)
{
RotateRL(parent);
}
else
{
assert(false);
}
break;
}
}