目录
AVL树的概念
AVL树结点的定义
AVL的插入
AVL树的旋转
左单旋
右单旋
左右双旋
右左双旋
AVL树的查找
AVL树的概念
二叉搜索树的缺点:
当构建二叉搜索树的数据有序或接近有序时二叉搜索树会退化为单链表。例如,当插入数据1,2,3,4,5,6,7,10,9,8时。会的到以下搜索二叉树。
在此二叉树中查找数据就相当于在单链表中查找数据,效率非常低下。
为了解决当构建二叉搜索树的数据有序或接近有序时二叉搜索树会退化为单链表的问题,在1962年两位俄罗斯数学家G.M. Adelson-Velsky和E.M. Landis提出通过旋转操作来使树保持二叉树的任何节点的两棵子树的高度最大差别为1,即任意节点的左右子树高度差的绝对值不超过1。
并命名为AVL树。AVL树是一种自平衡二叉搜索树。它的特点是为了保持这个平衡特性,在插入或删除节点时,AVL树会通过旋转操作来使树平衡。这种自平衡机制使得AVL树的查找、插入、删除等操作的时间复杂度始终保持在O(log n)级别。
AVL树是在搜索二叉树中增加特性,它仍然满足搜索二叉树性质。
对平衡因子(balance factor)的理解:
AVL树可以是一颗空树也可以是一棵具有以下性质的一颗二叉搜索树:
1.树的左右子树都是AVL树。
2.树的左右子树的高度差(平衡因子)的绝对值不超过1(-1/0/1)。
距离插入结点最近的,且平衡因子的绝对值大于1的结点为根的子树,我们称为最小不平衡子树。
平衡二叉树的实现原理:
平衡二叉树构建的基本思想就是在构建二叉排序树的过程中,每当插入一个结点时,更新平衡因子,检查是否因插入而破坏了树的平衡性,若是,则找出最小不平衡子树。在保持二叉排序树特性的前提下,调整最小不平衡子树中各结点之间的连接关系,进行相应的旋转,使之成为新的平衡子树。
总结:
1、插入新结点。
2、更新平衡因子。
3、判断是否平衡。
4、不平衡则进行旋转操作,再次更新平衡因子。
AVL树结点的定义
我们将树的结点定义为三叉链结构,方便实现回溯更新结点的平衡因子,并定义平衡因子为右子树高度-左子树高度。
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)
{}
AVL的插入
AVL树的插入与搜索二叉树的插入一样,从根结点开始按照以下方法找到待插入位置。有以下三个步骤:
待插入结点的key值比当前结点小就插入到该结点的左子树。
待插入结点的key值比当前结点大就插入到该结点的右子树。
待插入结点的key值与当前结点的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)//新增结点key值大于当前结点的key值往左子树去找待插入的位置
{
parent = cur;
cur = cur->_left;//往左子树去找待插入的位置
}
else if (cur->_kv.first > kv.first)//新增结点key值小与当前结点的key值往右子树去找待插入的位置
{
parent = cur;
cur = cur->_right;//往右子树去找待插入的位置
}
else//key值冗余
{
return false;
}
}
//待插入结点的key值大于parent的key值
//则往右插入
//待插入结点的key值小于parent的key值
//则往左插入
cur = new Node(kv);
if (parent->_kv.first>cur->_kv.first)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;//将新增结点与父结点关联
//更新平衡因子
while (parent)
{
if (cur == parent->_right)//新增结点插入父结点的右边
{
parent->bf++;
}
else//新增结点插入父结点的左边
{
parent--;
}
if (parent->_bf == 1 || parent->_bf == -1)//继续往上判断更新
{
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 0)//左右子树一样高 不再更新
{
break;
}
else if (parent->_bf == 2 || parent->_bf == -2)//平衡因子的绝对值超过1 需要进行旋转处理
{
if (parent->_bf == 2 && cur->_bf == 1)//左单旋处理
{
//将以parent结点为根结点的最小不平衡子树进行左单旋处理
RotateL(parent);
}
if (parent->_bf == -2 && cur->_bf == -1)//右单旋处理
{
//将以parent结点为根结点的最小不平衡子树进行右单旋处理
RotateR(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1)//右左双旋处理
{
//将以parent结点为根结点的最小不平衡子树进行左旋处理
RotateRL(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1)//右左双旋处理
{
//将以parent结点为根结点的最小不平衡子树进行左旋处理
RotateLR(parent);
}
else//出现其他情况 就是在插入新增结点前就出现错误
{
assert(false);
}
}
}
return true;
}
平衡因子的更新
若结点插入成功后,新增结点只会的对祖先的平衡因子有影响,但一个结点的平衡因子是否要更新,与该结点的左右子树的高度有关,所以并不是每次新增结点后,所有的祖先都要更新平衡因子。
如图:
新增结点使结点7的右子树增高1,所以结点7的平衡因子更新为1-0=1;
新增结点使结点8的左子树增高1,所以结点8的平衡因子更新为2-2=0;
新增结点并没有使结点6的左右子树增高,所以结点6的平衡因子不更新;
所以我们插入结点后需要倒着往上更新平衡因子,更新规则如下:
新增结点在parent的右边,parent的平衡因子++。
新增结点在parent的左边,parent的平衡因子−− 。
每更新完一个结点的平衡因子后,都需要进行以下判断:
如果parent的平衡因子等于-1或者1,表明还需要继续往上更新平衡因子,直到根结点。
若parent更新后的平衡因子为-1或1,则只有0经过−−/++操作后会变成-1/1,说明新结点的插入使得parent的左子树或右子树增高了,即改变了以parent为根结点的子树的高度,从而会影响parent的父结点的平衡因子,因此需要继续往上更新平衡因子。
如果parent的平衡因子等于0,表明无需继续往上更新平衡因子了。
若parent更新后的平衡因子为0,只有-1/1经过++/−−操作后会变成0,说明新结点插入到了parent左右子树当中高度较矮的一棵子树,插入后使得parent左右子树的高度相等了,此操作并没有改变以parent为根结点的子树的高度,从而不会影响parent的父结点的平衡因子,因此无需继续往上更新平衡因子。
如果parent更新后的平衡因子等于-2或者2,表明此时以parent结点为根结点的子树已经不平衡了,需要进行旋转处理。
此时parent结点的左右子树高度之差的绝对值已经超过1了,不满足AVL树的要求,因此需要进行旋转处理。
注意:我们将树的结点定义为三叉链结构是为了方便通过cur=parent;parent=parent->_parent;进行回溯更新平衡因子。当parent的平衡因子为2/-2时,那么上一次更新的结点的平衡因子一定不为0,因为如果为0的话就会在上一次更新完成后停止更新,而新增结点的父结点的平衡因子更新后一定为-1/0/1,而不可能是2/-2,因为新增结点只会插入到一个空树中去,在新增结点插入前其父结点的状态有以下几种可能:
1、父结点是一个左右子树均为空的叶子结点,其平衡因子是0,新增结点插入后其平衡因子更新为-1/1。
2、父结点是一个左子树为空的结点,其平衡因子是1,新增结点插入到其父结点的空子树当中,使得其父结点左右子树当中较矮的一棵子树增高了,新增结点后其平衡因子更新为0。3、父结点是一个右子树为空的结点,其平衡因子是-1,新增结点插入到其父结点的空子树当中,使得其父结点左右子树当中较矮的一棵子树增高了,新增结点后其平衡因子更新为0。
综上所述,当parent的平衡因子为-2/2时,cur的平衡因子必定是-1/1而不会是0。
AVL树的旋转
如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构, 使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:
所以我们可以将旋转处理分为以下四类:
1、当parent的平衡因子为-2,cur的平衡因子为-1时,进行右单旋。
2、当parent的平衡因子为-2,cur的平衡因子为1时,进行左右双旋。
3、当parent的平衡因子为2,cur的平衡因子为-1时,进行右左双旋。
4、当parent的平衡因子为2,cur的平衡因子为1时,进行左单旋。
左单旋
在插入前,AVL树是平衡的,新节点插入到60的右子树(注意:此处不是右孩子)中,60右子树增加了一层,导致以30为根的二叉树不平衡,要让30平衡,只能将30右子树的高度减少一层,左子树增加一层,即将右子树往上提,这样30转下来,因为30比60小,只能将其放在60的左子树,而如果60有左子树,左子树根的值一定大于30,小于60,只能将其放在30的右子树,旋转完成。
更新节点的平衡因子即可。在旋转过程中,有以下几种情况需要考虑:
1. 60节点的左孩子可能存在,也可能不存在。
2. 30可能是根节点,也可能是子树,如果是根节点,旋转完成后,要更新根节点; 如果是子树,可能是某个节点的左子树,也可能是右子树。
操作步骤:
1、将subRL链接称为parent的右子树。
2、让parent成为subR的左子树。
3、让subR成为这颗最小不平衡子树的根。
4、更新平衡因子。
左单旋代码如下:
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* pparent = parent->_parent;
// 1、将subRL链接称为parent的右子树。
parent->_right = subRL;
if (subRL)
{
subRL->_parent = parent;
}
//2、让parent成为subR的左子树。
subR->_left = parent;
parent->_parent = subR;
//3、让subR成为这颗最小不平衡子树的根。
if (pparent == nullptr)//parent为根结点
{
_root = subR;//更新subR为根结点
_root->_parent = nullptr;
}
else//parent不为根结点
{
if (pparent->_left == parent)
{
subR->_parent==pparent;
pparent->_left = subR;
}
else
{
subR->_parent == pparent;
pparent->_right = subR;
}
}
//4、更新平衡因子。
subL->_bf = parent->_bf = 0;
}
右单旋
在插入前,AVL树是平衡的,新节点插入到30的左子树(注意:此处不是左孩子)中,30左子树增加了一层,导致以60为根的二叉树不平衡,要让60平衡,只能将60左子树的高度减少一层,右子树增加一层,即将左子树往上提,这样60转下来,因为60比30大,只能将其放在30的右子树,而如果30有右子树,右子树根的值一定大于30,小于60,只能将其放在60的左子树,旋转完成。
更新节点的平衡因子即可。在旋转过程中,有以下几种情况需要考虑:
1. 30节点的右孩子可能存在,也可能不存在。
2. 60可能是根节点,也可能是子树,如果是根节点,旋转完成后,要更新根节点; 如果是子树,可能是某个节点的左子树,也可能是右子树。
操作步骤:
1、将subRL链接称为parent的右子树。
2、让parent成为subR的左子树。
3、让subR成为这颗最小不平衡子树的根。
4、更新平衡因子。
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
Node* pparent = parent->_parent;
//1、将subRL链接称为parent的右子树。
parent->_left = subLR;
if (subLR)
{
subRL->_parent = parent;
}
//2、让parent成为subR的左子树。
subL->_right = parent;
parent->_parent = subL;
//3、让subR成为这颗最小不平衡子树的根。
if (pparent==nullptr)
{
_root = subL;
_root->_parent = nullptr;
}
else
{
if (pparent->_left == parent)
{
pparent->_left = subL;
subL->_parent = pparent;
}
if (pparent->_right == pparent)
{
pparent->_right = subL;
subL->_parent = pparent;
}
}
//4、更新平衡因子。
subL->_bf = parent->_bf = 0;
}
左右双旋
右左双旋的步骤:
1、将以subL为根结点的子树进行左单旋。
2、将以parent为根结点的不平衡子树进行右单旋。
3、更新平衡因子。
经过左右双旋转,需要更新平衡因子,平衡因子的更新需要根据新增结点的插入的为位置情况进行区分:
1、当新增结点插入subLR的右子树,subLR原始平衡因子是1时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、-1、0。
2、当新增结点插入subLR的左子树时,左右双旋后parent、subL、subLR的平衡因子分别更新为1、0、0。
3、当subLR就是新增结点时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、0、0。
左右双旋的代码:
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(subL);
RotateR(parent);
if (bf == 1)
{
parent->_bf = 0;
subLR->_bf = 0;
subL->_bf = -1;
}
else if (bf == -1)
{
parent->_bf == 1;
subLR->_bf = 0;
subL->_bf=0
}
else if (bf == 0)
{
parent->_bf = 0;
subLR->_bf = 0;
subL->_bf = 0
}
else
{
assert(false);
}
}
右左双旋
右左双旋的步骤:
1、将以subR为根结点的子树进行右单旋。
2、将以parent为根结点的不平衡子树进行左单旋。
3、更新平衡因子。
经过右左双旋转,需要更新平衡因子,平衡因子的更新需要根据新增结点的插入的为位置情况进行区分:
1、当新增结点插入subRL的右子树,subRL原始平衡因子是1时,左右双旋后parent、subR、subRL的平衡因子分别更新为-1、0、0。
2、当新增结点插入subRL的左子树,subRL原始平衡因子是-1时,左右双旋后parent、subR、subRL的平衡因子分别更新为0、-1、0。
3、当subRL就是新增结点时,左右双旋后parent、subL、subRL的平衡因子分别更新为0、0、0。
右左双旋代码:
//右左双旋
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL-> = subR->_left;
int bf = subRL->_bf;
// 1、将以subR为根结点的子树进行右单旋。
RotateRL(subR);
//2、将以parent为根结点的不平衡子树进行左单旋。
RotateRL(parent);
if (bf == 1)
{
parent->_bf = -1;
subRL->_bf = 0;
subR->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 0;
subRL->_bf = 0;
subR->_bf = 1;
}
else if (bf == 0)
{
parent->_bf = 0;
subRL->_bf = 0;
subR->_bf = 0;
}
else
{
assert(false);
}
}
AVL树的查找
与二叉搜索树的查找方式一样AVL树的查找逻辑如下:
从根结点开始,若树为空树,则树中没有key值查找失败,返回nullptr。
若key值小于当前结点的值,则往当前结点的左子树当中进行查找。
若key值大于当前结点的值,则往当前结点的右子树当中进行查找。
若key值等于当前结点的值,则查找成功,返回对应结点。
//AVL树的查找函数
Node* Find(const K& key)
{
//从根结点开始,若树为空树,
Node* cur = _root;
while (cur)
{
if (key < cur->_kv.first) //若key值小于当前结点的值,则往当前结点的左子树当中进行查找。
{
cur = cur->_left; //在该结点的左子树当中查找
}
else if (key > cur->_kv.first) //若key值大于当前结点的值,则往当前结点的右子树当中进行查找。
{
cur = cur->_right; //在该结点的右子树当中查找
}
else //若key值等于当前结点的值,则查找成功,返回对应结点。
{
return cur; //返回该结点
}
}
return nullptr; //树中没有key值查找失败,返回nullptr。
}