文章目录
AVL树的概念
二叉搜索树,查找效率很高
,但是当但数据有序或接近有序二叉搜索树
将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下
为了解决上面的问题,就得优化二叉搜索树,让二叉搜索树的高度不要太高,接近平衡(左右子树高度差不要太多)
于是,两位俄罗斯的数学家G.M.A
delson-V
elskii和E.M.L
andis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度
所以AVL树也叫做高度平衡搜索树
AVL树的性质
AVL树其实就是特殊的二叉搜索树
它的特点是:
- 本身首先是一棵二叉搜索树
- 每个结点的
左右子树的高度之差
的绝对值(平衡因子
)不超过1(-1/0/1)
也就是说,AVL树,本质上是带了平衡功能的二叉查找树
如果它有n个结点
,其高度可保持在
O(log2N),搜索时间复杂度
O(log2N)
AVL树的实现
AVL树的实现有多种方式,这里采用三叉链和平衡因子来完成平衡的功能
AVL树节点
采用KV结构存储键值对数据,节点中还包括平衡因子(右子树高度-左子树高度),parent指针,左子树指针,右子树指针,还有节点的构造函数
template<class K, class V>
struct AVLTreeNode {
//三叉链
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
//平衡因子 Balance factor
int _bf;
//存储键值对
pair<K, V> _kv;
public:
//平衡因子初始化为0
AVLTreeNode(const pair<K, V>& kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_bf(0)
,_kv(kv)
{}
};
AVL树的插入
AVL树是引入了平衡因子的二叉搜索树,所以AVL树的插入分为三个部分
- 按照二叉搜索树的插入方式插入新节点
- 更新平衡因子,并检测是否破坏了AVL树的平衡性质
- 如果平衡因子等于2或者-2,说明树的平衡已经出现了问题,需要进行
旋转处理
插入新结点
按照二叉搜索树的插入方式
- 树为空,创建一个结点,直接链接到根结点
- 树不为空,按照二叉搜索树的性质找到插入的位置,再创建结点插入链接
平衡因子的更新和检测
插入节点后,节点所对应的父节点的高度可能会发生变化,所以需要对新节点的父节点的平衡因子进行更新,以便于检测树的平衡性
而往往一个节点的子树高度发生变化后,可能也会影响到祖先节点子树的高度变化,所以平衡因子的更新可能需要不断向上更新
平衡因子的更新规则
- 新增的结点在parent的左边,parent的平衡因子
--
- 新增的结点在parent的右边,parent的平衡因子
++
平衡因子的检测
由于插入新结点可能会引起祖先节点的平衡因子变化,也可能会导致树的不平衡,所以在更新完新增节点parent的平衡因子后,还需要对新增节点到根节点路径上的所有节点的平衡因子进行检测
- parent的平衡因子
等于0
,则插入成功
- parent的平衡因子
等于1或者-1
,还需要继续往上更新平衡因子
- parent的平衡因子
等于2或者-2
,此时以parent结点为根结点的子树已经不平衡了
,需要进行旋转处理
AVL树的旋转
当parent的平衡因子是2或者-2时,此时二叉搜索树已经不平衡了,需要对这个树进行旋转操作,让这个树重新平衡
AVL树的旋转分为四种
,左单旋,右单旋,左右双旋,右左双旋
而平衡因子的四种情况分别对应四种旋转:
- parent的平衡因子为
2
,说明parent的右子树高,设parent的右子树为subR
- 当subR的平衡因子为
1
时,执行左单旋
- 当subR的平衡因子为
-1
时,执行右左双旋
- 当subR的平衡因子为
- parent的平衡因子为
-2
,说明parent的左子树高,设parent的左子树为subL
- 当subR的平衡因子为
-1
时,执行右单旋
- 当subR的平衡因子为
1
时,执行左右双旋
- 当subR的平衡因子为
旋转完成后,原parent为根的子树高度降低,已经平衡,不需要再向上更新
左单旋
如果新增的节点插入到较高右子树的右侧,就要进行左单旋
左单旋的步骤:
- 让subRL
作为
parent的右子树 - 让parent
作为
subR的左子树 - 如果parent
为根结点
,则让zubR作为新的
根结点 - 如果parent
不是根节点
,则维护subR和parent父亲结点的链接关系 - 更新平衡因子
void _RotateL(Node* parent) {
Node* subR = parent->_right;
Node* subRL = subR->_left;
//保存根节点的父节点,用来维护改变后的节点关系
Node* grandParent = parent->_parent;
//让subRL作为parent的右子树
parent->_right = subRL;
if (subRL) {
subRL->_parent = parent;
}
//让parent作为subR的左子树
subR->_left = parent;
parent->_parent = subR;
//检测parent是否为根节点
if (parent == _root) {
_root = subR;
subR->_parent = nullptr;
}
else {
if (grandParent->_left == parent) {
grandParent->_left = subR;
}
else {
grandParent->_right = subR;
}
subR->_parent = grandParent;
}
//更新平衡因子
subR->_bf = parent->_bf = 0;
}
右单旋
如果新增的节点插入到较高左子树的左侧,就要进行右单旋
右单旋的步骤和代码和左单旋相似,可参考完成
左右双旋
如果新增节点插入较高左子树的右侧,就要进行左右单旋:先进行左单旋,再进行右单旋
左右双旋的步骤:
- 先以subL为旋转点进行左单旋
- 再以parent为旋转点进行右单旋
- 更新平衡因子
左右双旋平衡因子的更新分为三种情况
- 当新增的结点在
subLR的右侧
,也就是subLR的平衡因子为1
时
进行左右双旋后,parent
、subL
、subLR
的平衡因子分别更新为0
、-1
、0
- 当新增的节点在
subLR
左侧,也就是subLR的平衡因子为-1
时
进行左右双旋后,parent
、subL
、subLR
的平衡因子分别更新为1
、0
、0
- 当新增的节点就是
subLR
,也就是subLR的平衡因子为0
时
进行左右双旋后,parent
、subL
、subLR
的平衡因子分别更新为0
、0
、0
左右双旋代码如下
void _RotateLR(Node* parent) {
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
_RotateL(subL);
_RotateR(parent);
//更新平衡因子
if (bf == 1) {
//插入的新结点在subLR右边
subLR->_bf = 0;
subL->_bf = -1;
parent->_bf = 0;
}
else if (bf == -1) {
//插入的新结点在subLR的左边
subLR->_bf = 0;
subL->_bf = 0;
parent->_bf = 1;
}
else if (bf == 0) {
//插入的新结点就是subLR
subLR->_bf = 0;
subL->_bf = 0;
parent->_bf = 0;
}
}
右左双旋
右左双旋的步骤:
- 先以subR为旋转点进行右单旋
- 再以parent为旋转点进行左单旋
- 更新平衡因子
右左双旋平衡因子的更新分为三种情况
- 当新增的结点在
subRL的右侧
,也就是subRL的平衡因子为1
时
进行右左双旋后,parent
、subR
、subRL
的平衡因子分别更新为-1
、0
、0
- 当新增的结点在
subRL的左侧
,也就是subRL的平衡因子为-1
时
进行右左双旋后,parent
、subR
、subRL
的平衡因子分别更新为0
、1
、0
- 当新增的结点就是
subRL
,也就是subRL的平衡因子为0
时
进行右左双旋后,parent
、subR
、subRL
的平衡因子分别更新为0
、0
、0
右左双旋代码如下
void _RotateRL(Node* parent) {
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
_RotateR(subR);
_RotateL(parent);
if (bf == 1) {
subRL->_bf = 0;
parent->_bf = -1;
subR->_bf = 0;
}
else if (bf == -1) {
subRL->_bf = 0;
parent->_bf = 0;
subR->_bf = 1;
}
else if (bf == 0) {
subRL->_bf = 0;
parent->_bf = 0;
subR->_bf = 0;
}
else {
assert(false);
}
}
AVL树insert实现代码
pair<Node*, bool> Insert(const pair<K, V>& kv) {
//按照二叉搜索树的方式插入新节点
//_root 为空,直接插入
if (_root == nullptr) {
_root = new Node(kv);
return make_pair(_root, true);
}
//_root不为空,找到插入位置再插入
Node* parent = _root;
Node* cur = _root;
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 {
//key已经存在,插入失败
return make_pair(cur, false);
}
}
//此时cur为空,已经找到插入位置
cur = new Node(kv);
//记录新插入的结点
Node* newNode = cur;
if (parent->_kv.first > cur->_kv.first) {
parent->_left = cur;
cur->_parent = parent;
}
else {
parent->_right = cur;
cur->_parent = parent;
}
//调节节点的平衡因子
while (parent) {
if (parent->_left == cur) {
parent->_bf--;
}
else {
parent->_bf++;
}
//对AVL树的平衡性进行检查
if (parent->_bf == 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) {
//右单旋
_RotateR(parent);
}
else { //cur->_bf == 1
//左右双旋
_RotateLR(parent);
}
}
else { //parent->_bf == 2
if (cur->_bf == 1) {
//左单旋
_RotateL(parent);
}
else { //cur->_bf == -1
//右左双旋
_RotateRL(parent);
}
}
//旋转完退出
break;
}
else {
//出错
assert(false);
}
}
return make_pair(newNode, true);
}
AVL树的查找
AVL树的查找逻辑和二叉搜索树一样
- 若树为空,查找失败,返回
nullptr
- key值小于当前节点,就去当前节点的左子树查找
- key值大于当前节点,就去当前节点的右子树查找
- key值等于当前节点,查找成功,返回对应节点指针
Node* Find(const K& key) {
Node* cur = _root;
while (cur) {
if (cur->_kv.first > key) {
cur = cur->_left;
}
else if (cur->_kv.first < key) {
cur = cur->_right;
}
else {
return cur;
}
}
return nullptr;
}
AVL树value的修改
AVL树的key不能修改,只能通过key修改对应的value值
可以对运算符[]
进行重载
insert的返回值是一个键值对,键值对的first值是成功插入结点的指针,或者已经存在的结点的指针
可以调用insert函数用来获取想要修改的key所对应的value
当使用 对象[key]
时
- 如果key不在树中,先插入键值对
<key, V()>
,然后返回该键值对中value的引用
- 如果key已经存在,返回键值为key结点
value的引用
V& operator[](const K& key) {
pair<Node*, bool> ret = Insert(make_pair(key, V()));
return ret.first->_KV.second;
}
AVL树的验证
可以用中序遍历是否有序的方式来验证AVL树的搜索性质
//中序遍历
void InOrde() {
_InOrder(_root);
}
void _InOrder(Node* root) {
if (root == nullptr) {
return;
}
_InOrder(root->_left);
cout << root->_kv.first << " : " << root->_kv.second << endl;
_InOrder(root->_right);
}
这种方式验证了AVL树的搜索性质,但验证不了它的平衡性
要验证平衡性,需要再进行判断,在验证平衡性时,顺便验证AVL树的平衡因子
bool IsAVLTree() {
return _IsBalance(_root);
}
//计算树的高度
int _HeightT(Node* root) {
if (root == nullptr) {
return 0;
}
int leftHeight = _HeightT(root->_left);
int rightHeight = _HeightT(root->_right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
//验证树的平衡性
bool _IsBalance(Node* root) {
if (root == nullptr) {
return true;
}
int leftHeight = _HeightT(root->_left);
int rightHeight = _HeightT(root->_right);
//判断平衡因子是否符合AVL树的要求
if (rightHeight - leftHeight != root->_bf) {
cout << "平衡因子异常" << root->_kv.first << endl;
return false;
}
return abs(rightHeight - leftHeight) < 2
&& _IsBalance(root->_left)
&& _IsBalance(root->_right);
}
AVL树的删除
因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不过与删除不同的时,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置
具体实现参考
AVL树的性能
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log2N。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。
因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合