当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1,达到高度平衡,即可降低树的高度,从而减少平均搜索长度。即如果一棵二叉搜索树的任意节点左右子树高度差绝对值都<=1,它就是AVL树。
空树也算AVL树,AVL树一般具有一下性质:
AVL树是在搜索树的基础上进行平衡优化后的结果。对一颗搜索树而言,如果数据有序或接近有序搜索树将退化为单支树,这时查询的时间复杂度将变为线性。二AVL树通过旋转操作使得树总是保持平衡,保证查询时高效的时间复杂度。类比AVL树的作用就像快排的三数取中一样解决了快排在数据有序或接近有序时的一趟遍历才能排好一个数的问题,一样是保证了效率。
AVL树主要包括节点类AVLNode和树的本体AVLTree,AVLNode主要存放存储的数据和指向自己亲属的指针,而AVLTree里面主要是实现AVL树的功能:包括插入节点、删除节点等等。
节点类框架
这里我们存储的数据类型是pair,所以要传两个模板参数k和v分别对应键值对中key和value的类型
template<class k,class v>
struct AVLNode
{
// 构造函数
AVLNode(const pair<k,v>& kv)
:_parent(nullptr)
, _left(nullptr)
, _right(nullptr)
, _kv(kv)
, _bf(0)
{}
AVLNode<k,v>* _parent;// 指向父亲节点
AVLNode<k,v>* _left; // 指向左孩子
AVLNode<k,v>* _right; // 指向右孩子
pair<k,v> _kv; // 数据域,即键值对
int _bf; // 平衡因子:右子树高度减左子树高低
};
树的本体框架
成员变量只有一个就是根节点
template<class k, class v>
class AVLTree
{
public:
typedef AVLNode<k,v> Node;
private:
Node* _root=nullptr;//根节点
};
PS:该类只有一个成员变量,没必要特意写构造函数,可以直接在声明_root处赋值为nullptr。这是有必要的,不然会因为后面代码逻辑的原因而造成野指针的访问进而导致结果出问题。
比如我们在插入一个节点时,首先要判断该树是否为空,即_root是否等于nullptr,如果一开始我们没有给根节点初始化的话它就是个非空的随机值,
代码逻辑就会以为不是空树进而导致后面结果出错。
AVL树的插入大体分为两步:
插入成功返回true,失败返回false。传入的参数为键值对,调用时我们可以make_pair创造一个临时的键值对传入。
分两种情况处理:
bool Insert(const pair<k,v>& kv)
{
// 插入节点
// 1、空树的话,就让插入的那个节点作为根
if(!_root)
{
_root=new Node(kv);
return true;
}
// 2、不是空树,就按照搜索树的性质找到插入的位置和它的父亲
Node* cur=_root;
Node* parent=nullptr;
while(cur)
{
parent=cur;
if(cur->_kv.first==kv.first)
{
return false;
}
else if(cur->_kv.first > kv.first)
{
cur=cur->_left;
}
else
{
cur=cur->_right;
}
}
// 创建要插入的节点
Node* newNode=new Node(kv);
// 更新关系,插入节点
newNode->_parent=parent;
if(parent->_kv.first < newNode->_kv.first)
{
parent->_right=newNode;
}
else
{
parent->_left=newNode;
}
// 未完待续....
return true;
}
补充说明
因为节点的数据是键值对所以我们传的参数是pair,为了节省空间我们传引用,传入后我们会依照传入的pair完成深拷贝,整个过程不涉及到修改原pair所以我们用const修饰。
这里用const修饰还有另外一个原因,我们在调用传参时通常用make_pair(),这里会生成一个临时的pair对象,临时变量具有常性,想要引用必须加const
下面我们看看这个临时的pair对象从调用时的传入到最后通过深拷贝生成另外一个pair对象所经历的整个过程:
旋转的前因后果
根据:平衡因子 = 右子树高度 - 左子树高度 我们可以得到以下结论:
左单旋
插入后parent的平衡因子为2,说明右边高,右孩子的平衡因子为1也是右边高,需要对parent进行左单选,使左边高度增加,右边高度降低。设parent的右子树的根为subR,右子树的左子树的根为subRL
操作:把subRL作为parent的右孩子,parent作为subR的左孩子。
注意:subRL可能为空,parent可能为整棵树的根节点,这时它的父亲为空。
结果:不仅平衡了而且依然满足搜索树的性质。只有parent和subR这两个节点的连接关系发生改变,其他都没变。旋转完成后它们两个的平衡因子变为0
void RotateL(Node* parent)
{
Node* subR=parent->_right;
Node* subRL=subR->_left;
// 更新节点之间的连接关系
parent->_right=subRL;
if(subRL)// subRL不为空才需要更新它的父亲
{
subRL->_parent=parent;
}
subR->_left=parent;
Node* pparent=parent->_parent;
parent->_parent=subR;
if(!pparent)// parent为根时的处理
{
_root=subR;
subR->_parent=nullptr;
}
else
{
if(pparent->_left==parent)
{
pparent->_left=subR;
}
else
{
pparent->_right=subR;
}
subR->_parent=pparent;
}
// 更新平衡因子
parent->_bf=subR->_bf=0;
}
右单旋
parent的平衡因子为-2,说明parent的左子树高,左孩子的平衡因子为-1,也是左边高。设parent的左子树的根为subL,左子树的右子树的根为subLR
操作:把subLR给parent作为左孩子,然后parent作为subL的右孩子
注意:代码实现时要注意subLR可能为空,并且最后subL要连接上原来parent的父亲,当parent为根节点时它的父亲为nullptr
结果:不仅平衡了而且依然满足搜索树的性质。只有parent和subL的连接关系方式过改变,最终他们的平衡因子都为0
// 右单旋
void RotateR(Node* parent)
{
Node* subL=parent->_left;
Node* subLR=subL->_right;
// 更新节点之间的连接关系
parent->_left=subLR;
if(subLR)
{
subLR->_parent=parent;
}
subL->_right=parent;
Node* pparent=parent->_parent;
parent->_parent=subL;
if(!pparent)
{
_root=subL;
subL->_parent=nullptr;
}
else
{
if(pparent->_left==parent)
{
pparent->_left=subL;
}
else if(pparent->_right==parent)
{
pparent->_right=subL;
}
subL->_parent=pparent;
}
// 更新平衡因子
parent->_bf=subL->_bf=0;
}
左右双旋 和 右左双旋
两者都差不多,从最终结果看都是把subLR或subRL的两个子树分别分给它的父亲和爷爷,然后自己作为根节点。subLR或subRL的平衡因子决定了最后它父亲和爷爷的平衡因子的值,因为它要把自己的两个子树交给父亲和爷爷,它的平衡因子决定了它两个子树的高度,间接影响了它父亲和它爷爷的高度。
以左右双旋为例,它的情景包括三种,对应插入新节点后subLR的平衡因子可能为 -1、0、1这三种情况。
情况一:插入后subLR的平衡因子变为-1
情景二:插入后subLR的平衡因子变为1
PS:右左双旋的操作也大同小异,就不在演示了,只是平衡因子的处理结果相反:如果subRL的平衡因子为1,它爷爷的平衡因子为-1;如果subRL的平衡因子为-1,那么他父亲的平衡因子为1,;如果为0,那么它们祖孙三个也都是0
代码实现
注意:因为里面复用了单旋的接口,经过两次单旋后,祖孙三个节点的平衡因子都在被单旋操作里置为0了,所以最后我们只需特殊处理父亲或爷爷节点的平衡因子就行。
// 左右双旋
void RotateLR(Node* parent)
{
Node* subL=parent->_left;
Node* subLR=subL->_right;
int flag=subLR->_bf;// 记录subLR的平衡因子,最后要依据它来更新其他节点的平衡因子
// 依次旋转
RotateL(subL);
RotateR(parent);
// 根据subLR平衡因子的值更新不同插入情况下的平衡因子
if(flag==1)// 说明是在subLR的右子树插入的,那么subLR的左子树变为subL的右子树,subL平衡因子变为-1,subLR和parent的为0
{
subL->_bf==-1;
}
else if(flag==-1)// 说明是在subLR的左子树插入的,subLR的右子树最后会被分给parent作为左子树,parent的平衡因子变为-1,subL和subLR的平衡因子变为0
{
parent->_bf==1;
}
}
// 右左双旋
void RotateRL(Node* parent)
{
Node* subR=parent->_right;
Node* subRL=subR->_left;
int flag=subRL->_bf;
// 依次旋转
RotateR(subR);
RotateL(parent);
// 更新平衡因子
if(flag==1)
{
parent->_bf==-1;
}
else if(flag==-1)
{
subR->_bf==1;
}
}
总结
当以parent为根的子树不平衡,也就是parent的平衡因子为2或者-2时,会根据不同的平衡原因来进行不同的旋转操作:
①:parent的平衡因子为2,说明parent的右子树高,设parent的右子树的根为subR,右子树的左子树的根为subRL
②:parent的平衡因子为-2,说明parent的左子树高,设parent的左子树的根为subL,左子树的右子树的根为subLR
检查树是否平衡,对不平衡的树进行旋转处理,使其平衡并更新节点的平衡因子。下面的代码需要连接到第一步操作的代码的后面:
// 衔接到上面最后的未完待续哪里
// 更新节点的平衡因子
cur=newNode;
parent=cur->_parent;
while(parent)
{
// 向上更新平衡因子
if(cur==parent->_left)
{
--(parent->_bf);
}
else
{
++(parent->_bf);
}
// 检查是否需要调整
// 0的话就平衡了
// -1或1的话还要向上更新
// -2或2的话需要旋转处理
if(parent->_bf==0)// 平衡因子为0,整棵树高度依然不变,只是补了原来低的那边,依然平衡
{
break;
}
else if(parent->_bf==1 || parent->_bf==-1)// 整棵树高度增加了,但是这颗树依然平衡,再往上是否平衡不知道需要继续验证
{
cur=parent;
parent=parent->_parent;
}
else if(parent->_bf==2 || parent->_bf==-2)
{
// 右子树高
if(parent->_bf==2)
{
if(cur->_bf==1)// 右子树的右子树也高 --> 左单旋
{
RotateL(parent);
}
else if(cur->_bf==-1)// 右子树的左子树也高 --> 右左双旋
{
RotateRL(parent);
}
}
else if(parent->_bf==-2)// 左子树高
{
if(cur->_bf==-1)// 左子树的左子树也高 --> 右单旋
{
RotateR(parent);
}
else if(cur->_bf==1)// 左子树的右子树也高 --> 左右双旋
{
RotateLR(parent);
}
}
break;
}
}
为了方便浏览我们把前面的框架、插入和旋转的代码整合起来
// 树节点的定义
template<class k, class v>
struct AVLNode
{
// 构造一个节点
AVLNode(const pair<k,v>& kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
,_bf(0)
{}
AVLNode<k,v>* _left;
AVLNode<k,v>* _right;
AVLNode<k,v>* _parent;
pair<k,v> _kv;
int _bf;
};
// 树本体
template<class k, class v>
class AVLTree
{
public:
typedef AVLNode<k,v> Node;
// 插入节点
bool Insert(const pair<k,v>& kv)
{
// 空树的话,就让插入的那个节点作为根
if(!_root)
{
_root=new Node(kv);
return true;
}
// 不是空树,就按照搜索树的性质找到插入的位置和它的父亲
Node* cur=_root;
Node* parent=nullptr;
while(cur)
{
parent=cur;
if(cur->_kv.first==kv.first)
{
return false;
}
else if(cur->_kv.first > kv.first)
{
cur=cur->_left;
}
else
{
cur=cur->_right;
}
}
// 创建要插入的节点
Node* newNode=new Node(kv);
// 更新关系,插入节点
newNode->_parent=parent;
if(parent->_kv.first < newNode->_kv.first)
{
parent->_right=newNode;
}
else
{
parent->_left=newNode;
}
cur=newNode;
parent=cur->_parent;
while(parent)
{
// 向上更新平衡因子
if(cur==parent->_left)
{
--(parent->_bf);
}
else
{
++(parent->_bf);
}
// 检查是否需要调整
// 0的话就平衡了
// -1或1的话还要向上更新
// -2或2的话需要旋转处理
if(parent->_bf==0)// 平衡因子为0,整棵树高度依然不变,只是补了原来低的那边,依然平衡
{
break;
}
else if(parent->_bf==1 || parent->_bf==-1)// 整棵树高度增加了,但是这颗树依然平衡,再往上是否平衡不知道需要继续验证
{
cur=parent;
parent=parent->_parent;
}
else if(parent->_bf==2 || parent->_bf==-2)
{
// 右子树高
if(parent->_bf==2)
{
if(cur->_bf==1)// 右子树的右子树也高 --> 左单旋
{
RotateL(parent);
}
else if(cur->_bf==-1)// 右子树的左子树也高 --> 右左双旋
{
RotateRL(parent);
}
}
else if(parent->_bf==-2)// 左子树高
{
if(cur->_bf==-1)// 左子树的左子树也高 --> 右单旋
{
RotateR(parent);
}
else if(cur->_bf==1)// 左子树的右子树也高 --> 左右双旋
{
RotateLR(parent);
}
}
break;
}
}
return true;
}
private:
// 左单旋
void RotateL(Node* parent)
{
Node* subR=parent->_right;
Node* subRL=subR->_left;
// 更新节点之间的连接关系
parent->_right=subRL;
if(subRL)// subRL不为空才需要更新它的父亲
{
subRL->_parent=parent;
}
subR->_left=parent;
Node* pparent=parent->_parent;
parent->_parent=subR;
if(!pparent)// parent为根时的处理
{
_root=subR;
subR->_parent=nullptr;
}
else
{
if(pparent->_left==parent)
{
pparent->_left=subR;
}
else
{
pparent->_right=subR;
}
subR->_parent=pparent;
}
// 更新平衡因子
parent->_bf=subR->_bf=0;
}
// 右单旋
void RotateR(Node* parent)
{
Node* subL=parent->_left;
Node* subLR=subL->_right;
// 更新节点之间的连接关系
parent->_left=subLR;
if(subLR)
{
subLR->_parent=parent;
}
subL->_right=parent;
Node* pparent=parent->_parent;
parent->_parent=subL;
if(!pparent)
{
_root=subL;
subL->_parent=nullptr;
}
else
{
if(pparent->_left==parent)
{
pparent->_left=subL;
}
else if(pparent->_right==parent)
{
pparent->_right=subL;
}
subL->_parent=pparent;
}
// 更新平衡因子
parent->_bf=subL->_bf=0;
}
// 左右双旋
void RotateLR(Node* parent)
{
Node* subL=parent->_left;
Node* subLR=subL->_right;
int flag=subLR->_bf;// 记录subLR的平衡因子,最后要依据它来更新其他节点的平衡因子
// 依次旋转
RotateL(subL);
RotateR(parent);
// 根据subLR平衡因子的值更新不同插入情况下的平衡因子
if(flag==1)// 说明是在subLR的右子树插入的,那么subLR的左子树变为subL的右子树,subL平衡因子变为-1,subLR和parent的为0
{
subL->_bf==-1;
}
else if(flag==-1)// 说明是在subLR的左子树插入的,subLR的右子树最后会被分给parent作为左子树,parent的平衡因子变为-1,subL和subLR的平衡因子变为0
{
parent->_bf==1;
}
}
// 右左双旋
void RotateRL(Node* parent)
{
Node* subR=parent->_right;
Node* subRL=subR->_left;
int flag=subRL->_bf;
// 依次旋转
RotateR(subR);
RotateL(parent);
// 更新平衡因子
if(flag==1)
{
parent->_bf==-1;
}
else if(flag==-1)
{
subR->_bf==1;
}
}
Node* _root=nullptr;//根节点
};