引入
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。
因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
1、AVL树的概念
AVL树是在满足二叉搜索树特性的基础上,且满足以下特性:
- 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
- 它的左右子树都是AVL树
平衡因子的计算方法:右子树高度 - 左子树高度
下面这棵二叉树就是一棵AVL树:
2、AVL树的操作
2.1 AVL树结点的定义
template <class T>
class TreeNode
{
int m_bf;//该结点的平衡因子,可能取值有:0/1/-1
T m_data;
TreeNode<T> * m_left;
TreeNode<T> * m_right;
TreeNode<T> * m_parent;
public:
TreeNode(const T & val = T()) :
m_bf(0),
m_data(val),
m_left(nullptr),
m_right(nullptr),
m_parent(nullptr)
{
}
};
2.2 AVL树的插入
AVL树是在二叉搜索树的基础上进行操作的,对AVL树插入新的结点需要进行以下两个步骤:
1.按照二叉搜索树插入元素的规则插入结点
2.调整平衡因子
-
插入curh可分为以下两种情况:
1.若插入结点是parent的左孩子,则parent->bf - 1;
2.若插入结点是parent的右孩子,则parent->bf + 1; -
插入cur后parent的平衡因子可能有三种情况:0,正负1, 正负2
1.若parent->bf ==0,说明插入之前parent->bf ==±1,插入后被调整为0,此时满足AVL树的性质,插入成功;
2.若parent->bf == ±1,说明插入前parent->bf == 0,插入后被调整为±1,此时以parent为根的树的高度增加,需要向上更新;
3.若parent->bf == ±2,则parent的平衡因子违反平衡树的性质,需要对其进行旋转处理
bool Insert(const &T val)
{
if (m_root == nullptr)
{
m_root = new TreeNode<T>(val);
return true;
}
TreeNode<T> * cur = m_root;
TreeNode<T> *pre = nullptr;
while (cur)
{
if (val < cur->m_data)
{
pre = cur;
cur = cur->m_left;
}
else if (val > cur->m_data)
{
pre = cur;
cur = cur->m_right;
}
else
{
return false;
}
}
cur = new TreeNode<T>(val);
if (val < pre->m_data)
{
pre->m_left = cur;
}
else
{
pre->m_right = cur;
}
cur->m_parent = pre;
while (pre)
{
if (pre->m_left == cur)
{
pre->m_bf--;
}
else
{
pre->m_bf++;
}
if (pre->m_bf == 0)
{
break;
}
else if (pre->m_bf == 1 || pre->m_bf == -1)
{
cur = pre;
pre = pre->m_parent;
}
else
{
//插入新结点会导致原来的二叉树不平衡,可以总结为以下四种情况进行调整,使二叉树重新平衡
if (pre->m_bf == 2)
{
if (cur->m_bf == 1)//“右右”
{
Lround(pre);
}
else//“右左”
{
RLround(pre);
}
}
else
{
if (cur->m_bf == 1)//“左右”
{
LRround(pre);
}
else//“左左”
{
Rround(pre);
}
}
break;
}
}
return true;
}
2.3 AVL树的旋转
AVL树插入结点的过程中需要进行调整,使得满足AVL树的特性,AVL树的旋转可分为以下四种情况:
- 左-左型(右旋)
左-左型是表示左子树的左孩子插入新的结点,需要进行右旋。
void Rround(TreeNode<T> * pre)
{
TreeNode<T> * parent = pre->m_parent;
TreeNode<T> * cur = pre->m_left;
cur->m_parent = parent;
if(parent)
{
if (parent->m_left == pre)
{
parent->m_left = cur;
}
else
{
parent->m_right = cur;
}
}
else
{
m_root = cur;
}
pre->m_left = cur->m_right;
if (cur->m_right)
{
cur->m_right->m_parent = pre;
}
cur->m_right = pre;
pre->m_parent = cur;
cur->m_bf = pre->m_bf = 0;
}
- 右-右型(左旋)
右-右型是表示右子树的右孩子插入新结点,需要进行左旋。
void Lround(TreeNode<T> * pre)
{
TreeNode<T> * parent = pre->m_parent;
TreeNode<T> * cur = pre->m_right;
//若进行左单旋,可能需要对三个结点进行调整
cur->m_parent = parent;
if (parent)//若需要旋转的该结点有父结点
{
if (parent->m_left == pre)
{
parent->m_left = cur;
}
else
{
parent->m_right = cur;
}
}
else//若该结点没有父结点,则该结点就是根结点
{
m_root = cur;
}
//该结点的左孩子会成为原来父结点的右孩子
pre->m_right = cur->m_left;
if (cur->m_left)//若该结点原来的左孩子存在,则它的父亲结点会变成pre
{
cur->m_left->m_parent = pre;
}
cur->m_left = pre;//该结点之前的父结点将会变为该结点的左孩子
pre->m_parent = cur;//该结点变为原来父亲结点的父结点
cur->m_bf = pre->m_bf = 0;
}
- 左-右型(先左旋再右旋)
左-右型表示插入的新结点是左子树的右孩子,需要进行两步操作,先进行左旋,再进行右旋。
void LRround(TreeNode<T> * pre)
{
TreeNode<T> * left = pre->m_left;
TreeNode<T> * newroot = left->m_right;
int flag = newroot->m_bf;
Lround(pre->m_left);
Rround(pre);
if (flag == -1)
{
pre->m_bf = 1;
}
else
{
left->m_bf = -1;
}
}
- 右-左型(先右旋再左旋)
右-左型表示插入的新结点是右子树的左孩子,需要进行两步操作,先右旋再左旋。
void RLround(TreeNode<T> * pre)
{
TreeNode<T> * right = pre->m_right;
TreeNode<T> * newroot = right->m_left;
//旋转之前,保存newroot的平衡因子,旋转完成之后,需要根据该平衡因子来调整其他节点的平衡因子
int flag = newroot->m_bf;
Rround(pre->m_right);//先进行右单旋
Lround(pre);//再进行左单旋
//判断右左单旋后flag的结果
if (flag == -1)//当新结点的平衡因子为-1的情况是:在该结点的左孩子插入一个结点,经过右左单旋有的结果
{
right->m_bf = 1;
}
else//当新结点的平衡因子为 1 的情况是:在该结点的右孩子插入一个结点,经过右左单旋有的结果
{
pre->m_bf = -1;
}
}
总结:
假如以parent为根的子树不平衡,即parent的平衡因子为2或者-2,分以下情况考虑( pSubL: parent的左孩子,pSubLR: parent左孩子的右孩子)
- parent的平衡因子为2,说明parent的右子树高,设parent的右子树的根为pSubR
当pSubR的平衡因子为1时,执行左单旋
当pSubR的平衡因子为-1时,执行右左双旋 - parent的平衡因子为-2,说明parent的左子树高,设parent的左子树的根为pSubL
当pSubL的平衡因子为-1是,执行右单旋
当pSubL的平衡因子为1时,执行左右双旋
旋转完成后,原parent为根的子树个高度降低,已经平衡,不需要再向上更新。
2.4 AVL树的删除
因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不过与删除不同的时,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。
bool erase(const T &val)
{
if (m_root == nullptr)
{
m_root = new TreeNode<T>(val);
return true;
}
TreeNode<T> * cur = m_root;
TreeNode<T> *pre = nullptr;
while (cur)
{
if (val < cur->m_data)
{
pre = cur;
cur = cur->m_left;
}
else if (val > cur->m_data)
{
pre = cur;
cur = cur->m_right;
}
else
{
break;
}
}
if (cur == nullptr)
{
return false;
}
if (cur->m_left && cur->m_right)
{
TreeNode<T> * cur2 = cur->m_left;
if (cur2->m_right)
{
for (; cur2->m_right; pre2 = cur2, cur2 = cur2->m_right);
pre2->m_right = cur2->m_left;
cur2->m_left = cur->m_left;
}
cur2->m_right = cur->m_right;
if (cur->m_data < pre->m_data)
{
pre->m_left = cur2;
}
else
{
pre->m_right = cur2;
}
delete cur;
}
else if (cur->m_left)
{
if (cur->m_data < pre->m_data)
{
pre->m_left = cur->m_left;
}
else
{
pre->m_right = cur->m_left;
}
delete cur;
}
else
{
if (cur->m_data < pre->m_data)
{
pre->m_left = cur->m_right;
}
else
{
pre->m_right = cur->m_right;
}
delete cur;
}
}
3、AVL树的性能分析
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1
,这样可以保证查询时高效的时间复杂度为O(log2N)。
但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。
应用场景:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,
但一个结构经常修改,AVL树就不太适合,这样的操作红黑树更加合适。
红黑树详解