一.概念
1.基本概念

2.高度差与效率
既然左右子树高度差绝对值控制在1以内 ,那么这棵树就接近一个满二叉树的状态,所以它的高度就是可控的,为logN 。那么增删查改效率也能控制在logN,也就是说,对比普通二叉搜索树那种极端高度差(一棵子树有接近n的高度,而另一棵几乎没有节点,效率为O(N))的情况,效率有极大的提升。
3.平衡因子
这个东西其实可有可无,只是有了它更容易观察和控制树的平衡,所以在行文时引入它。


二.AVL树的实现
1.每个节点类模板的成员变量
AVL树节点的值仍旧是一对键值对,并储存在类模板pair中。为了方便遍历和观察控制高度差,成员变量除了存值的pair,还有左节点,右节点,父节点,以及平衡因子。
2.AVL树类模板的成员变量

3.插入
①.先按二叉搜索树的规则执行插入。
Ⅰ.用cur进行遍历
cur的key与待插入节点的key进行比较,待插入的小,就代表它将插入当下cur的右子树,那么就让cur往当下自己的右子树走,在往下走之前,别忘了将当下的cur赋值给parent。就这样让cur一直迫近待插入节点的父节点。当cur为待插入节点父节点时,遍历结束。相当于cur既当此生的自己,也当来生的父亲,还充当待插入节点的信标。
Ⅱ.执行插入
先判断待插入节点与其父节点大小,然后执行插入。
Ⅲ.插入完后,要链接上_parent。
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)
{//cur和待插入节点的值进行比较,待插入值大于cur,cur往右子树走,小于则往左子树走。
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 (parent->_kv.first > kv.first)
{
parent->_right = cur;
}
if (parent->_kv.first < kv.first)
{
parent->_left = cur;
}
//链接父节点
cur->_parent = parent;
}
②.更新平衡因子
插入后,就需要去更新一下从插入节点到根节点这一路的平衡因子 ,最坏的情况是更新到根节点,一般情况是更新到这一路中途就结束了。结束的标志是平衡因子均未出现问题(为0,-1,1)。
Ⅰ.更新原则
i.平衡因子 = 右子树高度 - 左子树高度
ii.只有在插入后高度发生改变才会影响当前节点平衡因子
iii.在插入时,假如高度增加,如果将节点插在parent右边,parent的_bf++,插在左边,_bf--

iv.parent的子树高度是否发生变化,决定了是否要向上进行更新
Ⅱ.更新停止条件

上面三种情况的图示:


由于旋转很重要且较难,就将它单拎出来。
3.5旋转
①.旋转的原则

②.左单旋

在a子树中插入新节点,此时parent的_bf变成2,平衡打破。b子树中首个节点的大小介于a,c之间,如老蒋一般中正(他中正个毛),旋即将b向左扭,成为10这个节点的右子树,然后将15这个节点提上去,10这个节点的一支,就反成了15节点的左子树。
③.右单旋

右单旋也是相同原理,只是变个方向的事,将完成插入后子树的兄弟子树,b子树(这个子树的首节点大小十分中正,介于a,c子树对应的节点之间,b子树首节点比c子树的要小,所以b子树抛给parent当左子树是符合搜索树规则的),抛给当下的parent节点(10),给parent节点当左子树,然后完成插入的子树的父亲节点(cur/subLR)又提上去作根节点,最后之前这个parent节点(10)这一支只能沦落作cur的右子树。
但不免会有些疑惑,这里的a,b,c子树不过是个框架,它的内里究竟是怎样的呢?



可以发现,这几种情况在旋转之后,parent和cur的_bf都被抹平,变成了0,所以下面代码实现时,要在旋转完以后,将这二位的_bf更新为1。
h可以等于任意数,但h越大,树越复杂,就没必要一一列举了。
④.左右双旋(大boss)
Ⅰ.引子
单旋场景是纯粹的一边高(同一支上每个节点的子树全是同一边高),举个例子,右单旋中,如果旋转前subL位于Parent左边,那么新插入节点也必然在subL左边。

