目录
前言
前面二叉搜索树一文中末尾提到过AVL树能解决因单支树导致性能下降的问题,是优化二叉搜索树的一种手段,接下来看看呗!
一、What is it?
AVL树也叫二叉平衡搜索树,是在二叉搜索树的基础上引入了平衡因子,具有以下的特点:
- 左右子树的高度差(简称平衡因子)的绝对值不超过1
- 左右子树都是AVL树
形如:
如果一棵二叉搜索树的高度是平衡的,他就是AVL树。若它有n个结点,其高度可保持在logN。
二、How achieve it?
Ⅰ、 结点类定义
这里的直接采用KV模型,每一个元素是pair,同时采用三叉链的结构,因为插入过程中有的情况需要向上调整,找祖先方便!
template<class K, class V>
struct AVLTreeNode
{
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
//存键值对
pair<K, V> _kv;
int _bf; // 平衡因子
AVLTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _bf(0)
{}
};
Ⅱ、实现
插入
这里设平衡因子=右子树-左子树
规则:
1.首先依旧按照二叉搜索树的规则插入。小的放在左子树,大的放在右子树
2.插入新结点后,平衡性可能遭到破坏,此时需要更新祖先结点的平衡因子,更新规则如下:
- 插入到父亲结点左侧,父亲结点的平衡因子-1即可
- 插入到父亲结点右侧,父亲结点的平衡因子+1即可
更新完成后,此时父亲结点的平衡因子可能存在三种情况0,1/-1,2/-2
情况一:为0
这种情况,说明在没更新前父节点的平衡因子为-1/1,插入后变成了0,此时,父亲结点所在的子树高度不变,不需在继续向上更新,插入成功!
情况二:1/-1
这种情况说明没更新前父节点的平衡因子为0,插入后变成了1/-1,此时父亲所在子树的高度增加,需要继续向上更新,直到为0
向上更新的逻辑:
cur=parent; parent=parent->_parent;
情况三:2 /-2
当到这种情况时,高度差大于1,严重违反了AVL树的特性,需要旋转处理。
旋转又分为四种情况的旋转:
- parent平衡因子为-2,cur平衡因子为-1,右单旋
- parent平衡因子为2,cur平衡因子为1,左单旋
- parent平衡因子为-2,cur平衡因子为1,先左旋再右旋,左右双旋
- parent平衡因子为2,cur平衡因子为-1,先右旋再左旋,右左双旋
注意:cur的平衡因子一定不为0,即cur不是新增结点,因为这种情况是一步一步向上更新而来,会先经过情况二。且进行旋转处理后就不需要在向上更新了,平衡了。
上述过程的代码如下:
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
//找位置插入,与二叉搜索树规则一样,用Key比较
while (cur)
{
if (kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
else if(kv.first>cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else
return false;
}
//找到空位置了
cur = new Node(kv);
if (kv.first < parent->_kv.first)
parent->_left = cur;
else
parent->_right = cur;
cur->_parent = parent;//三叉链
//插入成功,需更新调整平衡因子
while (parent)
{
//左边,祖先平衡因子--,反之++
if (cur == parent->_left)
parent->_bf--;
else
parent->_bf++;
//为0,直接结束
if (parent->_bf == 0)
break;
//为1/-1,继续向上调整
else if (parent->_bf == 1 || parent->_bf == -1)
{
cur = parent;
parent = parent->_parent;
}
//为2/-2,分四种情况旋转
else if (parent->_bf == 2 || parent->_bf == -2)
{
//第一种,左子树高,插入左子树的左侧,右单旋
if (parent->_bf == -2 && cur->_bf == -1)
{
RotaRTree(parent);
}
//第二种,右子树高,插入右子树的右侧,左单旋
else if (parent->_bf == 2 && cur->_bf == 1)
{
RotaLTree(parent);
}
//第三种,左子树高,插入左子树的右侧,左右单旋,先左单旋变成单纯左边高,在右单旋
else if (parent->_bf == -2 && cur->_bf == 1)
{
RotaLRTree(parent);
}
//第四种,右子树高,插入右子树的左侧,右左单旋,先右旋变成单纯的右边高,在左旋
else if (parent->_bf == 2 && cur->_bf == -1)
{
RotaRLTree(parent);
}
}
//不可能存在的情况
else
assert(false);
break;
}
return true;
}
旋转 (重难点)
右单旋
右单旋,这种情况原来的左子树比右子树高,新增结点在左子树的最左侧的结点后,即左左。
从上图不难分析出,右单旋的总体逻辑就是:
以parent为旋转点,parent的左子树变成了cur的右子树,cur的右子树变成parent。
注意:
1.cur结点的右子树可能不存在,为空,如h=0时。
2.parent结点可能是根结点,也可能是子树。
- 为根节点,要更新根节点
- 为子树,可能是左也可能是右,所以需要保存parent的parent
大家可能会有疑问,在4后新增不行吗?行,只不过变成了双旋的情况(下文都有提到,别急,现在是右单旋的情况)
注意:抽象图的存在是因为可能性很多很多,即h可以取0,1,2,……,n。上述h=1仅仅只是举例。
代码实现如下:
因为旋转点是parent,所以只传入parent。
void RotaRTree(Node* parent)
{
Node* subL = parent->_left;//父亲的左孩子
Node* subLR = subL->_right;//父亲左的右孩子
Node* pparent = parent->_parent;//当前结点可能是子树,要保存其父亲结点
//父亲左子树变成父亲左孩子的右子树
parent->_left = subLR;
if (subLR)//可能为空
subLR->_parent = parent;
subL->_right = parent;
parent->_parent = subL;
//parent结点为根结点
if (parent == _root)
{
_root = subL;
_root->_parent = nullptr;
}
//为子树
else
{
if (pparent->_left == parent)
pparent->_left = subL;
else
pparent->_right = subL;
subL->_parent = pparent;
}
//更新平衡因子
parent->_bf = subL->_bf = 0;
}
左单旋
与右单旋恰恰相反,原来的右子树比左子树高,新增结点在右子树的最右侧的结点。即右右。
从上图不难分析出,左单旋的总体逻辑与右单旋相反,就是:
以parent为旋转点,parent的右子树变成了cur的左子树,cur的左子树变成parent。
还是注意两个问题:
1.subR(cur)可能为空
2.parent可能为根节点,也可能是子树。
代码实现如下:
void RotaLTree(Node* parent)
{
Node* subR = parent->_right;//右孩子
Node* subRL = subR->_left;//右孩子的左子树
Node* pparent = parent->_parent;
//父亲右子树变成右孩子的左子树
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
subR->_left = parent;
parent->_parent = subR;
if (parent == _root)
{
_root = subR;
_root->_parent = nullptr;
}
else
{
if (pparent->_left == parent)
pparent->_left = subR;
else
pparent->_right = subR;
subR->_parent = pparent;
}
parent->_bf = subR->_bf = 0;
}
左右双旋
先左单旋,在右单旋。这种情况原来的左子树比右子树高,新增结点在左子树的最右侧的结点后。即左右。
从上图可以分析出,左右双旋的整体逻辑是:
当更新至parent的平衡因子为-2,cur平衡因子1时:
1.以subL为旋转点进行左单旋,即以subL作为参数,调用左旋函数即可。
2.以subLR为旋转点进行右单旋,即以subLR作为参数,直接调用右旋函数即可。
3.最后在更新平衡因子。
值得注意的是旋转完毕后的平衡因子更新有点讲究,会随着subLR起始的平衡因子分三种情况:(提醒:这里的起始平衡因子的意思是,插入新增结点后的平衡因子)
- subLR起始平衡因子为0时,左右双旋后,subLR平衡因子为0,subL为0,parent为0。(这种情况为subLR自己是新增结点,即h=0的情况,如下)
- subLR起始平衡因子为-1时,左右双旋后,subLR平衡因子为0,subL为0,parent为1。(这种情况是插入到左子树最右结点的左侧,如上述的抽象图)
- subLR起始平衡因子为1时,左右双旋后,subLR平衡因子为0,subL为-1,parent为0。(这种情况是插入到左子树最右结点的右侧,如下)
代码实现如下:
void RotaLRTree(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;//提前保存subLR的平衡因子,因为左右旋后会变
//先左旋
RotaLTree(parent->_left);//subL为旋转点,如上述的图中的3结点
//再右旋
RotaRTree(parent);//parent为旋转点,如图中的5结点
//更新平衡因子
if (bf == 0)//subLR为新插入的结点
{
subL->_bf = subLR->_bf = parent->_bf = 0;
}
else if (bf == -1)//新插入的结点在subLR的左边
{
subLR->_bf = 0;
subL->_bf = 0;
parent->_bf = 1;
}
else if (bf == 1)//新插入的结点在subLR的右边
{
subLR->_bf = 0;
subL->_bf = -1;
parent->_bf = 0;
}
else //不可能的情况
assert(false);
}
右左双旋
与左右相反。先右单旋,在左单旋。这种情况原来的右子树比左子树高,新增结点在右子树的最左侧的结点后。即右左。
从上图可以分析出,左右双旋的整体逻辑是:
当更新至parent的平衡因子为2,cur平衡因子-1时:
1.以subR为旋转点进行右单旋,即以subR作为参数,调用右旋函数即可。
2.以subRL为旋转点进行左单旋,即以subRL作为参数,调用左旋函数即可。
3.最后在更新平衡因子。
同样平衡因子的更新也很有讲究,会随着subRL起始的平衡因子分三种情况:(提醒:这里的起始平衡因子的意思是,插入新增结点后的平衡因子)
- subRL起始平衡因子为1时,左右双旋后,subRL平衡因子为0,subR为0,parent为-1。(这种情况是插入到右子树最左结点的右侧,如上图所示)
- subRL起始平衡因子为-1时,左右双旋后,subRL平衡因子为0,subR为1,parent为0。(这种情况是插入到右子树最左结点的左侧,如下图所示)
- subRL起始平衡因子为0时,左右双旋后,subRL平衡因子为0,subR为0,parent为0。(这种情况是subRL为新增结点,如下图所示)
代码实现如下:
void RotaRLTree(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;//提前保存subRL的平衡因子,因为左右旋后会变
//先右旋
RotaRTree(parent->_right);//subR为旋转点,如图中的5结点
//先左旋
RotaLTree(parent);//parent为旋转点,如上述的图中的3结点
//更新平衡因子
if (bf == 0)//subLR为新插入的结点
{
subR->_bf = subRL->_bf = parent->_bf = 0;
}
else if (bf == -1)//新插入的结点在subLR的左边
{
subRL->_bf = 0;
subR->_bf = 1;
parent->_bf = 0;
}
else if (bf == 1)//新插入的结点在subLR的右边
{
subRL->_bf = 0;
subR->_bf = 0;
parent->_bf = -1;
}
else //不可能的情况
assert(false);
}
AVL树验证
①验证中序遍历
若中序遍历得到的是一个有序序列,说明为二叉搜索树。
中序遍历和二叉搜索树写法类似
public:
void Inoder()
{
_Inoder(_root);
cout << endl;
}
private:
Node *_root;//私有成员变量
void _Inoder(Node* root)
{
if (root == nullptr)
return;
_Inoder(root->_left);
cout << root->kv.first<< " ";
_Inoder(root->_right);
}
②验证平衡
- 结点子树的高度差不超过1
- 结点的平衡因子是否正确
int _Height(Node* root)
{
if (root == nullptr)
return 0;
return max(_Height(root->_left), _Height(root->_right)) + 1;
}
bool _IsBlance(Node* root)
{
if (root == nullptr)
return true;
//1.结点子树高度差不超过1
int lefthight = _Height(root->_left);
int righthight = _Height(root->_right);
if (abs(lefthight - righthight) >= 2)
{
cout << root->_kv.first << endl;//输出不平衡结点
return false;
}
//2.结点平衡因子是否正确
if (righthight - lefthight != root->_bf)
{
cout << root->_kv.first << endl;
return false;
}
//递归左右子树,检查子树是否为AVL树
return _IsBlance(root->_left) && _IsBlance(root->_right);
}
③样例
void Test()
{
int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
AVLTree<int,int> v;
for (auto e : a)
{
v.Insert({e,e});//每个元素是一个pair
}
v.Inoder();
if (v.IsBlance())
{
cout << "Blance?:平衡" << endl;
}
else
{
cout << "Blance?:不平衡" << endl;
}
}
结果
可以看到,小编的是平衡的,若无法通过该样例,说明出错,但代码量又很大,直接找不方便,这里有个小技巧,每插入一个就检查是否平衡!这样能很快定位错误的点!
for (auto e : a)
{
v.Insert({e,e});
cout << "Insert:" << e << "->" << v.IsBlance() << endl;
}
查找
和二叉搜索树类似。只不过这里的元素是pair键值对。
Node* Find(const K>& key)
{
Node* cur = _root;
while (cur)
{
if (key < cur->_kv.first)
cur = cur->_left;
else if (key > cur->_kv.first)
cur = cur->_right;
else
return cur;
}
return nullptr;
}
大小
public:
int size()
{
return _size(_root);
}
private:
Node* _root=nullptr;
int _size(Node* root)
{
if (root == nullptr)
return 0;
return _size(root->_left) + _size(root->_right) + 1;
}
Ⅲ、完整实现代码
#pragma once
#include<assert.h>
#include<iostream>
using namespace std;
template<class K, class V>
struct AVLTreeNode
{
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
//存键值对
pair<K, V> _kv;
int _bf; // balance factor
AVLTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _bf(0)
{}
};
template<class K, class V>
class AVLTree
{
typedef AVLTreeNode<K,V> Node;
public:
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return 0;
}
Node* parent = nullptr;
Node* cur = _root;
//找位置插入,与二叉搜索树规则一样,用Key比较
while (cur)
{
if (kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
else if(kv.first>cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else
return false;
}
//找到空位置了
cur = new Node(kv);
if (kv.first < parent->_kv.first)
parent->_left = cur;
else
parent->_right = cur;
cur->_parent = parent;//三叉链
//插入成功,需更新调整平衡因子
while (parent)
{
//左边,祖先平衡因子--,反之++
if (cur == parent->_left)
parent->_bf--;
else
parent->_bf++;
//为0,直接结束
if (parent->_bf == 0)
break;
//为1/-1,继续向上调整
else if (parent->_bf == 1 || parent->_bf == -1)
{
cur = parent;
parent = parent->_parent;
}
//为2/-2,分四种情况旋转
else if (parent->_bf == 2 || parent->_bf == -2)
{
//第一种,左子树高,插入左子树的左侧,右单旋
if (parent->_bf == -2 && cur->_bf == -1)
{
RotaRTree(parent);
}
//第二种,右子树高,插入右子树的右侧,左单旋
else if (parent->_bf == 2 && cur->_bf == 1)
{
RotaLTree(parent);
}
//第三种,左子树高,插入左子树的右侧,左右单旋,先左单旋变成单纯左边高,在右单旋
else if (parent->_bf == -2 && cur->_bf == 1)
{
RotaLRTree(parent);
}
//第四种,右子树高,插入右子树的左侧,右左单旋,先右旋变成单纯的右边高,在左旋
else if (parent->_bf == 2 && cur->_bf == -1)
{
RotaRLTree(parent);
}
break;
}
//不可能存在的情况
else
assert(false);
}
return true;
}
bool IsBlance()
{
return _IsBlance(_root);
}
void Inoder()
{
_Inoder(_root);
cout << endl;
}
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (key < cur->_kv.first)
cur = cur->_left;
else if (key > cur->_kv.first)
cur = cur->_right;
else
return cur;
}
return nullptr;
}
int size()
{
return _size(_root);
}
private:
Node* _root=nullptr;
int _size(Node* root)
{
if (root == nullptr)
return 0;
return _size(root->_left) + _size(root->_right) + 1;
}
void RotaRTree(Node* parent)
{
Node* subL = parent->_left;//父亲的左孩子
Node* subLR = subL->_right;//父亲左的右孩子
Node* pparent = parent->_parent;//当前结点可能是子树,要保存其父亲结点
//父亲左子树变成父亲左孩子的右子树
parent->_left = subLR;
if (subLR)//可能为空
subLR->_parent = parent;
subL->_right = parent;
parent->_parent = subL;
//parent结点为根结点
if (parent == _root)
{
_root = subL;
_root->_parent = nullptr;
}
//为子树
else
{
if (pparent->_left == parent)
pparent->_left = subL;
else
pparent->_right = subL;
subL->_parent = pparent;
}
//更新平衡因子
parent->_bf = subL->_bf = 0;
}
void RotaLTree(Node* parent)
{
Node* subR = parent->_right;//右孩子
Node* subRL = subR->_left;//右孩子的左子树
Node* pparent = parent->_parent;
//父亲右子树变成右孩子的左子树
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
subR->_left = parent;
parent->_parent = subR;
if (parent == _root)
{
_root = subR;
_root->_parent = nullptr;
}
else
{
if (pparent->_left == parent)
pparent->_left = subR;
else
pparent->_right = subR;
subR->_parent = pparent;
}
parent->_bf = subR->_bf = 0;
}
void RotaLRTree(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;//提前保存subLR的平衡因子,因为左右旋后会变
//先左旋
RotaLTree(parent->_left);//subL为旋转点,如上述的图中的3结点
//再右旋
RotaRTree(parent);//parent为旋转点,如图中的5结点
//更新平衡因子
if (bf == 0)//subLR为新插入的结点
{
subL->_bf = subLR->_bf = parent->_bf = 0;
}
else if (bf == -1)//新插入的结点在subLR的左边
{
subLR->_bf = 0;
subL->_bf = 0;
parent->_bf = 1;
}
else if (bf == 1)//新插入的结点在subLR的右边
{
subLR->_bf = 0;
subL->_bf = -1;
parent->_bf = 0;
}
else //不可能的情况
assert(false);
}
void RotaRLTree(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;//提前保存subRL的平衡因子,因为左右旋后会变
//先右旋
RotaRTree(parent->_right);//subR为旋转点,如图中的5结点
//先左旋
RotaLTree(parent);//parent为旋转点,如上述的图中的3结点
//更新平衡因子
if (bf == 0)//subLR为新插入的结点
{
subR->_bf = subRL->_bf = parent->_bf = 0;
}
else if (bf == -1)//新插入的结点在subLR的左边
{
subRL->_bf = 0;
subR->_bf = 1;
parent->_bf = 0;
}
else if (bf == 1)//新插入的结点在subLR的右边
{
subRL->_bf = 0;
subR->_bf = 0;
parent->_bf = -1;
}
else //不可能的情况
assert(false);
}
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;
return max(_Height(root->_left), _Height(root->_right)) + 1;
}
bool _IsBlance(Node* root)
{
if (root == nullptr)
return true;
//1.结点子树高度差不超过1
int lefthight = _Height(root->_left);
int righthight = _Height(root->_right);
if (abs(lefthight - righthight) >= 2)
{
cout << root->_kv.first << endl;//输出不平衡结点
return false;
}
//2.结点平衡因子是否正确
if (righthight - lefthight != root->_bf)
{
cout << root->_kv.first << endl;
return false;
}
//递归左右子树,检查子树是否为AVL树
return _IsBlance(root->_left) && _IsBlance(root->_right);
}
};
void Test()
{
int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
AVLTree<int,int> v;
for (auto e : a)
{
v.Insert({e,e});
cout << "Insert:" << e << "->" << v.IsBlance() << endl;
}
v.Inoder();
if (v.IsBlance())
{
cout << "Blance?:平衡" << endl;
}
else
{
cout << "Blance?:不平衡" << endl;
}
cout << v.size() << endl;
}
三、性能
AVL 树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过 1 这样可以保证查询时高效的时间复杂度,即log_2N 。但是如果要对 AVL 树做一些结构修改的操 作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的( 即不会改变), 可以考虑 AVL 树,但一个结构经常修改,就不太适合。
好了,今天的分享就到这里,如果觉得对你有所帮助,欢迎三连 !!!!