目录
AVL的产生
二叉搜索树在一定程度上可以提高搜索效率,但是当序列是有序时:如果所示
此时二叉搜索树退化成单链表,搜索效率退化为O(N)。为了解决这个问题科学家引入了AVL树,又称平衡搜索二叉树。
AVL树的概念
AVL简称平衡二叉树。由前苏联的数学家 Adelse-Velskil 和 Landis 在 1962 年提出的高度平衡的二叉树,根据科学家的英文名也称为 AVL 树。换句话说,在需要频繁进行增删查改操作的场景中,AVL 树能始终保持高效的数据操作性能,具有很好的应用价值。它具有如下几个性质:
- 可以是空树。
- 假如不是空树,任何一个结点的左子树与右子树都是平衡二叉树,并且高度之差的绝值不超过 1。
AVL 树既是二叉搜索树,也是平衡二叉树,同时满足这两类二叉树的所有性质,因此是一种平衡二叉搜索树(balanced binary search tree)。
这里再次提一句: AVL树是平衡二叉树,但平衡二叉树不是AVL树!!!
可能你看到很多博客说平衡二叉树就是AVL树,其实这完全是错误的,平衡二叉树是平衡二叉树,AVL树是AVL树,二者有关系但并不是说平衡二叉树就是AVL树。这个要分清楚。
AVL树是由二叉搜索树与平衡二叉树的性质相结合的一种树,包含二叉搜索树与平衡二叉树。所以可以说 AVL树是平衡二叉树,但平衡二叉树不是AVL树!!!
平衡二叉树它的左子树和左子树的高度之差(平衡因子)的绝对值不超过1,且它的左子树和右子树都是一颗平衡二叉树。
现在尝试判断一下,下面那个树不是平衡二叉树
图一:
图二:
图三:
图一,很明显就能看出图1中任何子树的高度差都在没有超过1,
图二,节点25的左子树高度是3,而右子树高度是5,高度差已经超过1;对于节点32而言,左子树高度是3,右子树高度是1,高度差已经超出1,所以该树不是平衡二叉树。
图三,对于节点25的左子树高度是3,而右子树高度是5,高度差已经超过1;对于节点28来说,左子树的高度是0,右指数的高度是2,高度差已经超过1;所以两处不平衡,不是平衡二叉树。
在平衡二叉树的基础上添加上二叉搜索树的性质就成为了AVL树,
即:左子树全部小于根,右子树全部大于根。
AVL树的节点定义
我们这里直接实现KV模型的AVL树。
AVL树与二叉搜索树与众不同之处就是多了旋转这一步骤,然而为了方便后序的旋转操作,将AVL树的结点定义为三叉链结构,(还存在平衡因子,后面解释作用与存在原因)
template<class k, class v>
struct AVLTreeNode
{
//三叉链
AVLTreeNode* _left;
AVLTreeNode* _right;
AVLTreeNode* _parent;
//平衡因子
int _bf;
pair<k, v> _kv;
AVLTreeNode(const pair<k, v>& kv)
: _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
, _kv(kv)
{}
};
AVL树的插入
AVL树的插入也是最为麻烦的,就比如说一个AVL树在插入前符合要求,然而插入后破坏了平衡,但如果我们知道这时会破坏平衡还好,但是坏就坏在不知道,在插入前不知道是左子树深度高还是右子树深度高,这就是麻烦之处。
然而,前面已经提到了AVL树是一种特别的平衡二叉树,平衡二叉树里面就有个概念就是:平衡因子,解决了这一问题。
平衡因子
定义
某节点的左子树与右子树的高度(深度)差即为该节点的平衡因子(BF,Balance Factor),平衡二叉树中不存在平衡因子大于 1或小于-1 的节点。在一棵平衡二叉树中,节点的平衡因子只能取 0 、1 或者 -1 ,分别对应着左右子树等高,左子树比较高,右子树比较高。
有了平衡因子这一说,那么在插入前我都知道是左子树深度高还是右子树深度高了,又因为AVL树又有二叉搜索树的性质,所以我在插入前就可以知道其要被插入的位置,所以说对于插入还是有了一点思路,知道了什么时候会失衡,失衡的大致情况是什么。
举一个例子:
在平衡搜索二叉树中插入一个节点1
树结构变为:
此时平衡就会被打破,那么如果想要调节,从而达到平衡,那么就要想办法使其左子树的深度减少,
对于此又会引入另一个新的概念:
最小失衡树
在新插入的结点向上查找,以第一个平衡因子的绝对值超过 1或小于-1 的结点为根的子树称为最小不平衡子树。也就是说,一棵失衡的树,是有可能有多棵子树同时失衡的。而这个时候,我们只要调整最小的不平衡子树,就能够将不平衡的树调整为平衡的树。
AVL树种解决失衡问题通过旋转最小失衡树来使整棵树达到平衡,旋转的目的就是减少高度,通过降低整棵树的高度来平衡。哪边的树高,就把那边的树向上旋转。
对于此类的调节方法共可以分为四种旋转情况
插入的四种旋转方式
左旋(处理LL型违规)
右旋(处理RR型违规)
左右双旋(处理LR型违规)
右左双旋(处理RL型违规)
单旋
右旋
当插入0后,就会破坏破坏
此时树的平衡性被破坏此时从插入节点往上查到最小失衡树为3,3这个节点不平衡原因是它的左树的高度太高,因此我们只需要对3这个节点进行右旋。
下面的操作用抽象图代替
大致操作为如下:
在一个AVL树种插入一个值在a子树,从而使得平衡被破坏,使得60变为最小失衡树的根节点,我们令60为parent(下面为了简便用P代替)60的左子树为subPL(下面为了简便代替为PL),PL的右子树为subPLR(PLR)。
然后进行如图的操作。
图中的操作共可以分为3步走:
- 将b变为60的左
- 60变为30的右
- 30为新的根
注意:
不要忘记更新平衡因子。
注意要考虑特殊情况,判断一下PL是否在不在。
考虑特殊情况parent是不是为_root根节点,如果是要更新根节点,如果不是要将parent的_parent的孩子指针更新。
代码如下:
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
{
subLR->_parent = parent;
}
subL->_right = parent;
Node* parent_parent = parent->_parent;
parent->_parent = subL;
if (_root == parent)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (parent_parent->_left == parent)
{
parent_parent->_left = subL;
}
else
{
parent_parent->_right = subL;
}
subL->_parent = parent_parent;
}
parent->_bf = subL->_bf = 0;
}
左旋
同样我们用抽象图代替,在c子树下插入,从而破坏平衡,令30为P,30的右为PR,PR的左子树为PRL,大致操作可以分为3步骤:
- b变为30的右
- 30变为60的左
- 60为新根
注意:
不要忘记更新平衡因子。
注意要考虑特殊情况,判断一下PR是否在不在。
考虑特殊情况parent是不是为_root根节点,如果是要更新根节点,如果不是要将parent的_parent的孩子指针更新。
代码如下:
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
subR->_left = parent;
if (subRL)
{
subRL->_parent = parent;
}
Node* parent_parent = parent->_parent;
parent->_parent = subR;
if (_root == parent)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (parent_parent->_left == parent)
{
parent_parent->_left = subR;
}
else
{
parent_parent->_right = subR;
}
subR->_parent = parent_parent;
}
subR->_bf = parent->_bf = 0;
}
双旋
在单旋我们的插入都是在图一的a(进行右旋),或者在图二的c(进行左旋)进行插入后破坏平衡,然后进行单旋从而恢复平衡,他的插入可以说是直线的插入,因此经过一次旋转就可以使其达到平衡。但是如果是折线过来又改怎么旋转呢。这时候就要用双旋了。
就比如说在图一的b插入(进行左右双旋),在图二的b插入(进行右左双旋)。
右左双旋
右左双旋就是在如图的b上插入节点
但是这个插入又分为两种,是在b的左子树与b的右子树部分插入,也就是分为在b1上插入,或在b2上插入,但实际上写代码的时候是不需要这样考虑的,写代码的时候还是一种情况,这里分两种情况是为了方便讲解详细操作步骤,从而使得过程更为好理解。
情况一:在b1上插入
情景二:在b2上插入
双旋重要的是要回更新平衡因子,两次的旋转还是很好解决的,下面我们以情况一来假设模拟一下。在双旋中,我们只需要先对PR进行右单旋,后对P进行左单旋便可以。总的来说还是很简单。
对于平衡因子的调整,我们就需要分情况了,这时候分为三种情况
- subRL自己就是新增
- 插入的节点在b1上
- 插入的节点在b2上
这三种情况来说还是很好解决的
对于第一种就不多说了只需要将P,PR,PRL的平衡因子全等于0即可。
对于情况二
我们把图给出,对于此时
PRL的平衡因子为0,
P的为0
PR为1
对于情况三
我们把图给出,对于此时
PRL的平衡因子为0,
P的为-1
PR为0
最后代码展示:
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(parent->_right);
RotateL(parent);
//调整平衡因子
if (bf == 0)
{
// subRL自己就是新增
parent->_bf = subR->_bf = subRL->_bf = 0;
}
else if (bf == -1)
{
//在b上增加
parent->_bf = 0;
subRL -> _bf = 0;
subR->_bf = 1;
}
else if (bf == 1)
{
subR->_bf = subRL->_bf = 0;
parent->_bf = -1;
}
else
{
assert(false);
}
}
左右双旋
同样左右双旋也是要分两种情况来定,最后调整平衡因子,这一部分代码,就留给你来写了,就不在一步一步来说明了
直接给出代码:
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(parent->_left);
RotateR(parent);
//调整平衡因子
if (bf == 0)
{
// subLR自己就是新增
parent->_bf = subL->_bf = subLR->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 1;
subL->_bf = subLR->_bf = 0;
}
else if (bf == 1)
{
parent->_bf = subLR->_bf = 0;
subL->_bf = -1;
}
else
{
assert(false);
}
}
还有图解:
先对30左旋,后对40右旋。
旋转方式的选择
四种的旋转方式说完了,那么我们就要写代码,判断什么时候要进行那种旋转了,总的来说还是靠平衡因子来选择的。
注意:
插入后的调整要寻找最小失衡树!!!,而不是说插入后的那一小部分树来进行调整。
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)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_kv.first < kv.first)
{
parent = cur;
cur = cur->_right;
}
else
{
return false;
}
}
cur = new Node(kv);
if (parent->_kv.first > kv.first)
{
parent->_left = cur;
cur->_parent = parent;
}
else
{
parent->_right = cur;
cur->_parent = parent;
}
//调整
while (parent)
{
if (parent->_left == cur)
{
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)
{
RotateL(parent);
}
else if (parent->_bf == -2 && cur->_bf == -1)
{
RotateR(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1)
{
RotateRL(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
RotateLR(parent);
}
// 1、旋转让这颗子树平衡了
// 2、旋转降低了这颗子树的高度,恢复到跟插入前一样的高度,
// 所以对上一层没有影响,不用继续更新
break;
}
else
{
break;
}
}
return true;
}
AVL树的删除
AVL的删除是比插入多了平衡调整,比较麻烦,真正实现起来要200-300行的代码,总的来说是插入的代码量的二倍左右,我也不会,这就不多说什么了,但推荐一个文章,里面就有删除:
【数据结构】AVL树的删除(解析有点东西哦)_avl树删除节点-优快云博客
其余函数
find与中序遍历打印
总的来说AVL树到此就结束了,剩下的就是find函数与中序遍历函数,这两个函数与二叉搜索树的实现完完全全相同
void InOrder()
{
_InOrder(_root);
cout << endl;
}
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_kv.first << " ";
_InOrder(root->_right);
}
Node* findLastIndex(K key)
{
Node* pre = _root;//记录前一个节点
Node* cur = _root;
while (cur) {
pre = cur;
if (cur->_kv.first == key) {
break;
}
else if (cur->_kv.first > key) {
cur = cur->_left;
}
else {
cur = cur->_right;
}
}
return pre;
}
检测是否为AVL树
当然还有检查目前的AVL树是不是符合要求
int _Height(Node* root)
{
if (root == nullptr)
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 == nullptr)
{
return true;
}
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
if (rightHeight - leftHeight != root->_bf)
{
cout << root->_kv.first << "平衡因子异常" << endl;
return false;
}
return abs(rightHeight - leftHeight) < 2
&& _IsBalance(root->_left)
&& _IsBalance(root->_right);
}
到这就完了,拜拜