AVL树的概念
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
简单来说,就是一颗二叉搜索树,这颗树每一个节点的左右子树高度差的绝对值都不能超过1且都是AVL树
这样结构的好处就是,我们搜索起来它的时间复杂度是O(logN)
节点
AVL树与之前碰见的二叉树不一样,它节点是这样的:
template<class T>
struct AVLTreeNode
{
AVLTreeNode<T>* _parent; //父亲节点
AVLTreeNode<T>* _left; //左孩子
AVLTreeNode<T>* _right; //右孩子
T _val; //值
int bf; //平衡因子
};
它多了一个父节点,同时多了一个平衡因子,这个平衡因子就是来帮助我们之后调整二叉树的。
快速写一个构造函数:
AVLTreeNode(const T& val = T())
:_val(val)
, _parent(nullptr)
, _left(nullptr)
, _right(nullptr)
, _bf(0)
{}
不需要什么参数,就传过来一个值即可,如果没传,就调用这里的缺省值。
插入
其实插入跟之前二叉搜索树是一样的
但是不同的地方在于,我们插入之后要更新我们的平衡因子,不是更新插入节点的,而是更新父亲节点的。
这里看图:
这里就要提一下,平衡因子是怎么算的:
如果左子树插入一个节点,那父亲的平衡因子就-1
如果右子树插入一个节点,那父亲的平衡因子就+1
按照上面逻辑,现在算一下插入之前的平衡因子:
现在插入一个:
此时,这棵树就很平衡,当然,也可以在其他地方插入,那就会不平衡,这里我们先写一个插入,不平衡的情况之后分析。
先写一个模板,这个模板里面只有一个参数,就是根节点
template<class T>
class AVLTree
{
typedef AVLTreeNode<T> Node;
private:
Node* _root;
};
插入
bool insert(const T& val)
{
if (_root == nullptr)
{
_root = new Node(val);
return true;
}
else
{
//插入
Node* cur = _root;
Node* parent = cur->_parent;
while (cur)
{
if (cur->_val > val)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_val < val)
{
parent = cur;
cur = cur->_right;
}
else
assert(false);
}
cur = new Node(val);
if (cur == parent->_left)
parent->_left = cur;
else
parent->_right = cur;
}
}
调整
我们插入完后,要更新父亲节点的平衡因子:
//更新平衡因子
while (parent)
{
if (cur == parent->_left)
parent->_bf--;
else
parent->_bf++;
}
更新完了,就该判断是否平衡了
这里简单看几个图分析情况:
第一种情况:
右单旋
假设此时已经左边高了,而我们又在左边插入了一个元素,此时,就变成了这样的情况,为了方便后续操作,将这个三个节点分别称之为:A/B/C
将最高的那个节点拿下来,将其最为第二高的右节点
假设情况在复杂一点:
我们看一下插入前后的平衡因子:
当前节点为-1且父亲节点为-2,这就要右旋转了,为了之后方便写代码,这里将节点重新取名
这样,高度不仅没变,还保持了二叉搜索树的条件,为了方便阅读,这里给节点单独取名
void _RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subR->_right;
Node* pparent = parent->_parent;
if (subLR)
subLR->_parent = parent;
subL->_right = parent;
parent->_left = subLR;
if (parent == _root)
{
_root = subL;
}
else
{
if (parent == pparent->_left)
pparent->_left = subL;
else
pparent->_right = subL;
subL->_parent = pparent;
}
parent->_bf = subL->_bf = 0;
}
这里可以对照着图理解一下,因为最后旋转完高度应该是一样的,所以平衡因子应该重置为0
---
左单旋
这种完全是跟右单旋反过来,它是这样的图:
具体思路跟右单旋相反:
具体思路差不多,看代码:
void _RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
if (subRL)
subRL->_parent = parent;
Node* pparent = parent->_parent;
subR->_left = parent;
parent->_right = subRL;
parent->_parent = subR;
if (parent == _root)
_root = subR;
else
{
if (pparent->_left == parent)
pparent->_left = subR;
else
pparent->_right = subR;
}
s
写完了先来测试一下效果:
这是一个左单旋,2的左边是1,右边是3,调整没问题。
右单选,也没问题
---
左右双旋
有时,就会出现这种情况,A B C不都在一边,对于A来说,B在它的左边,对于B来说,C在它的右边,此时就不是单纯的左旋转或者右旋转能解决问题的了。
此时,无论在E、F哪边插入,都会失去平衡,面对这种情况,我们就要考虑双旋,转两次来解决问题
这时,我们就要先左旋,再右旋:
左旋:
再右旋;
此时,就完成了对称调整
这里要注意的一点是,在调整之前C的平衡因子是多少,因为会有以下几种情况:
假设C调整之前是-1,说明在E插入,即上图调整
假设C调整之前是1,说明在F插入,即下图调整
根据调整之前不同,AB的值也不同
如果是-1:A= -1,B=C=0
如果是1:B=1,A=C=0
这里复用之前写好的旋转逻辑即可:
void _RotateLR(Node* parent)
{
Node* subL = parent->_left;//B
Node* subLR = subL->_right;//C
int bf = subLR->_bf;
_RotateL(parent->_left);
_RotateR(parent);
if (bf == 1)
subL->_bf = -1;
else if (bf == -1)
parent->_bf = 1;
else
assert(false);
}
右左双旋
大体可以参考左右双旋,思路也是一样的
几乎是镜像左右旋,同样根据C的bf值有不同的结果,最后调整一下即可
void _RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
_RotateR(parent->_right);
_RotateL(parent);
if (bf == -1)
subR->_bf = 1;
else if (bf == 1)
parent->_bf = -1;
}
删除之后也许会更新,了解插入的旋转可以帮助我们理解二叉搜索树的结构了