喜欢的点赞,收藏,关注一下把!
1.AVL树的概念
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年
发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
- 它的左右子树都是AVL树
- 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 O ( l o g 2 n ) O(log_2 n) O(log2n),搜索时间复杂度O( l o g 2 n log_2 n log2n)。
AVL树实现有很多方法,今天我们用的是平衡因子=右子树高度-左子树高度用来评估每个子树的状态,如果每颗子树都是平衡的,那这颗树当然也是平衡的。
AVL树插入和删除会影响祖先节点的平衡因子的改变,我们当然可以使用栈来记录每个节点的父亲。但是今天这里我们使用的是三叉。也就是多一个指针指向每个节点的父亲。虽然方便了一些,但是写的时候要小心,不然容易丢节点。
2.AVL树节点的定义
这里我们用的是pair存放数据,当然可以不用pair来存数据就像K模型二叉搜索树那样也行,看自己的喜欢。
template<class T,class V>
struct AVLTreeNode
{
pair<T, V> _kv;
AVLTreeNode<T, V>* _left;
AVLTreeNode<T, V>* _right;
AVLTreeNode<T, V>* _parent;
int _bf = 0;//平衡因子
AVLTreeNode(const pair<T,V>& kv)
:_kv(kv)
,_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_bf(0)
{}
};
3.AVL树的插入
AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:
- 按照二叉搜索树的方式插入新节点
- 调整节点的平衡因子
template<class T,class V>
class AVLTree
{
typedef AVLTreeNode<T, V> Node;
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->_right;
}
else if(cur->_kv.first > kv.first)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(kv);
if (cur->_kv.first < parent->_kv.first)
{
parent->_left = cur;
cur->_parent = parent;
}
else
{
parent->_right = cur;
cur->_parent = parent;
}
//1.调整平衡因子
return true;
}
private:
Node* root = nullptr;
};
插入这部分代码倒是没问题,难的是新节点插入后,AVL树的平衡性可能会遭到破坏,此时就需要更新平衡因子,并检测是否破坏了AVL树,破坏了AVL树就需要旋转调整再次变成AVL树。
看下面一些具体情况:
新增在左,parent->_bf- -
新增在右,parent->_bf++
注意上图,可以看到更新parent的平衡因子可能会更新到根为止。那是否需要继续向上更新就需要一个判断。当然是否旋转也需要一个判断。
是否继续向上更新依旧:子树的高度是否变化
-
parent->_bf == 0,说明之前parent->_bf是1或者-1,说明之前parent一边高一边低,而这次的插入是把矮的那边填上了,parent所在子树高度不变,不需要往上继续更新。
-
parent->_bf == 1 或者 -1,说明之前parent->_bf为0,两边一样高,现在插入使一边变得更高了,parent所在子树高度变了,继续往上更新。
-
parent->_bf == 2 或者 -2,说明之前parent->_bf是1或者-1,现在插入导致严重不平衡,违反规则,就地处理—>旋转。
旋转:
- 让这颗子树左右高度之差绝对值不超过1
- 旋转过程中继续保持它是搜索树
- 更新调整孩子节点的平衡因子
- 让这颗子树的高度跟插入前保持一致(这样旋转之后就不要往上更新平衡因子了,更新旋转结束)
如果是以具体的节点插入导致旋转来分析,那情况实在是太多了,因此我们用四种抽象图,来把插入导致旋转的所有情况都总结起来,归类成四种状况。
3.1LL
新节点插入较高右子树的右侧—右右:左单旋
下面是抽象图包含了左旋的所有情况
a/b/c是高度为h的AVL子树
c插入节点,导致高度变化为h+1,就会左单旋。
我们在看一些在c插入导致左旋的具象图情况
当h==2,就有这么多情况,但是幸好我们有抽象图,把这一类都统统左旋就可以解决问题。
接下来由抽象图来看看如何左旋?
旋转动作:
60左边,调整到30的右边
30变成60的左边
原来30做子树的根,现在60做子树的根
我们只动了三个地方,就可以把这颗子树旋转然后达到平衡。为了清晰,我们标注一下。
因此我们写左旋代码,就可以根据上面的旋转动作进行旋转,
parent右指针指向subRL
subR左指针指向parent
subR变成子树的根。
虽然我们大方向是这样的,但是一定要注意的是。我们可是三叉,每更新一个节点都不能忘记更新该节点的_parent指针,并且从刚才就一直在说的是子树的根,因为你调整的可能就是整颗树或者这个树的部分子树。 如果是子树必须要把调整之后的子树和整颗树链接起来。
void RotaleL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL)//subRL可能为空,这里不判断,下面这句就有野指针的风险
subRL->_parent = parent;
//调整的可能是子树,必须要先把当前子树根的父亲记录下来,否则下一步更新当前子树根的父指针
//那调整之后的新子树的根就不能和树链接在一起了
Node* ppNode = parent->_parent;
subR->_left = parent;
parent->_parent = subR;
if (ppNode == nullptr)//判断调整的是子树还是整颗树
{
//整颗树
_root = subR;
_root->_parent = nullptr;
}
else
{
if (parent == ppNode->_left)
{
ppNode->_left = subR;
}
else
{
ppNode->_right = subR;
}
subR->_parent = ppNode;
}
//更新调整孩子节点的平衡因子
parent->_bf = subR->_bf = 0;
}
3.3RR
新节点插入较高左子树的左侧—左左:右单旋
a/b/c是高度为h的AVL树
a插入节点,导致高度变化为h+1,就会右单旋
这个抽象图包含了所有右旋的情况。
旋转动作:
30的右边变成60的左边
60变成30的右边
30变成新的子树的根
同样给三个标记
右旋和左旋非常类似,会写其中一种另一种也没问题。
void RotaleR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
Node* ppNode = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (ppNode == nullptr)
{
_root = subL;
_root->_parent = nullptr;
}
else
{
if (parent == ppNode->_left)
{
ppNode->_left = subL;
}
else
{
ppNode->_right = subL;
}
subL->_parent = ppNode;
}
parent->_bf = subL->_bf = 0;
}
3.3RL
目前为止我们学了左旋,右旋已经能解决大部分的情况呢,那下面这种情况还能通过单旋来解决吗?
显然已经不行了。
像这样的情况我们要通过双旋来解决问题
新节点插入较高左子树的右侧—左右:先左单旋再右单旋
a/d是高度为h的AVL树
b/c是高度为h-1的AVL树
b新增或者c新增(图中画的是b新增)
下面画一画具体图看一看
那左右双旋怎么旋转呢?
旋转动作:
30为轴点,进行一个左单旋
b变成30的右边
30变成60的左边
60变成子树的根
90为轴点,进行一个右单旋
c变成90的左边
90变成60的右边
60变成子树的根
为了看清,还是给三个标记
具体旋转如下:
先以parent的左孩子为轴点进行一趟左旋
再以parent为轴点进行一趟右旋
刚才我们写了左旋,右旋,直接调用函数就可以了。但是我们在左旋,右旋最后的代码更新平衡因子,都给了0
而这种双旋的平衡因子并不是都是0,并且多了一个节点的平衡因子要更新。
因此在旋转之后需要重新更新三个节点的平衡因子这是其一。
刚刚说了b,c都可以新增。并且在b新增和在c最后三个节点的平衡因子是有差别的这是其二。
注意到我们可以根据subLR的平衡因子,来判定是在b新增还是在c新增。从而更为准确的更新三个节点的平衡因。
还有一个最容易忽略的问题,该节点就是新增这是其三
而这种情况,我们直接左旋,右旋,也不用在更新三个节点的平衡因子了。
void RotaleLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;//由subLR的平衡因子来判断,在左侧新增,或右新增,或本身就是新增
RotaleL(parent->_left);
RotaleR(parent);
//更新平衡因子
if (bf == -1)//在左侧增
{
parent->_bf = 1;
subL->_bf = 0;
subLR->_bf = 0;
}
else if (bf == 1)//在右侧增
{
parent->_bf = 0;
subL->_bf = -1;
subLR->_bf = 0;
}
else if (bf == 0)//本身就是新增节点
{
parent->_bf = subL->_bf = subLR->_bf = 0;
}
else
{
assert(false);
}
}
3.4RL
新节点插入较高右子树的左侧—右左:先右单旋再左单旋
当然还需要注意本身就是新增节点。
void RotaleRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotaleR(parent->_right);
RotaleL(parent);
if (bf == -1)//左侧新增
{
parent->_bf = 0;
subR->_bf = 1;
subRL->_bf = 0;
}
else if (bf == 1)//右侧新增
{
parent->_bf = -1;
subR->_bf = 0;
subRL->_bf = 0;
}
else if (bf == 0)//本身就是新增
{
parent->_bf = subR->_bf = subRL->_bf = 0;
}
else
{
assert(false);
}
}
3.5插入
bool Insert(const pair<T, 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->_right;
}
else if(cur->_kv.first > kv.first)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(kv);
if (cur->_kv.first < parent->_kv.first)
{
parent->_left = cur;
cur->_parent = parent;
}
else
{
parent->_right = cur;
cur->_parent = parent;
}
//1.更新平衡因子
while (parent) //parent为空,也就更新到根
{
//新增在左,parent->_bf--
//新增在右,parent->_bf++
if (cur == parent->_left)
parent->_bf--;
else
parent->_bf++;
if (parent->_bf == 0)//说明以前parent->_bf为1或者-1,这次插入插到矮的那一边,子树高度没变,不需要往上继续更新
{
break;
}
else if (parent->_bf == 1 || parent->_bf == -1)//说明以前parent->_bf为0,这次插入导致子树高度变化,需要往上继续更新
{
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)//严重不平衡
{
if (parent->_bf == 2 && cur->_bf == 1)//左旋
{
RotaleL(parent);
}
else if (parent->_bf == -2 && cur->_bf == -1)//右旋
{
RotaleR(parent);
}
else if (parent->_bf == -2 &&cur->_bf == 1)//先左旋,再右旋
{
RotaleLR(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1)//先右旋,在左旋
{
RotaleRL(parent);
}
else
{
assert(false);
}
break;
}
else
{
assert(false);
}
}
return true;
}
4.AVL树的删除
AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不过与插入不同的是,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。
被删的节点无非就是左为空,右为空,左右都不为空这三种情况,为什么是三种呢,因为左右都为空我们可以归结到左为空或右为空的情况,关于如何删除这部分内容二叉搜索树有详细介绍。
删除节点之后要更新parent的平衡因子,而删除平衡因子的更新与插入完全相反。
删除parent左孩子,parent->_bf++
删除parent右孩子,parent->_bf- -
是否继续向上更新依据:子树的高度是否变化
- parent->_bf==0,说明之前parent->_bf为1或者-1,删除是父亲把唯一的一颗子树删了,导致父亲子树高度改变了,继续向上更新平衡因子
- parent->_bf==1或者-1,说明之前parent->_bf为0,左右都有子树,删除只是把左子树或右子树删了,而子树高度依旧没变化,不需要向上更新平衡因子
- parent->_bf==2或者-2,严重不平衡,原地旋转
在插入的时候,我们旋转之后跟插入之前高度保持一致,更新旋转就都结束了,而删除的时候,除非parent的平衡因子等于1/-1,或者更新到根才能结束!!!
根据祖先节点更新后的平衡因子,就分为上面三种情况,下面我们根据图在把三种情况具体阐述。
1.祖先节点原来平衡因子是0,在它的左子树或者右子树被缩短后,它的平衡因子变成1或者-1,由于子树高度没有变,从祖先节点到根节点的所有节点都不需要调整,此时可以结束本次删除的重新平衡过程。
2.祖先节点原来平衡因子为1或者-1,但较高的子树被缩短,则祖先节点的平衡因子变成0,此时以该节点为根的子树平衡,但其高度减1,为此必须继续向上更新。
3.祖先节点原来平衡因子为1或者-1,但较短的子树被缩短了,则祖先节点的平衡因子变成2或-2,此时以该节点为根的子树严重不平衡,为此需要进行旋转操作来恢复平衡。
根据祖先节点较高子树的根(该子树未被缩短)的平衡因子,有如下三种平衡化操作
(1)如果祖先节点较高子树的根的平衡因子为0,执行一次单旋转操作来恢复子树的平衡
注意旋转平衡后,以q节点为根的子树高度没有变化,因此不需要继续向上更新了,删除结束
(2)如果祖先节点较高子树的根的平衡因子和祖先节点平衡因子正负号相同,执行一次单旋转操作来恢复子树的平衡
注意旋转平衡之后,以q节点为根的子树高度减1,所以需要继续向上更新
(3)如果祖先节点较高子树的根的平衡因子和祖先节点平衡因子正负号相反,执行一次双旋转操作来恢复子树的平衡
注意旋转平衡之后,以r节点为根的子树高度减1,所以需要继续向上更新
bool Erase(const pair<T, V>& kv)
{
if (_root == nullptr)
return false;
Node* parent = nullptr;
Node* cur = _root;
while (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
{
//1.左为空
//2.右为空
//3.左右都不为空
if (cur->_left == nullptr)
{
if (parent == nullptr)
{
_root = cur->_right;
_root->_parent = nullptr;
}
else
{
if (cur == parent->_left)
{
parent->_bf++;
parent->_left = cur->_right;
}
else
{
parent->_bf--;
parent->_right = cur->_right;
}
}
delete cur;
}
else if (cur->_right == nullptr)
{
if (parent == nullptr)
{
_root = cur->_left;
_root->_parent = nullptr;
}
else
{
if (cur == parent->_left)
{
parent->_bf++;
parent->_left = cur->_left;
}
else
{
parent->_bf--;
parent->_right = cur->_left;
}
}
delete cur;
}
else//左右都不为空
{
Node* parent = cur;
Node* MinRight = cur->_right;
while (MinRight->_left)
{
parent = MinRight;
MinRight = MinRight->_left;
}
cur->_kv = MinRight->_kv;
if (MinRight == parent->_left)
{
parent->_bf++;
parent->_left = MinRight->_right;
}
else
{
parent->_bf--;
parent->_right = MinRight->_right;
}
delete MinRight;
}
//虽然上面已经调整了父亲的平衡因子,但是可能还会持续向上更新
bool IsLR = true;//刚才已经更新过了父亲的,因此第一次进循环先不更新
while (parent)
{
if (IsLR == false)
{
if (cur == parent->_left)
{
parent->_bf++;
}
else
{
parent->_bf--;
}
}
IsLR = false;
//parent->_bf ==1或者-1,说明之前parent->_bf为0,子树高度没变不需要再向上更新
if (parent->_bf == 1 || parent->_bf == -1)
{
return true;
}
else if (parent->_bf == 0)//parent->_bf ==0,说明之前parent->_bf为1或者-1,子树高度减1,需要再向上更新
{
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)//严重不平衡,需要旋转
{
//根据刚才分析,需要旋转有三种情况
// p--->parent,q就是根较高的子树
//1.q==0,单旋
//2.p和q正负号相同,单旋
//3.p和q正负号不同,双旋
int sign = 0;//以此记录parent的正负号
Node* higherchild = nullptr;
if (parent->_bf > 0)
{
sign = 1;
higherchild = parent->_right;//当父亲的平衡因子大于0,较高子树再它的右边
}
else
{
sign = -1;
higherchild = parent->_left;//当父亲的平衡因子小于0,较高子树再它的右边
}
//现在就以sign和higherchild来判断如何旋转
if (higherchild->_bf == 0)
{
if (sign > 0)//左旋
{
RotaleL(parent);
parent->_bf = 1;
higherchild->_bf = -1;
}
else
{
RotaleR(parent);
parent->_bf = -1;
higherchild->_bf = 1;
}
//parent->_bf=1/-1,以前parent->_bf=0,旋转后子树高度没变,不需要向上更新了
return true;
}
else if (higherchild->_bf == sign)//同号,单旋
{
//parent->_bf为正
if (sign == 1)
{
RotaleL(parent);
}
else
{
RotaleR(parent);
}
}
else //异号,双旋
{
//parent->_bf为正
if (sign == 1)
{
RotaleRL(parent);
}
else
{
RotaleLR(parent);
}
}
//同号和异号,新的子树的根的平衡因子都是0,旋转后子树高度缩短,继续向上更新
//因此要找新子树的根开始更新平衡因子
cur = parent;
parent = cur->_parent;
}
else
{
assert(false);
}
}
}
}
return false;
}
5.判断是否是AVL树
我们可以根据AVL树的性质来判断
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
- 它的左右子树都是AVL树
- 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
int TreeHight(Node* root)
{
if (root == nullptr)
return 0;
int lefthight = TreeHight(root->_left);
int righthight = TreeHight(root->_right);
return lefthight > righthight ? lefthight + 1 : righthight + 1;
}
bool _IsBalance(Node* root)
{
if (root == nullptr)
return true;
int leftHight = TreeHight(root->_left);
int rightHight = TreeHight(root->_right);
return abs(rightHight - leftHight) < 2 && _IsBalance(root->_left) && _IsBalance(root->_right);
}
关于AVL树的构造,拷贝构造,赋值,析构等和二叉搜索树差不多,想了解的可以看我上一篇博客二叉搜索树。