一、AVL树概念
AVL树出现在二叉搜索树之后,弥补了二叉搜索树的一些缺点。
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
- 它的左右子树都是AVL树
- 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
但是,AVL树不一定都有平衡因子,因为平衡因子只是用来检测平衡的方式,如果有其他检测的方法,也可以将平衡因子弃之不用。
例:
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 log2n,搜索时间复杂度O(log2n)。
二、AVL树节点的定义
一个AVL树的节点和二叉搜索树类似,不同的是AVL树采用三叉链的结构更方便控制(即子节点也有一个指向父节点的指针)。但这个和平衡因子一样,不是必须要求的,也可采用其他方法,只要能完成相同的操作即可(本篇文章采用三叉链进行讲解)。
另外,对AVL树中存储的数据,这里采用pair<K,V>的方式,可以适用于更多的场景。
代码如下:
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)
, _bf(0)//初始时,该节点没有左右孩子,平衡因子为0
, _kv(kv)
{}
};
AVL树类模板初始如下:
template<class K, class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
AVLTree()
:_root(nullptr)
{}
private:
Node* _root;
};
三、AVL树的插入
AVL树的插入分为三步:
- 像二叉搜索树一样按照左孩子小于父节点,右孩子大于父节点的规则插入节点
- 检查每一个节点的平衡因子绝对值是否小于2
- 若平衡因子绝对值大于1,则进行旋转
插入节点:
若树为空树,直接将新增节点作为根节点。否则再依次向下比较节点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)
{//比较新增节点和当前节点的key值,确定将其放在左/右
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;
}
}
//走到这里时表明已经找到合适的节点作为新增节点的父节点
cur = new Node(kv);
if (parent->_kv.first < kv.first)
{//将新增节点与当前节点链接时不要忘记处理新增节点的父节点指针
parent->_right = cur;
cur->_parent = parent;
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
}
但是,就上图而言,很明显已经不是一棵AVL树了,因为它的节点中出现了平衡因子不小于2的情况。所以为了保证每插入一个节点之后,AVL树结构不被破坏,就需要检查节点的平衡因子。
本篇文章中的平衡因子为右子树高度-左子树高度
又因为一个节点的平衡因子与左右子树有关,所以新增一个节点之后,只会影响新增节点的祖先节点的平衡因子,也就是说,只需要检测新增节点到根节点所在路径中的节点平衡因子。
更新平衡因子的详细步骤:
- 首先更新新增节点的父节点平衡因子
如果新增节点为父节点的左孩子,则父节点平衡因子-1;若为右孩子,则父节点的平衡因子+1。
- 然后向上更新父节点的祖先节点的平衡因子
这里有三种情况:
①插入新增节点后,父节点平衡因子为0。则说明插入之前父节点已经有一个孩子,插入后只是新增一个孩子,以父节点为根的子树高度不变,也就不会影响祖先的平衡因子,则更新结束。
②插入新增节点后,父节点平衡因子为1或-1。则说明更新前父节点平衡因子为0,更新后,父节点的左右子树其中之一的高度+1。这时必定会影响到父节点的祖先节点的平衡因子,所以需要继续向上更新。
③插入新增节点后,父节点平衡因子为2或-2,这时父节点为根的子树已经不平衡了,需要进行旋转(下文讲解)。
上面三种情况如图:
注意,这里最坏情况下,会一直更新到根节点。
代码实现如下:
while (parent)
{
if (cur == parent->_left)
parent->_bf--;
else
parent->_bf++;
if (parent->_bf == 0)
{
break;
}
else if (parent->_bf == 1 || parent->_bf == -1)
{
// 继续往上更新
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
// 旋转处理
}
else
{
// 说明插入更新平衡因子之前,树中平衡因子就有问题了
assert(false);
}
}
四、AVL树的旋转
当一棵树中某一棵子树的根节点的平衡因子绝对值大于1时,为了保证AVL树结构,就必须进行旋转操作。而旋转又分为四种----左单旋、右单旋、左右双旋和右左双旋。下面一 一介绍:
右单旋:
当某一个节点的平衡因子为-2且子节点平衡因子为-1时,表示左子树的高度比右子树大2,为了平衡,就必须进行右单旋,如下:
这样既保证了平衡因子满足要求,又保证了搜索树的特性,而且这棵树的高度完成了-1。
注意,需要调整的不一定是整棵树,也可能是某一棵子树,这种情况下,调整子树不会破坏整棵树的平衡。
代码如下:
if (parent->_bf == -2 && cur->_bf == -1)
{
RotateR(parent);
}
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)//不能将空节点的父节点赋值
subLR->_parent = parent;
//记录父节点的父节点,方便为后续subL的父节点赋值
Node* parentParent = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (parent == _root)
{//父节点为根,则subL变为新的根
_root = subL;
_root->_parent = nullptr;
}
else
{
if (parentParent->_left == parent)
parentParent->_left = subL;
else
parentParent->_right = subL;
subL->_parent = parentParent;
}
//别忘了调整subL和parent的平衡因子
subL->_bf = parent->_bf = 0;
}
左单旋:
类比右单旋,左单旋就是一个节点的平衡因子为2且子节点平衡因子为1的情况,如图:
这里就不粘贴代码了,在文章最后会给出完整代码。
双旋(左右/右左):
当一棵树的子树的左子树的高度大于右子树,且这棵子树的子树的右子树高度大于左子树时(相反情况也适用),就需要用到双旋,如图:
这种情况就需要用到左右双旋,直接复用左单旋和右单旋即可。
void RotateLR(Node* parent)
{
RotateL(parent->_left);
RotateR(parent);
}
至于右左双旋类比即可。
但这样还不够,因为在某些场景下,有些节点的平衡因子会出错,这种情况下文再详谈。
五、AVL树的验证
上文中,我们已经实现了AVL树的插入,那么该怎么验证这个插入的过程没有错误呢?
答:需要对整棵树计算出左子树和右子树的高度,判断它们的差的绝对值是否小于2。
注意,这里计算的过程应该包含每一棵子树。不用平衡因子检测是因为平衡因子本身可能出错。
代码如下:
bool IsBalance()
{
return _IsBalance(_root);
}
int Height(Node* root)
{
if (root == NULL)
return 0;
int leftHeight = Height(root->_left);
int rightHeight = Height(root->_right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
bool _IsBalance(Node* root)
{
if (root == NULL)
return true;
// 对当前树进行检查
int leftHeight = Height(root->_left);
int rightHeight = Height(root->_right);
if (rightHeight - leftHeight != root->_bf)
{//检查平衡因子是否有误
cout << root->_kv.first << "现在是:" << root->_bf << endl;
cout << root->_kv.first << "应该是:" << rightHeight - leftHeight << endl;
return false;
}
return abs(rightHeight - leftHeight) < 2
&& _IsBalance(root->_left)
&& _IsBalance(root->_right);
}
事实是上面的实现AVL树的代码确实会导致一些情况下,某些平衡因子出错的情况。
而且错误是出现在双旋的过程中的,该过程中需要控制平衡因子。在双旋中,把一些节点的平衡因子置0,但经过两次旋转后,它们的平衡因子可能并不为0。
例:
可以看到,这里双旋之后,3的平衡因子为-1,但原代码处理后,3的平衡因子为0,因为3在第一次左旋的时候,已经将其平衡因子置零了。
上面这种情况是左右双旋,而当右左双旋时,结果就会是右边的节点的平衡因子变为1。
当h=0,6为新增节点时,双旋后平衡因子都为0(具体过程可以通过画图观察一下)。
那么怎么区分这三种情况,并做出改正呢?
可以看到,这三种情况跟旋转前的6的平衡因子有关(6的平衡因子为1、-1和0的情况)。所以,在旋转之前记录6的平衡因子,再对旋转后的6的左/右节点的平衡因子进行改正即可。
这里只给出左右双旋的图示,右左双旋可以类比,总结如下:
- 旋转前 sunLR:-1;旋转后 parent:1,subL:0,subLR:0
- 旋转前 subLR:1;旋转后 parent:0,subL:-1,subLR:0
- 旋转前 subLR:0;旋转后 parent:0,subL:0,subLR:0
下面给出完整的代码:
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)
, _bf(0)//初始时,该节点没有左右孩子,平衡因子为0
, _kv(kv)
{}
};
template<class K, class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
AVLTree()
:_root(nullptr)
{}
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{//根节点为空,直接将新增节点作为根节点
_root = new Node(kv);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{//比较新增节点和当前节点的key值,确定将其放在左/右
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;
}
}
//走到这里时表明已经找到合适的节点作为新增节点的父节点
cur = new Node(kv);
if (parent->_kv.first < kv.first)
{//将新增节点与当前节点链接时不要忘记处理新增节点的父节点指针
parent->_right = cur;
cur->_parent = parent;
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
// 控制平衡
// 1、更新平衡因子 -- 新增节点到根节点的祖先路径
// 2、出现异常平衡因子,那么需要旋转平衡处理
while (parent)
{
if (cur == parent->_left)
parent->_bf--;
else
parent->_bf++;
if (parent->_bf == 0)
{
break;
}
else if (parent->_bf == 1 || parent->_bf == -1)
{
// 继续往上更新
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
// 旋转处理
// 右单旋
if (parent->_bf == -2 && cur->_bf == -1)
{
RotateR(parent);
}
else if (parent->_bf == 2 && cur->_bf == 1) // 左单旋
{
RotateL(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;
}
else
{
// 说明插入更新平衡因子之前,树中平衡因子就有问题了
assert(false);
}
}
return true;
}
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL)
{
subRL->_parent = parent;
}
Node* parentParent = parent->_parent;
subR->_left = parent;
parent->_parent = subR;
if (_root == parent)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (parentParent->_left == parent)
parentParent->_left = subR;
else
parentParent->_right = subR;
subR->_parent = parentParent;
}
subR->_bf = parent->_bf = 0;
}
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)//不能将空节点的父节点赋值
subLR->_parent = parent;
//记录父节点的父节点,方便为后续subL的父节点赋值
Node* parentParent = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (parent == _root)
{//父节点为根,则subL变为新的根
_root = subL;
_root->_parent = nullptr;
}
else
{
if (parentParent->_left == parent)
parentParent->_left = subL;
else
parentParent->_right = subL;
subL->_parent = parentParent;
}
//别忘了调整subL和parent的平衡因子
subL->_bf = parent->_bf = 0;
}
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(parent->_left);
RotateR(parent);
if (bf == 1)
{
parent->_bf = 0;
subL->_bf = -1;
subLR->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 1;
subL->_bf = 0;
subLR->_bf = 0;
}
else if (bf == 0)
{
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
}
else
{
assert(false);
}
}
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(parent->_right);
RotateL(parent);
if (bf == 1)
{
parent->_bf = -1;
subR->_bf = 0;
subRL->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 0;
subR->_bf = 1;
subRL->_bf = 0;
}
else if (bf == 0)
{
parent->_bf = 0;
subR->_bf = 0;
subRL->_bf = 0;
}
else
{
assert(false);
}
}
bool IsBalance()
{
return _IsBalance(_root);
}
int Height(Node* root)
{
if (root == NULL)
return 0;
int leftHeight = Height(root->_left);
int rightHeight = Height(root->_right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
bool _IsBalance(Node* root)
{
if (root == NULL)
return true;
// 对当前树进行检查
int leftHeight = Height(root->_left);
int rightHeight = Height(root->_right);
if (rightHeight - leftHeight != root->_bf)
{
cout << root->_kv.first << "现在是:" << root->_bf << endl;
cout << root->_kv.first << "应该是:" << rightHeight - leftHeight << endl;
return false;
}
return abs(rightHeight - leftHeight) < 2
&& _IsBalance(root->_left)
&& _IsBalance(root->_right);
}
private:
Node* _root;
};
六、AVL树的性能
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log2N 。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:
插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。
因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。
七、总结
本篇文章主要是介绍了AVL树的插入,对其中的四种旋转方法做了详解。
重中之重要注意区分什么时候该用哪一种合适的旋转方法(根据节点的平衡因子判断),以及双旋之后该对哪些节点的平衡因子做出调整(不需要死记硬背,记住其中一种双旋结果,另外的就可以类比了)。
此外,本篇文章中并没有少搜索、修改以及删除的方法。因为搜索和修改比较简单,类比搜索树即可。而删除操作还是需要进行平衡因子的检查以及相应的旋转操作,可以类比插入。
另外,本篇文章中AVL树中的数据是pair<int,int>型的,可以适用于更多场景。读者可以根据自身需要,将数据类型改为int等。
本篇完,青山不改,绿水长流!