而以下场景,新插入节点是在subL右边的,如果再用右单旋的思路去旋转,旋转后从左边高变右边高了,那么旋转后的树仍旧无法平衡。

可以观察到,插入后,若平衡因子异号,不是纯粹的一边高(10这个节点的子树是左边高,而5这个节点的子树是右边高),那么就用不了单旋的思路。
Ⅱ.步入正题
那么该如何操作呢,我们进入双旋。

先单旋一遍,将造成一遍高一边低局面的子树的根节点进行旋转,使得单边高矮统一。

然后就是纯粹的一边高的树,将这棵树再次单旋即可得到平衡的树。
4.旋转代码实现
4-1.右单旋
①.命名规范
parent:待旋转节点(_bf绝对值变为2的节点),老的根节点
subL(cur):parent左子节点,旋转后新的根节点
subLR:subL右子节点,新插入节点的右兄弟节点
②.易忽视的小细节
Ⅰ.由于是三叉链的结构,旋转之后,还要更改指向经过变动的节点(包括subLR以及parent)的_parent的指向。

Ⅱ.subLR为空的情况。不判断会直接访问空指针。

Ⅲ.subL是新的根节点,不代表它是整棵树的根节点,它可能只是某棵子树的根节点,所以需要讨论subL是子树根节点还是整棵树根节点。
如果是整棵树根节点,subL的_parent需要指向空。
如果是子树根节点,需要改变Parent父节点(Pparent)的指向,从指向parent改成指向subL


那么如何找到Parent的父节点呢?
就需要在右旋之前或者说Parent还是根节点时,用一个新指针Pparent记录这个节点。

Ⅳ.旋转完之后,subL和parent的_bf发生变化,还需要更新
③.全部代码(小细节,坑很多)
void RotateR(Node* Parent)
{//右单旋
Node* subL = Parent->_left;
Node* subLR = subL->_right;
Parent->_left = subLR;
if(subLR)
subLR->_parent = Parent;//由于是三叉链的结构,旋转之后,还要更改_parent的指向
Node* Pparent = Parent->_parent;//记录Parent的父亲节点
Parent->_parent = subL;
subL->_right = Parent;
if (Parent == _root)//Parent节点就是整棵树根节点的情况
{//旋转后记得更新_root
_root == subL;
subL->_parent = nullptr;//根节点的父节点为空。
}
else//不是整棵树的根
{
if (Pparent->_left == parent)
{
Pparent->_left = subL;
subL->_parent = Pparent;
}
else
{
Pparent->_right = subL;
subL->_parent = Pparent;
}
}
}
4-2.左单旋
基本跟上面相同,倒个个儿的事。
void RotateL(Node* Parent)
{//左单旋
Node* subR = Parent->_right;
Node* subRL = subR->_left;
Parent->_right = subRL;
if (subRL)
{
subRL->_parent = Parent;
}
Node* Pparent = Parent->_parent;
subR->_left = Parent;
Parent->_parent = subR;
if (Parent == _root)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (Pparent->_left == Parent)
{
Pparent->_left = subR;
subR->_parent = Pparent;
}
else if(Pparent->_right == Parent)
{
Pparent->_right = subR;
subR->_parent = Pparent;
}
}
subR->_bf = 0;
Parent->_bf = 0;
}
4-3左右双旋
整棵树中,在插入后,平衡因子发生改变的也仅仅是Parent,subL,subLR三个。而影响其他两个节点的平衡因子的关键人物是------subLR(下面值为8的节点),可以依照新节点插入subLR子树的位置,来分成以下三种情况(确切的说是两种):



场景1

场景2

