目录
1.什么是AVL树?
2.AVL树的实现
2.1AVL树结点的定义
2.2AVL树的插入
2.2.1插入的步骤
2.2.2插入情况分析
2.2.3旋转操作的分析
2.3AVL树的查找
3.AVL树的验证
4.AVL树的性能分析
1.什么是AVL树?
AVL树其实就是一棵加了限制条件的二叉搜索树。由于二叉搜索树在插入元素为有序or接近有序的情况下,会退化成单支树,查找元素的时间复杂度变成了O(N),相当于在在顺序表中进行查找,那么树形结构的优势就完全体现不出来了, 伟大的计算机前辈觉不允许这种事情发生,于是两位俄罗斯的数学家就发明了AVL树;要想不让二叉搜索树退化成单支树,这两位数学家的想法是 保证每棵树及其子树的左右子树高度差的绝对值不超过1。当左右子树高度差的绝对值超过1,就需要调整了。
所以,AVL树具有以下性质:
- 1.左右子树的高度差的绝对值不超过1;
- 2.左右子树都是AVL树;
- 3.空树也算是AVL树。
满足以上性质的树,能达到一种相对平衡的状态,所以AVL树也是一棵平衡二叉搜索树。
为什么是左右高度差不超过1,不超过0不是更加平衡吗?有些条件下是无法做到左右子树高度差不超过0的,不超过1就是最好的状态。
2.AVL树的实现
数据结构的实现,一般只需实现其增、删、查、改;但AVL树也是一棵二叉搜索树,是不允许修改的,所以我们重点实现其插入操作。
2.1AVL树结点的定义
AVL树的结点可以像下面这样定义:
- _left指向左孩子。
- _right指向右孩子。
- _parent指向父亲。
- _bf是平衡因子,用来记录左右子树的高度差。(平衡因子并不是必须的,但是我们采用平衡因子的方式帮助我们控制树的结构)
- _kv是存储的数据,用一个键值对来存储。
2.2AVL树的插入
2.2.1插入的步骤
AVL树的插入主要分为如下两步
- 1.按二叉搜索树的规则插入;比当前结点大,就去右边插入,比当前节点小,就去左边插入。
- 2.更新平衡因子;插入节点后,必然有树的平衡因子会变化,平衡因子 一旦不满足AVL树的性质,就需要调整了。(调整规则为在父结点的左边插入,父结点的平衡因子 减1,在父结点的右边插入,父结点的平衡因子 加1;如果更新后,父结点的父结点受到影响,还需要向上继续更新平衡因子)
插入节点会影响哪些节点的平衡因子呢?
- 如果在65的左边插入结点,65的平衡因子会发生变化,60的平衡因子也会发生变化,82的平衡因子也会发生变化,58的平衡因子也会发生变化。
- 如果在83的左边插入结点,83的平衡因子会发生变化,82的平衡因子会发生变化
- 所以插入结点会影响新增结点的部分祖先。
2.2.2插入情况分析
(注:下面的讲解用c代表新增结点,用p代表新增结点的父结点,pp代表父结点的父结点)
二叉树插入结点之后,树的形状有很多,但是大致可以划分成三种情况:p->_bf == 0,p->_bf == 1/-1,p->_bf == 2/-2。
- 当插入结点后,p->_bf == 0,说明更新之前 p->_bf == 1/-1,在p的矮的那边插入了结点,p子树变得均衡了,p的高度不变,不会影响pp结点;更新结束。
- 当插入结点后,p->_bf == 1/-1,说明更新之前 p->_bf == 0,在p的一边插入,p变得不均衡了,但是不违反规则,p的高度变了,可能会影响pp结点,需要继续往上更新。(更新之前p的平衡因子不可能是2/-2,因为AVL树的规则不允许出现这种情况,一旦出现平衡因子是2/-2的场景就需要调整)
- 当插入结点后,p->_bf == 2/-2,说明p所在的子树违反了AVL树的规则,需要旋转处理;旋转处理 是将不平衡的AVL树变得平衡的一种手段,让p所在子树的高度回到了插入之前,不会对上层的平衡因子产生影响。
代码如下(旋转逻辑后面有详细代码示例):
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr) // 注意在空树中插入的情况
{
_root = new Node(kv);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur) // 往上更新到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; // 如果存在就不需要插入了
}
}
cur = new Node(kv);
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
cur->_parent = parent;
}
else
{
parent->_left = cur;
cur->_parent = parent;
} // 判断新增的结点在父节点的哪一边插入
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 = cur->_parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
// 旋转处理,让不平衡的二叉搜索树变得平衡。
}
}
return true;
}
2.2.3旋转操作的分析
我们之所以要进行旋转操作,是因为当前的子树出现了左右子树高度差为2/-2的情况,不满足AVL树的规则;所以我们需要降低当前树的高度,使当前这棵不平衡的子树变成平衡的子树。让p所在的子树的高度回到了插入之前,因此不会对上层子树的平衡因子产生影响。
二叉树的插入情况比较复杂,但是大致可以划分成 左单旋、右单旋、左右双旋、右左双旋。我们只需要分清出,什么情况下,使用哪一种旋转方式即可。
- 左单旋:当p所在的子树的平衡因子为2,且c是p的右孩子,c所在的子树的平衡因子为1,此时就可以使用左单旋降低p所在子树的高度。如下图所示:
- 右单旋:当p所在的子树的平衡因子为-2,且c是p的左孩子,c所在的子树的平衡因子为-1,此时就可以使用右单旋降低p所在子树的高度。如下图所示:
- 左右双旋:当p所在的子树的平衡因子为-2,且c是p的左孩子,c所在的子树的平衡因子为1,此时就可以先对c所在的子树进行左单旋,再对p所在的子树进行右单旋。如下图所示:
- 右左双旋:当p所在的子树的平衡因子为2,且c是p的右孩子,c所在的子树的平衡因子为-1,此时就可以先对c所在的子树进行右单旋,再对p所在的子树进行左单旋。如下图所示:
旋转之后,平衡因子是怎么变化的呢?读者可以自己画画图,就可以总结出规律了;
- 左单旋和右单旋都是让原来的p和c所在的子树的平衡因子变为0。
- 左右双旋之后,平衡因子的变化比较复杂,但是可以根据原来c的右孩子的平衡因子的大小来划分情况,如下图所示:
- 右左双旋之后,平衡因子的变化可以根据原来c的左孩子的平衡因子的大小来划分情况,如下图所示:
2.3AVL树的查找
在AVL树中查找一个结点,就相当于在二叉搜索树中查找一个结点,找到了就返回该结点的地址,没找到就返回空指针,代码如下:
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_kv.first < key)
{
cur = cur->_right;
}
else if (cur->_kv.first > key)
{
cur = cur->_left;
}
else
{
return cur;
}
}
return NULL;
}
3.AVL树的验证
当我们实现了一棵简单的AVL树之后,该如何检测该AVL树实现的是否正确呢?
验证是否是合格的AVL树分为两步 1.验证其是否是二叉搜索树,可以通过中序遍历,看其数据是否是升序判断。2.验证其是否符合AVL树的性质,我们不能通过检测所有子树的平衡因子来判断,因为,我们不敢百分之百保证平衡因子的更新是正确的,只能求出每个子树的左右高度差来判断。代码如下:
bool _IsBalance(Node* root, int& height)
{
if (root == nullptr)
{
height = 0;
return true;
}
int leftHeight = 0, rightHeight = 0;
if (!_IsBalance(root->_left, leftHeight)
|| !_IsBalance(root->_right, rightHeight))
{
return false;
}
if (abs(rightHeight - leftHeight) >= 2)
{
cout <<root->_kv.first<<"不平衡" << endl;
return false;
}
if (rightHeight - leftHeight != root->_bf)
{
cout << root->_kv.first <<"平衡因子异常" << endl;
return false;
}
height = leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
return true;
}
bool IsBalance()
{
int height = 0;
return _IsBalance(_root, height);
}
4.AVL树的性能分析
AVL树是对二叉搜索树的优化升级,很好的克服了二叉搜索树在插入数据的顺序为有序 or 接近有序时退化成单支树的问题,保证了查询的时间复杂度为O(log_N),但是其代价就是通过更多的旋转来控制;如果要对AVL树做一些结构上的修改操作,修改操作很有可能会改变子树的平衡因子,从而引发旋转,如果是删除操作的话,可能要旋转到根的位置,效率低下。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。
基于AVL树的缺点,有大佬发明了另一种平衡二叉搜索树 —— AVL树,我们后面讲解。