目录
一. AVL树的概念
二. AVL树节点的定义
三. AVL树的插入(重点)
四. AVL树的旋转
1.新节点插入较高右子树的右侧——左单旋
2. 新节点插入较高左子树的左侧——右单旋
3. 新节点插入较高左子树的右侧——左右双旋
4. 新节点插入较高右子树的左侧——右左双旋
五. AVL树的验证
五. AVL树的性能
六. AVL树的删除(了解向)
七. AVL树的完整实现代码
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查 找元素相当于在顺序表中搜索元素,效率低下。
因此,两位俄罗斯的数学家G.M.Adelson-Velskii 和E.M.Landis在1962年发明了一种解决上述问题的方法:
当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
由此,该树被称为AVL树,即两位科学家名字的第一个字母。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在
O(logN),搜索时间复杂度O(logN)。
实现的是kv结构:
template
struct AVLTreeNode
{
pair _kv;
AVLTreeNode* _left;
AVLTreeNode* _right;
AVLTreeNode* _parent;
int _bf;//平衡因子
//AVL树并没有规定必须要选择设计平衡因子,只是一个实现的选择,方便控制
//构造函数
AVLTreeNode(const pair& kv)
:_kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
{}
};
AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。AVL树的插入过程可以分为两步:
按二叉搜索树的方式插入新节点,规则和二叉搜索一致
bool insert(const pair& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
_root->_bf = 0;
return true;
}
//遍历查找插入节点
Node* parent = nullptr;
Node* cur = _root;
while (cur != nullptr)
{
parent = cur;
if (cur->_kv.first < kv.first)
cur = cur->_right;
else if (cur->_kv.first > kv.first)
cur = cur->_left;
else
return false;
}
//将节点插入树中
cur = new Node(kv);
if (parent->_kv.first > kv.first)
parent->_left = cur;
else
parent->_right = cur;
//将新插入节点的_parent被parent赋值
cur->_parent = parent;
//平衡二叉搜索树
while (parent != nullptr)
{
//更新父节点的平衡因子
if (parent->_left == cur)
parent->_bf--;
else
parent->_bf++;
//判断是否需要继续更新
//1或-1说明高度变了,需要继续更新祖先节点的平衡因子
if (parent->_bf == 1 || parent->_bf == -1)
{
cur = cur->_parent;
parent = parent->_parent;
}
//0说明平衡了,不需要继续往上更新
else if (parent->_bf == 0)
{
break;
}
//2和-2,不符合规则,需要旋转
else if (parent->_bf == 2 || parent->_bf == -2)
{
//右边高,左单旋
if (parent->_bf == 2 && cur->_bf == 1)
RotateL(parent);
//左边高右单旋
else if (parent->_bf == -2 && cur->_bf == -1)
RotateR(parent);
//先
else if (parent->_bf == -2 && cur->_bf == 1)
RotateLR(parent);
else if (parent->_bf == 2 && cur->_bf == -1)
RotateRL(parent);
// 旋转完成后,整棵树已经是平衡搜索二叉树,跳出循环
break;
}
else
{
//走到这说明这颗二叉树本来就有子树不满足平衡二叉树
assert(false);
}
}
}
新节点插入后,AVL树的平衡性可能会遭到破坏,需要更新平衡因子,并检测是否破坏了AVL树的平衡性:
在cur位置插入后,parent的平衡因子一定需要调整,在插入之前,parent的平衡因子分为三种情况:-1,0,1,分以下两种情况:
- cur插入到parent的左侧,parent的平衡因子-1
- cur插入到parent的右侧,parent的平衡因子+1
做完上述操作parent的平衡因子可能有三种情况:0,正负1,正负2
- 如果parent的平衡因子为0,说明插入前parent的平衡因子为正负1,插入后被调整成0,满足AVL树的性质,插入成功
- parent的平衡因子为正负1,说明插入前parent的平衡因子一定为0,插入后被更新成正负1,以parent为根的树的高度增加,继续向上更新平衡因子
- parent的平衡因子为正负2,parent的平衡因子违反平衡树的性质,需要进行旋转处理
//更新平衡因子
while (parent != nullptr)
{
//更新父节点的平衡因子
if (parent->_left == cur)
parent->_bf--;
else
parent->_bf++;
//判断是否需要继续更新
//1或-1说明高度变了,需要继续更新祖先节点的平衡因子
if (parent->_bf == 1 || parent->_bf == -1)
{
cur = cur->_parent;
parent = parent->_parent;
}
//0说明平衡了,不需要继续往上更新
else if (parent->_bf == 0)
{
break;
}
//2和-2,不符合规则,需要旋转
else if (parent->_bf == 2 || parent->_bf == -2)
{
//进行旋转处理
}
else
{
//走到这说明这颗二叉树本来就有子树不满足平衡二叉树
assert(false);
}
}
如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构, 使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:
旋转时需要始终记住旋转的原则:
当插入到树中的数据如果为一个升序的数组,二叉搜索树会退化成一个只有右子树的单支树,效率大幅度下降,此时就会需要使用到左单旋对二叉搜索树进行平衡调整。
先看下面的几个插入:
当新增的节点如上图所示,新增节点为60的右节点时,此时就会违反平衡树的规则,右子树的右侧较高,需要进行左单旋。
左单旋的方式:
以30为旋转点,将60的左子树给30的右子树,30变成60的左子树
就变成了第三幅图的样子。
当新增节点为60右节点的右节点时,此时会违反平衡树的规则,右子树的右侧较高,需要进行左单旋。
左单旋的方式:
以30为旋转点,将60的左子树给30的右子树,30变成60的左子树
由于高度和节点多了会导致的情况变的很复杂,有了以上的先例,
使用具象图模板来表示可能出现的情况,用以下的具象图来表示,用长方形条来表示子树,
左单旋模板:
当子树的高度为h时,在b/c子树插入新增节点时,由于右子树比较高,需要左单旋,将b给30的右子树,30变成60的左子树。
【因为b子树中的节点的值一定在30~60之间,所以能够满足二叉搜索树的规则,旋转后60的左右子树的高度相同,达到了平衡树的要求】
因为只有30和60子树的高度变了,并且两者的左右子树高度相同,所以平衡因子也应该更新为0
代码实现如下:
//右边高左单旋
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
//将根的右子树的左子树赋值给根的右子树
parent->_right = subRL;
//subRL为空不能访问
if(subRL)
subRL->_parent = parent;
//将根节点变成根的右子树的左子树
subR->_left = parent;
//更新根的右子树的父节点
subR->_parent = parent->_parent;
//如果parent是根节点需要更新根节点
if (parent == _root)
{
_root = subR;
subR->_parent = nullptr;
}
//如果parent上面还有祖先节点,需要更新祖先节点的左节点/右节点
else
{
if (parent->_parent->_left == parent)
parent->_parent->_left = subR;
else
parent->_parent->_right = subR;
}
//更新根的父节点
parent->_parent = subR;
//更新旋转后两个节点的平衡因子
subR->_bf = parent->_bf = 0;
}
需要注意的是由于是三叉链实现,所以要更新父节点指针,注意subRL可能为空树,以及parent可能是根节点,也可能是一颗子树,注意更新根节点和parent的_parent的孩子节点指针,parent的_parent最后更新是因为更新其他节点的父节点还需要使用
当插入到树中的数据如果为一个降序的数组,二叉搜索树会退化成一个只有左子树的单支树,效率大幅度下降,此时就会需要使用到右单旋对二叉搜索树进行平衡调整。
方式和左单旋类似,
右单旋模板:
当子树高度为h时,在a/b子树插入新增节点时,由于左子树比较高,需要右单旋,将b给60的左子树,60变成30的右子树。
【因为b子树中节点的值一定是在30到60之间的,改变后仍然满足二叉搜索树的规则,旋转后30的左右子树高度相同,满足平衡树的要求】
因为只有30和60子树的高度变了,并且两者的左右子树高度相同,所以平衡因子也应该更新为0
代码实现如下:
//左边高右单旋
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
//根节点的左指针指向左子树的右子树
parent->_left = subLR;
//如果根节点的左子树的右子树不为空则更新_parent
if (subLR)
subLR->_parent = parent;
//根节点的左子树的右指针指向parent
subL->_right = parent;
//更新subL的_parent
subL->_parent = parent->_parent;
//处理parent是根节点和不是根节点的情况
//如果parent是根节点,则赋值给_root
if (parent == _root)
{
_root = subL;
subL->_parent = nullptr;
}
//否则链接上祖先节点
else
{
//确定是祖先节点的左还是右,并链接上
if (parent->_parent->_left == parent)
parent->_parent->_left = subL;
else
parent->_parent->_right = subL;
}
//更新parent的_parent
parent->_parent = subL;
//更新平衡因子
subL->_bf = parent->_bf = 0;
}
需要注意的是由于是三叉链实现,所以要更新父节点指针,注意subLR可能为空树,以及parent可能是根节点,也可能是一颗子树,注意更新根节点和parent的_parent的孩子节点指针,parent的_parent最后更新是因为更新其他节点的父节点还需要使用
左右双旋是针对当从随机数里读取数据时仍然要满足二叉平衡搜索树的规则,即在乱序的情况下能够保持是二叉平衡搜索树。
双旋的情况比较特殊,左右双旋处理当一开始左子树的右子树较高的情况,就会需要先左单旋,然后再右单旋,由于双旋也是由单旋衍生出的,所以和单旋一样,当树中的结点变多时不好去讨论情况,所以也是采用具象图的方式进行解释
左右双旋模板:
当新增节点插入在b时,导致左子树的右侧较高,进行左单旋,以30为旋转点,将b给30的右子树,30变成60的左子树,就会变成单纯的左子树左侧较高,再进行右单旋,以90为旋转点,将c给90的左子树,90变成60的右子树。
此时,第四幅图的60左右子树高度是相等的,即60的左右子树是平衡的,60的平衡因子是0,30的左右子树高度相等,即30的左右子树是平衡的,30的平衡因子是0,90的左子树比右子树矮一层,即90的左右子树是平衡的,但是90的平衡因子是1
当新增节点插入在c时,导致左子树的右侧较高,进行左单旋,以30为旋转点,将b给30的右子树,30变成60的左子树,就会变成单纯的左子树左侧较高,再进行右单旋,以90为旋转点,将c给90的左子树,90变成60的右子树。
此时,第四幅图的60左右子树高度是相等的,即60的左右子树是平衡的,60的平衡因子是0,90的左右子树高度相等,即90的左右子树是平衡的,90的平衡因子是0,30的右子树比左子树矮一层,即30的左右子树是平衡的,但是30的平衡因子是-1
当60为新增节点插入在30的右子树时,导致左子树的右侧较高,进行左单旋,以30为旋转点,将60的左子树(nullptr)给30的右子树,30变成60的左子树,就会变成单纯的左子树左侧较高,再进行右单旋,以90为旋转点,将60的右子树(nullptr)给90的左子树,90变成60的右子树。
此时,第三幅图的60左右子树高度是相等的,即60的左右子树是平衡的,60的平衡因子是0,30的左右子树高度相等,即30的左右子树是平衡的,30的平衡因子是0,90的左右子树高度相等,即90的左右子树是平衡的,90的平衡因子是0
左右双旋也就只有上面三种情况,因为无论是从a子树(右单旋)还是d子树(平衡)亦或者是在30的左子树(右单旋)插入引发的都不是双旋。
由此通过上述讨论,可以发现虽然30,60,90的高度都变了,需要更新平衡因子,但是也就上面三种更新情况,即当subLR(60节点)分别为-1,0,1的情况,对应上面的在60的左侧插入新增节点,60为新增节点,在60的右侧插入新增节点,分三种情况讨论更新平衡因子即可。
代码如下:
//先右边高,左单旋后左边高,右单旋
void RotateLR(Node* parent)
{
//记录下根节点的左子树和根节点的左子树的右子树
Node* subL = parent->_left;
Node* subLR = subL->_right;
//记录下根节点的左子树的右子树的平衡因子,后续将三个平衡因子进行更改需要使用
int bf = subLR->_bf;
//先进行左单旋再进行右单旋
RotateL(parent);
RotateR(parent);
//更新parent,subL,subLR的平衡因子
//当在subLR的左子树插入新节点导致左比右高,进而导致平衡因子变成-1,在进行左右双旋后平衡因子的更新
if (bf == -1)
{
parent->_bf = 1;
subLR->_bf = 0;
subL->_bf = 0;
}
//当在subLR的右子树插入新节点导致右比左高,进而导致平衡因子变成1,在进行左右双旋后平衡因子的更新
else if (bf == 1)
{
parent->_bf = 0;
subLR->_bf = 0;
subL->_bf = -1;
}
//当在subLR这颗子树就是新插入的节点,平衡因子是0,在进行左右双旋后平衡因子的更新
else if(bf == 0)
{
parent->_bf = 0;
subLR->_bf = 0;
subL->_bf = 0;
}
//走到这说明旋转前就有问题
else
{
//subLR的_bf旋转前就已经出现问题
assert(false);
}
}
因为我们根据subLR这个节点来更新subLR,subL,parent的平衡因子,而在进行左单旋和右单旋,我们会将subL和subLR的平衡因子更新为0,所以要先存一份。经历完左右双旋后根据存下来的这一份平衡因子来判断我们应该怎么更新三者的平衡因子。
右左双旋是针对当从随机数里读取数据时仍然要满足二叉平衡搜索树的规则,即在乱序的情况下能够保持是二叉平衡搜索树。
双旋的情况比较特殊,右左双旋处理当一开始右子树的左子树较高的情况,就会需要先右单旋,然后再左单旋,由于双旋也是由单旋衍生出的,所以和单旋一样,当树中的结点变多时不好去讨论情况,右左双旋和左右双旋类似,所以也是采用具象图的方式进行解释
当新增节点插入在c时,导致右子树的左侧较高,进行右单旋,以90为旋转点,将c给90的左子树,90变成60的右子树,就会变成单纯的右子树右侧较高,再进行左单旋,以30为旋转点,将b给30的右子树,30变成60的左子树。
此时,第四幅图的60左右子树高度是相等的,即60的左右子树是平衡的,60的平衡因子是0,30的左子树比右子树高度一层,即30的左右子树是平衡的,但是30的平衡因子是-1,90的左右子树高度相等,即90的左右子树是平衡的,90的平衡因子是0
当新增节点插入在b时,导致右子树的左侧较高,进行右单旋,以90为旋转点,将c给90的左子树,90变成60的右子树,就会变成单纯的右子树右侧较高,再进行左单旋,以30为旋转点,将b给30的右子树,30变成60的左子树。
此时,第四幅图的60左右子树高度是相等的,即60的左右子树是平衡的,60的平衡因子是0,30的左右子树高度相等,即30的左右子树是平衡的,30的平衡因子是0,90的右子树高度比左子树高一层,即90的左右子树是平衡的,但是90的平衡因子是1
当60为新增节点插入在30的左子树时,导致右子树的左侧较高,进行右单旋,以30为旋转点,将60的右子树(nullptr)给30的左子树,30变成60的右子树,就会变成单纯的右子树右侧较高,再进行左单旋,以90为旋转点,将60的左子树(nullptr)给90的右子树,90变成60左子树。
此时,第三幅图的60左右子树高度是相等的,即60的左右子树是平衡的,60的平衡因子是0,30的左右子树高度相等,即30的左右子树是平衡的,30的平衡因子是0,90的左右子树高度相等,即90的左右子树是平衡的,90的平衡因子是0
右左双旋也就只有上面三种情况,因为无论是从a子树(平衡)还是d子树(左单旋)亦或者是在30的右子树(左单旋)插入引发的都不是双旋。
由此通过上述讨论,可以发现虽然30,60,90的高度都变了,需要更新平衡因子,但是也就上面三种更新情况,即当subRL(60节点)分别为-1,0,1的情况,对应上面的在60的左侧插入新增节点,60为新增节点,在60的右侧插入新增节点,分三种情况讨论更新平衡因子即可。
代码如下所示:
//先左边高,左单旋后右边高,左单旋
void RotateRL(Node* parent)
{
//记录下根节点的右子树和根节点的右子树的左子树
Node* subR = parent->_right;
Node* subRL = subR->_left;
//记录下根节点的右子树的左子树的平衡因子,后续将三个平衡因子进行更改需要使用
int bf = subRL->_bf;
//进行右左双旋
RotateR(parent);
RotateL(parent);
//更新parent,subR和subRL的平衡因子
//当在subRL的右子树插入新节点导致右比左高,进而导致平衡因子变成1,在进行左右双旋后平衡因子的更新
if (bf == 1)
{
parent->_bf = -1;
subR->_bf = 0;
subRL->_bf = 0;
}
//当在subRL的左子树插入新节点导致左比右高,进而导致平衡因子变成-1,在进行左右双旋后平衡因子的更新
else if (bf == -1)
{
parent->_bf = 0;
subR->_bf = 1;
subRL->_bf = 0;
}
//当在subRL这颗子树就是新插入的节点,平衡因子是0,在进行左右双旋后平衡因子的更新
else if (bf == 0)
{
parent->_bf = 0;
subR->_bf = 0;
subRL->_bf = 0;
}
else
{
assert(false);
}
}
因为我们根据subRL这个节点来更新subRL,subR,parent的平衡因子,而在进行右单旋和左单旋,我们会将subL和subLR的平衡因子更新为0,所以要先存一份。经历完右左双旋后根据存下来的这一份平衡因子来判断我们应该怎么更新三者的平衡因子。
综上所述:
假如以parent为根的子树不平衡,即parent的平衡因子为2或者-2,分以下情况考虑:
1. parent的平衡因子为2,说明parent的右子树高,设parent的右子树的根为subR
- 当subR的平衡因子为1时,执行左单旋
- 当subR的平衡因子为-1时,执行右左双旋
2. parent的平衡因子为-2,说明parent的左子树高,设parent的左子树的根为subL
- 当subL的平衡因子为-1是,执行右单旋
- 当subL的平衡因子为1时,执行左右双旋
旋转完成后,原parent为根的子树个高度降低,已经平衡,不需要再向上更新。
AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:
1. 验证其为二叉搜索树
2. 验证其为平衡树
使用以下代码进行测试:
public:
//中序遍历
void inoder()
{
_inoder(_root);
}
//返回二叉树的高度
int Height()
{
return _height(_root);
}
//判断是否是AVL树
bool IsBalanceTree()
{
return _IsBalanceTree(_root);
}
//层序遍历
vector> levelOrder()
{
vector> vv;
if (_root == nullptr)
return vv;
queue q;
int levelSize = 1;
q.push(_root);
while (!q.empty())
{
// levelSize控制一层一层出
vector levelV;
while (levelSize--)
{
Node* front = q.front();
q.pop();
levelV.push_back(front->_kv.first);
if (front->_left)
q.push(front->_left);
if (front->_right)
q.push(front->_right);
}
vv.push_back(levelV);
for (auto e : levelV)
{
cout << e << " ";
}
cout << endl;
// 上一层出完,下一层就都进队列
levelSize = q.size();
}
return vv;
}
private:
//中序遍历
void _inoder(Node* root)
{
if (root == nullptr)
return;
_inoder(root->_left);
cout << root->_kv.first << " ";
_inoder(root->_right);
}
//求二叉平衡树的高度
int _height(Node* root)
{
if (root == nullptr)
return 0;
int left = _height(root->_left);
int right = _height(root->_right);
return left > right ? left + 1 : right + 1;
}
//判断是否是AVL树
bool _IsBalanceTree(Node* root)
{
//空树也是AVL树
if (nullptr == root)
return true;
//计算root节点的平衡因子:即root左右子树的高度差
int leftHeight = _height(root->_left);
int rightHeight = _height(root->_right);
int diff = rightHeight - leftHeight;
//如果计算出的平衡因子与root的平衡因子不相等,或者
//root平衡因子的绝对值超过1,则一定不是AVL树
if (abs(diff) >= 2)
{
cout << root->_kv.first << "节点平衡因子异常" << endl;
return false;
}
if (diff != root->_bf)
{
cout << root->_kv.first << "节点平衡因子不符合实际" << endl;
return false;
}
//root的左和右如果都是AVL树,则该树一定是AVL树
return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}
并输入以下数据进行测试:
常规用例:
16, 3, 7, 11, 9, 26, 18, 14, 15
特殊用例:
4, 2, 6, 1, 3, 5, 15, 7, 16, 14
VL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这 样可以保证查询时高效的时间复杂度,即O(logN)。但是如果要对AVL树做一些结构修改的操 作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时, 有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。
因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不过与删除不同的是删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。
删除了节点,所以往上更新的平衡都应该-1,直至遇到不平衡的节点进行旋转或者更新到节点的平衡因子为-1/1时停止,否则将一路更新到根节点,左子树改变-(-1)就需要加1,而右子树改变就应该-1。
注:更新到-1或者1的时候停止是因为,能更新到-1或者1的节点,以该节点为根节点的子树本来就是平衡,即根节点的平衡因子是0,当删除一个其中一个节点后仍然满足平衡树(前提是删除的这个节点满足二叉搜索树的要求)的要求
有如上AVL树:
当删除的节点为9时
此时应该8节点的右指针指向9的孩子节点,再链接9后面的子树,并且需要从8节点往上更新各个节点的平衡因子,因为删除了节点且为8的右子树,所以往上更新都应该-1,一路更新到根节点
当删除的节点为6时
此时就应该反过来,由于是左节点,所以就应该+1,再链接6节点后的子树,由于以7为根节点的子树不再平衡,所以应该进行旋转,由于是左高,所以应该进行右单旋
当删除的节点为2时
删除了1的右节点,由于1的平衡因子本来是0,所以减一后是-1,说明子树平衡,不需要向上更新平衡因子,只需要链接2节点后的子树即可
以上只是列了几种的情况,真正讨论起来,情况复杂很多,并且细节也很多,这里不过多赘述。
具体实现参考《算法导论》或《数据结构-用面向对象方法与C++描述》殷人昆版。
以下是AVL树完整代码的展示:
#pragma once
#include
#include
#include