场景3
可复用上面两个单旋的代码,Parent->left(subL)先左单旋,Parent再右单旋。但困难的地方在于平衡因子的更新,照两个单旋各自的更新来操作,最后Parent,subL,subLR的平衡因子统统会被抹平成0,这显然是不对的。所以不能单纯的用单旋里的更新机制来调节双旋的平衡因子。
所以得在两个单旋之前,插入之后,用新常量bf记录subLR的平衡因子。然后在两次单旋以后,用bf按以上三种情况讨论,重写正确的平衡因子:
if (bf == -1)
{//场景1
subLR->_bf = 0;
subL->_bf = 0;
Parent->_bf = 1;
}
if else(bf == 1)
{//场景2
subLR->_bf = 0;
subL->_bf = -1;
Parent->_bf = 0;
}
if else(bf == 0)
{//场景3
subLR->_bf = 0;
subL->_bf = 0;
Parent->_bf = 0;
}
else
{
assert(false);
}
完整代码:
void RotateLR(Node* Parent)
{//左右双旋
Node* subL = Parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;//记录平衡因子
RotateL(Parent->_left);
RotateR(Parent->_right);//单旋后三个节点_bf全部抹0,但双旋后的平衡因子绝不可能为此值。所以要记下插入后旋转前的平衡因子。
//更新正确的平衡因子
if (bf == -1)
{//场景1
subLR->_bf = 0;
subL->_bf = 0;
Parent->_bf = 1;
}
else if(bf == 1)
{//场景2
subLR->_bf = 0;
subL->_bf = -1;
Parent->_bf = 0;
}
else if(bf == 0)
{//场景3
subLR->_bf = 0;
subL->_bf = 0;
Parent->_bf = 0;
}
else
{
assert(false);
}
}
4-4右左双旋
基本思想和上面相似,只是这里要反一下,subR先右旋,然后Parent再左旋,然后旋转前,插入时bf,及其对应的每种旋转后三个节点_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)
{
subRL->_bf = 0;
subR->_bf = 1;
Parent->_bf = 0;
}
else if (bf == 1)
{
subRL->_bf = 0;
subR->_bf = 0;
Parent->_bf = -1;
}
else if (bf == 0)
{
subRL->_bf = 0;
subR->_bf = 0;
Parent->_bf = 0;
}
else
{
assert(false);
}
}
5.再探插入之满足各个旋转的条件
我们将具体旋转过程(单作一个接口)和插入中控制旋转分开,上面部分已阐述旋转过程以及其分类的具体代码实现,接下来回到Insert接口中,讨论插入后需旋转的情况。
5-1.接入右单旋
首先得明晰具体情境,完成插入操作后,Insert接口中的Parent对应的是右旋接口中的Parent,cur对应的是右旋接口中的subL。
那么判断为右单旋的条件就是(说明插在cur左边,左边高度增加,那么cur高度差就变成了-2):

代码:
if (Parent->_bf == -2 && cur->_bf == -1)
{//右单旋
RotateR(Parent);
}
5-2.接入左单旋

5-3.接入左右双旋
cur和Parent两个节点的子树,由于高的一侧相反,所以平衡因子也是相反的,因此条件是:

5-4.接入右左双旋
反一下就行:

6.求高度
递归展开图:
走一个后续遍历:

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;
}
7.检测平衡
不能用类里的平衡因子检测,会监守自盗,要单独实现一个接口。单独求高度和高度差。
注意:abs()是取绝对值的意思
走一个前序遍历:
bool _isBalanceTree(Node* root)
{
if (root == nullptr)
{//空树也是AVL
return true;
}
//计算平衡因子
int leftHight = _Height(root->_left);
int rightHight = _Height(root->_right);
int diff = rightHight - leftHight;
//判断异常与否
if (abs(diff) >= 2)
{
cout << root->_kv.first << "高度差异常" << endl;
return false;
}
if (diff != root->_bf)
{
cout << root->_kv.first << "平衡因子异常" << endl;
return false;
}
return _isBalanceTree(root->_left) && _isBalanceTree(root->_right);
}
8.查找
由于接近满二叉树,所以开销为h = logN次,效率很高。



