引言:
AVL树是一种特殊的二叉搜索树,二叉搜索树虽然可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树具有如下性质:
- 左右子树都是AVL树
- 左右子树高度差绝对值不超过1
一、AVL树结点的定义
template<class T>
struct AVLTreeNode
{
AVLTreeNode(const T& data)
: _pLeft(nullptr), _pRight(nullptr), _pParent(nullptr)
, _data(data), _bf(0)
{}
AVLTreeNode<T>* _pLeft; // 该节点的左孩子
AVLTreeNode<T>* _pRight; // 该节点的右孩子
AVLTreeNode<T>* _pParent; // 该节点的双亲
T _data;
int _bf; // 该节点的平衡因子
};
二、AVL树的插入
与普通的二叉搜索树不同,AVL树在数据插入后需要维护整棵树的平衡,并且更新结点的平衡因子
AVL树的插入过程可以分为两步:
- 按照二叉搜索树的方式插入新节点
- 调整节点的平衡因子
bool Insert(const T& data)
{
//如果是空树,直接插入并结束
if (_pRoot == nullptr)
{
_pRoot = new Node(data);
return true;
}
//这一块代码的目的是找到插入位置的父亲结点
Node* parent = nullptr;
Node* cur = _pRoot;
while (cur)
{
if (data < cur->_data)
{
parent = cur;
cur = cur->_pLeft;
}
else if (data > cur->_data)
{
parent = cur;
cur = cur->_pRight;
}
else
{
return false;
}
}
if (cur == parent->_pLeft)
{
cur = new Node(data);
parent->_pLeft = cur;
}
else
{
cur = new Node(data);
parent->_pRight = cur;
}
//从parent结点开始向上更新祖先的平衡因子
while (parent)
{
if (parent->_bf == 0)
{
break;//插入之后,该结点平衡因子为0说明以该结点为根的子树平衡了
//且插入前后高度不变,所以这个结点的祖先的平衡因子不会受到影响,结束
}
else if (parent->_bf == 1 || parent->_bf == -1)
{
//平衡因子从0变为1或者-1,祖先可能变成2或-2,需要继续向上更新
cur = parent;
parent = parent->_pParent;
}
else
{
//结点的平衡因子变成了2或者-2,需要进行旋转了
//这里开始需要对AVL树进行旋转,之后再讲
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
{
RotateLR(parent);
}
break;
}
}
return true;
}
三、AVL树的旋转
1.右单旋
这是一幅抽象图,左子树较高且在左子树的左侧插入 ,`h>=0,此时需要进行右单旋
我们可以看到这样旋转之后该子树依然是一个二叉搜索树,巧妙的是,根节点的平衡因子被更新 成0了,这说明:在旋转之后不用再对祖先更改平衡因子了
void RotateR(Node* pParent)
{
Node*subL = pParent->_pLeft;
Node* subLR = subL->_pRight;
pParent->_pLeft = subRL;
if (subLR)
{
subLR->_pParent = pParent;
}
Node*parentParent = pParent->_pParent;
subL->_pRight = pParent;
if (pRoot == pParent)
{
pRoot = subL;
subL->_pParent = nullptr;
}
else
{
if (parentParent->_pLeft == pParent)
{
parentParent->_pLeft = subL;
}
else
{
parentParent->_pRight = subL;
}
subL->_pParent = parentParent;
}
pParent->_bf = subL->_bf = 0;
}
2.左单旋
左单旋和右单旋类似,只是方向相反,右子树较高且在右子树的右侧插入
void RotateL(Node* pParent)
{
Node*subR = pParent->_pRight;
Node* subRL = subR->_pLeft;
pParent->_pRight = subRL;
if (subRL)
{
subRL->_pParent = pParent;
}
Node* parentParent = pParent->_pParent;
pParent->_pParent = subR;
subR->_pLeft = pParent;
if (parent == pRoot)
{
pRoot = subR;
subR->_pParent = nullptr;
}
else
{
if (parentParent->_pLeft = pParent)
{
parentParent->_pLeft = subR;
}
else
{
parentParent->_pRight = subR;
}
}
pParent->_bf = subR->_bf = 0;
}
3. 左右双旋
对于单侧插入,只需要旋转一次。但是对于如下的情况,单次旋转无法解决问题
和单旋不同,我们需要对子树再往下画一层,如图所示
这棵树的左子树较高,并且在左子树的右子树插入,导致单旋无法一次解决
旋转过程
step1:
step2:
新插入结点无论插入在subLR的左还是右, 最终subLR平衡因子为0
// 左右双旋
void RotateLR(Node* pParent)
{
Node* subL = pParent->_pLeft;
Node* subLR = pParent->_pRight;
int bf = subLR->_bf;
RotateL(subL);
RotateR(pParent);
//subLR就是新插入结点
if (bf == 0)
{
pParent->_bf = subL->_bf = subLR->_bf = 0;
}
else if (bf == 1)
{
pParent->_bf = -1;
subL->_bf = -1;
subLR->_bf = 0;
}
else if(bf==-1)
{
pParent->_bf = 1;
subL->_bf = 1;
subLR = 0;
}
}
4.右左双旋
与左右双旋类似
//右左双旋
void RotateRL(Node* pParent)
{
Node* subR = pParent->_pRight;
Node* subRL = subR->_pLeft;
int bf = subRL->_bf;
RotateR(subR);
RotateL(pParent);
//subRL就是新插入结点
if (bf == 0)
{
pParent->_bf = subR->_bf = subRL->_bf = 0;
}
else if (bf == 1)
{
pParent->_bf = 0;
subR->_bf = -1;
subRL->_bf = 0;
}
else if(bf==-1)
{
pParent->_bf = 1;
subR->_bf = 1;
subRL->_bf = 0;
}
}