前面对map/multimap/set/multiset进行了简单的介绍,在其文档介绍中发现,这几个容器有个
共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中
插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此
map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现
目录
[1. AVL树](#1. AVL树)
[1.1 概念](#1.1 概念)
[1.2 AVL树节点的定义](#1.2 AVL树节点的定义)
[1.3 AVL树的插入(健康版)](#1.3 AVL树的插入(健康版))
[1.4 AVL树的旋转(非健康版)](#1.4 AVL树的旋转(非健康版))
[1. 右旋图解](#1. 右旋图解)
[2. 左旋图解](#2. 左旋图解)
[3. 右左双旋图解](#3. 右左双旋图解)
[4. 左右双旋图解](#4. 左右双旋图解)
[1.5 AVL树的验证](#1.5 AVL树的验证)
[1.5.1 二叉搜索树的验证](#1.5.1 二叉搜索树的验证)
[1.5.2 AVL树的验证](#1.5.2 AVL树的验证)
[1.6 AVL树的删除](#1.6 AVL树的删除)
[1.7 AVL树的性能](#1.7 AVL树的性能)
1. AVL树
1.1 概念
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树 ,查
找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii
和E.M.Landis在1962年发明了一种解决上述问题的方法:
当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树。
它的左右子树都是AVL树
左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)

如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在
O(logN),搜索时间复杂度O(logN)
1.2 AVL树节点的定义
AVL与红黑树中存放的数据都是pair<k,v>结构。
cpp
struct AVLTreeNode
{
AVLTreeNode<K, V>* _left;//左孩子
AVLTreeNode<K, V>* _right;//右孩子
AVLTreeNode<K, V>* _parent;//父亲
pair<K, V> _kv;
int _bf;//平衡因子,健康的平衡因子绝对值不大于1
AVLTreeNode(const pair<K, V>& kv)
:_bf(0)
,_kv(kv)
, _left(nullptr)
, _right(nullptr)
,_parent(nullptr)
{}
};
这里可以看到,AVL树的节点与平衡二叉树的节点区别在于AVL树节点有一个平衡因子的存在,也即1.1概念中所述。同时AVL树采用了三叉链结构,好处在于在对树进行操作时,不必额外存储节点的父亲。
1.3 AVL树的插入(健康版)
AVL树的插入规则与平衡二叉树一样,不过在插入时要兼顾平衡因子绝对值不大于1的规则,如果某节点的平衡因子绝对值大于1,则需要进行处理。
与平衡二叉树一样,在插入节点时需要先找到插入的位置,这里我们可以复刻平衡二叉树的代码。
插入之后需要对其平衡因子(后面简称bf)进行判断。
那么插入一个节点其bf应该怎么判断呢?
很简单,bf是以该节点为根的树左右子树高度差,我们可以规定,左边插入节点bf--,右边插入节点bf++,新增的节点bf当然是为0的。

请看上图,上图只是AVL树的具象图之一,但我们所讲的特性已经足够展示。
左边插入,bf--;右边插入,bf++。我们发现当bf等于0时,对树的影响终止,当bf等于1或-1时,影响会继续向上扩散。那么为什么没有bf==2||bf==-2呢?因为如果发生这种状况,说明树有了问题,需要进行处理(在后面的非健康版讲解)。
根据这个逻辑我们先来实现一段代码。
cpp
bool insert(const pair<K, V>& kv)
{
if (_root == nullptr)//如果树为空,建立根节点
_root = new Node(kv);
else//插入
{
Node* cur = _root,*parent=nullptr;//虽然是三叉链结构,但这里的parent仍需要,因为我们找到的位置是nullptr
while (cur)//找到需要插入位置的父亲
{
if (cur->_kv.first > kv.first)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_kv.first < kv.first)
{
parent = cur;
cur = cur->_right;
}
else
return false;
}
cur = new Node(kv);
if (kv.first < parent->_kv.first)//判断插入的位置是父亲的左还是右
{
parent->_left = cur;
parent->_bf--;
}
else
{
parent->_right = cur;
parent->_bf++;
}
while (parent)//进行bf的处理
{
if (parent->_bf == 0)//如果父亲bf==0,终止影响
break;
else if (parent->_bf == 1 || parent->_bf == -1)//父bf 1 or-1
{//继续向上影响,更新父子节点的位置
cur = parent;
parent = cur->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)//需旋转
{
//树结构已经发生问题,不满足AVL树的特性,需要进行处理
}
}
}
return true;
}
1.4 AVL树的旋转(非健康版)
前面的插入是插入后树中节点bf绝对值都不大于1的情况,那么如果有节点的bf绝对值大于1怎么办呢?
这时就需要对其进行处理,所谓处理就是将树的部分进行旋转,使其满足AVL树的特性。
我们来看看。
这里是右旋转。可以看到,右旋转的发生是当cur->bf==-1&&parent->bf==-2时。
1. 右旋图解

右旋代码
cpp
void RotateR(Node* parent)//右单旋
{
//记录会进行迁移的节点
Node* cur = parent->left;
Node* CR = cur->_right;
parent->_left = CR;
if (CR)
CR->_parent = parent;
cur->_right = parent;
Node* grandfather = parent->_parent;//记录parent的父亲
parent->_parent = cur;
if (parent == _root)//parent为根
{
_root = cur;
cur->_parent = nullptr;
}
else//不为根
{
if (parent == grandfather->_left)//判断parent的位置
grandfather->_left = cur;
else
grandfather->_right = cur;
cur->parent=grandfather;
}
cur->_bf = 0;//更新bf
parent->_bf = 0;
}
2. 左旋图解
这里是左旋转,左旋转的发生是当cur->bf==1&&parent->bf==2时的。

左旋代码
cpp
void RotateL(Node* parent)//左单旋
{
//记录会进行迁移的节点
Node* cur = parent->_right;//cur节点
Node* CL = cur->_left;//cur的左孩子
parent->_right = CL;//cur->_left变成parent->_right
if (CL)//如果CL存在,更新其父亲,以免触发空指针的解引用
CL->_parent = parent;
cur->_left = parent;//parent给cur->_left
Node* grandfather = parent->_parent;//先记录parent的父亲
parent->_parent = cur;//更新parent的父亲
if (parent == _root)//parent为根,将cur置为_root,其父亲为空
{
_root = cur;
cur->_parent = nullptr;
}
else//不为根,更新其父亲
{
if (parent == grandfather->_left)
grandfather->_left = cur;
else
grandfather->_right = cur;
cur->parent=grandfather;
}
cur->_bf = 0;
parent->_bf = 0;
}
难道就仅限于此了吗?当然不是,我们接着看。
当类似以下结构就需要双旋转了,即parent->bf绝对值为2,cur->bf绝对值为1,且二者一正一负时就需要双旋转。
下图为parent==2&&cur->bf==-1,需要右左双旋。
也就是说当parent==-2&&cur->bf==1需要左右双旋。
3. 右左双旋图解
cur->left->bf==1时

而图中cl即cur->left的bf不同,会产生不同的效果,因为cur->left的子树最终会分别成为parent与cur的子树 .
cur->left->bf==1时

右左双旋代码
cpp
void RotateRL(Node* parent)//右左双旋
{
Node* cur = parent->_right;
Node* CL = cur->_left;
int bf = CL->_bf;
RotateR(cur);
RotateL(parent);
CL->_bf = 0;
if (bf == -1)
{
cur->_bf = -1;
parent->_bf = 0;
}
else if (bf == 1)
{
cur->_bf = 0;
parent->_bf = 1;
}
else
{
cur->_bf = 0;
parent->_bf = 0;
}
}
4. 左右双旋图解
cur->right->bf==1时

cur->right->bf==-1时

左右双旋代码
cpp
void RotateLR(Node* parent)//左右双旋
{
Node* cur = parent->_left;
Node* CR = cur->_right;
int bf = CR->_bf;
RotateL(cur);
RotateR(parent);
CR->bf = 0;
if (bf == -1)
{
cur->_bf = 0;
parent->bf = 1;
}
else if (bf == 1)
{
cur->_bf = -1;
parent->_bf = 0;
}
else
{
cur->_bf = 0;
parent->_bf = 0;
}
总结
假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑
- pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR
当pSubR的平衡因子为1时,执行左单旋
当pSubR的平衡因子为-1时,执行右左双旋
- pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL
当pSubL的平衡因子为-1是,执行右单旋
当pSubL的平衡因子为1时,执行左右双旋
旋转完成后,原pParent为根的子树个高度降低,已经平衡,不需要再向上更新。
1.5 AVL树的验证
1.5.1 二叉搜索树的验证
只要中序遍历可以得到一个有序序列,就说明是二叉搜索树。
1.5.2 AVL树的验证
AVL树的验证:
每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
节点的平衡因子是否计算正确
验证代码
cpp
bool _IsBanlance(Node* cur, int& height)
{
if (cur == nullptr)
{
return true;
height = 0;
}
int leftheight = 0, rightheight = 0;//记录每棵树的左树与右树高度
if (!_IsBanlance(cur->_left, leftheight) || !_IsBanlance(cur->_right, rightheight))//有任何一棵子树不满足条件
return false;
int SubHeight = rightheight - leftheight;//每个节点其下树的高度差都是他的平衡因子
if (abs(SubHeight) > 1)//高度差
{
cout << cur->_kv.first <<":"<<cur->_bf<< endl;
cout << "高度差错误,不平衡" << endl;
return false;
}
height = leftheight > rightheight ? leftheight + 1 : rightheight + 1;//更新高度
return true;
}
1.6 AVL树的删除
因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置
1.7 AVL树的性能
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即logN。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。