目录
1.AVL树的概念
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
- 它的左右子树都是
AVL
树- 左右子树高度之差(简称平衡因子)的绝对值不超过
1(-1/0/1)
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在
O
(
l
o
g
2
n
)
O(log_2 n)
O(log2n) ,搜索时间复杂度O(
l
o
g
2
n
log_2 n
log2n)
2.AVL树模拟实现
2.1AVL树节点的定义
struct AVLTreeNode
{
pair<K, V> _kv;
AVLTreeNode<K, V>* _left; //该节点的左孩子
AVLTreeNode<K, V>* _right; //该节点的右孩子
AVLTreeNode<K, V>* _parent; //该节点的双亲
int _bf;//该节点的平衡因子
AVLTreeNode(const pair<K,V>& kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
,_bf(0)
{}
};
2.2AVL的插入
假设下面这棵树是我们最开始AVL树,接下来我们需要对其进行节点的插入,并针对不同的情况有不同的处理情况
1.新增节点在parent节点的左边,parent的平衡因子减一(减减)
2.新增节点在parent节点的右边,parent的平衡因子加一(加加)
3.更新parent平衡因子==0,说明parent所在的子树的高度不变,不会再影响祖先,不用继续沿着到root的路径继续往上更新,插入结束
4.更新后
parent平衡因子==1 or -1
,说明parent
所在的子树的高度变化,会再影响祖先,需要继续沿着到root
的路径继续往上更新
5.更新后parent平衡因子==2 or -2
,说明parent
所在的子树的高度变化且不平衡,对parent
所在子树进行旋转,让子树平衡,插入结束(涉及旋转下面将分情况进行讲解)
6.更新到根节点,没有出现parent平衡因子==2 or -2
的情况更新结束
2.3AVL树的旋转
旋转的时候涉及的问题:
旋转之后的树需要满足两个条件:1.保持这棵树是二叉搜索树;2.变成平衡树(AVL树),且降低这个子树的高度
2.3.1左单旋
当更新parent
(30)节点的平衡因子为2
,cur
(60)节点的平衡因子为1
,造成parent
(30)所在子树右边高,需要进行左单旋
以下是h等于0和h等于1的具象图:
根据上面两种右边子树比较高的情况,需要将cur
的左子树链接成为parent
的右子树,让parent
链接成为cur
的左子树。情况一:插入之前,AVL树高度为1;插入节点后,树高度为2,旋转之后,树高度为1且达到平衡,不用继续沿着到root
的路径继续往上更新,插入结束;情况二:插入之前,AVL树高度为2;插入节点后,树高度为3;旋转之后,树高度为2且达到平衡,不用继续沿着到root
的路径继续往上更新,插入结束。
代码如下:
void RotateL(Node* parent)
{
Node* cur = parent->_right;
Node* curleft = cur->_left;
//curleft成为parent的右边
parent->_right = curleft;
//curleft可能为空
if (curleft)
{
//改变curleft父指针的指向
curleft->_parent = parent;
}
//parent成为cur的左边
cur->_left = parent;
//记录parent的父节点指针
Node* ppnode = parent->_parent;
//改变parent父指针的指向
parent->_parent = cur;
//判断parent节点是否为根节点
if (parent == _root)
{
//cur成为根节点
_root = cur;
cur->_parent = nullptr;
}
else
{
//cur链接parent的父节点
if (ppnode->_left == parent)
{
ppnode->_left = cur;
}
else if (ppnode->_right == parent)
{
ppnode->_right = cur;
}
cur->_parent = ppnode;
}
parent->_bf = cur->_bf = 0;
}
2.3.2右单旋
当更新后,parent
(30)节点的平衡因子为-2
,cur
(60)节点的平衡因子为-1
,造成parent
(30)所在子树左边高,需要进行右单旋
以下是h等于0和h等于1的具象图:
情况一(h等于0): 插入之前,AVL树高度为1;插入节点后,树高度为2,旋转之后,树高度为1且达到平衡,不用继续沿着到root
的路径继续往上更新,插入结束;情况二(h等于1): 插入之前,AVL树高度为2;插入节点后,树高度为3;旋转之后,树高度为2且达到平衡,不用继续沿着到root
的路径继续往上更新,插入结束。
分析: 根据上面两种左边子树比较高的情况,需要将cur
的右子树链接成为parent
的左子树,让parent
链接成为cur
的左子树。局部根节点parent
已经出现向左边倾斜的情况,当插入节点时导致parent
所在子树左边高且不平衡,经过旋转后,cur
成为新的局部根节点,cur
所在子树的高度与未插入节点前的高度一致,不会对上层节点造成影响,达到平衡状态(即cur
的平衡因子为0)
代码如下:
void RotateR(Node* parent)
{
Node* cur = parent->_left;
Node* curright = cur->_right;
//curright链接成为parent的左子树
parent->_left = curright;
if (curright)
{
//curright不为空情况进行,改变其父指针指向
curright->_parent = parent;
}
//parent链接成为cur的右子树
cur->_right = parent;
//记录parent父指针
Node* ppnode = parent->_parent;
//让parent父指针指向cur
parent->_parent = cur;
//cur与原parent指向父节点链接
if (parent == _root)
{
//parent为根节点的情况
_root = cur;
cur->_parent = nullptr;
}
else
{
//parent为局部子树根节点
//判断parent原先为局部左子树还是局部右子树
if (ppnode->_left == parent)
{
ppnode->_left = cur;
}
else
{
ppnode->_right = cur;
}
cur->_parent = ppnode;
}
//更新parent\cur的平衡因子为0
parent->_bf = cur->_bf = 0;
}
2.3.3右左双旋
2.3.3.1旋转情况分析
当插入节点更新平衡因子后,parent
(30)节点的平衡因子为2
,cur
(60)节点的平衡因子为-1
,造成parent
(30)所在子树右边高,但右子树cur节点所在子树却左边高,这时候已经parent所在的子树并不是纯粹的右边高了,所以只进行左单旋不能解决问题,需要进行右左双旋。
以下是h等于0和h等于1的具象图:
仅仅进行左单旋操作:
从上面的图可以看出,若
parent
所在子树不是完全右边高的情况,对其进行左单旋操作不能使AVL树达到平衡
进行右左双旋操作:
parent
(30)所在子树右边高,但右子树cur
节点所在子树却左边高,像一个折线;需要先对以cur
节点为根节点的子树进行右旋操作,然后对parent
节点为根节点的子树进行左旋操作,经过双旋操作后,curleft
代替parent
节点成为新的(局部)根节点,解决了不平衡问题,使左右子树高度一致,达到平衡状态。
2.3.3.2平衡因子更新分析
问题: 双旋调整后,parent\cur\curleft
三个节点的位置发生了变化,这三个节点的平衡因子是否都无脑改为0呢?答案:不是的!那么该这三个节点的平衡因子应该怎么修改呢?友友们不要着急,且和我一起慢慢分析吧!
h==0(curleft->_bf ==0
)的情况:
分析: ①当
curleft
的平衡因子为0
时,curleft
没有节点给cur\parent
节点,且cur\parent
节点原本也无左右子树,所以cur\parent
节点的平衡因子都更新为0
;
h==1(curleft->_bf ==-1
)的情况:
分析: ②当
curleft
的平衡因子为-1
,curleft
没有右节点可以链接成为cur
节点的左子树,以cur
节点为根节点右旋之后,造成cur
节点所在子树右边偏高,所以cur
节点的平衡因子更新为1
;curleft
有左节点可以链接成为parent
的右子树,以parent
节点为根节点左旋之后,使parent
节点左右子树高度一致达到平衡状态,所以parent
的平衡因子也更新为0
;
h==1(curleft->_bf ==-1
)的情况:
分析: ③当
curleft
的平衡因子为1
,curleft
有右节点可以链接成为cur
节点的左子树,以cur
节点为根节点右旋之后,使cur
节点左右子树高度一致达到平衡状态,所以cur
节点的平衡因子更新为0
;curleft
有左节点链接成为parent
的右子树,以parent
节点为根节点左旋之后,造成parent
节点所在子树左边偏高,所以parent
节点的平衡因子更新为-1
;
代码如下:
void RotateRL(Node* parent)
{
Node* cur = parent->_right;
Node* curleft = cur->_left;
int bf = curleft->_bf;//旋转过程中,curleft的bf发生变化,需要提前记录
//先以cur(parent->_right)为根节点右旋
//再以parent为根节点左旋
RotateR(cur);
RotateL(parent);
//针对不同curleft->_bf的情况进行处理
if (bf == 0)
{
curleft->_bf = 0;
cur->_bf = 0;
parent->_bf = 0;
}
else if (bf == 1)
{
curleft->_bf = 0;
cur->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1)
{
curleft->_bf = 0;
cur->_bf = 1;
parent->_bf = 0;
}
else
{
assert(false);
}
}
右左双旋平衡因子更新总结分析:
分析上面三种平衡因子更新的情况,可以看出右左双旋(先右旋,再左旋)结果的本质是:curleft
的右边给了cur
的左边,curleft
的左边给了parent
的左边,cur
节点和parent
分别成为curleft
的右左子树。
2.3.4右左双旋
2.3.4.1旋转情况分析
当插入节点更新平衡因子后,parent
(90)节点的平衡因子为-2
,cur
(30)节点的平衡因子为1
,造成parent
(90)所在子树左边高,但其左子树cur节点所在子树却左边高,这时候已经parent
所在的子树并不是纯粹的左边高了,所以只进行右单旋不能解决问题,需要进行右左双旋。
以下是h等于0和h等于1的具象图:
仅仅进行右单旋操作:
从上面的图可以看出,若
parent
所在子树不是完全左边高的情况,对其进行右单旋操作不能使AVL树达到平衡
进行左右双旋操作:
parent
(90)所在子树左边高,但右子树cur
节点所在子树却右边高,像一个折线;需要先对以cur
节点为根节点的子树进行左旋操作,然后对parent
节点为根节点的子树进行右旋操作,经过双旋操作后,curleft
代替parent
节点成为新的(局部)根节点,解决了不平衡问题,使左右子树高度一致,达到平衡状态。
2.3.4.2平衡因子更新分析
h==0(curright->_bf ==0
)的情况:
分析: ①当
curright
的平衡因子为0
时,curright
没有节点给cur\parent
节点,且cur\parent
节点原本也无左右子树,所以cur\parent
节点的平衡因子都更新为0
;
h==-1(curright->_bf ==-1
)的情况:
分析: ②当
curright
的平衡因子为-1
,curright
有左节点链接cur
节点的右子树,以cur节点为根节点左旋之后,使cur节点左右子树高度一致达到平衡状态,所以cur
的平衡因子也更新为0
;curright
没有右节点链接成为parent
的左子树,以parent
节点为根节点右旋之后,造成parent
节点所在子树右边偏高,所以parent
节点的平衡因子更新为1
;
h==1(curright->_bf ==1
)的情况:
分析: ③当
curright
的平衡因子为1
,curright
没有左节点可以链接cur
节点的右子树,以cur节点为根节点左旋之后,造成parent
节点所在子树左边偏高,所以cur
的平衡因子也更新为-1
;curright
有右节点可以链接成为parent
的左子树,以parent
节点为根节点右旋之后,使parent
节点左右子树高度一致达到平衡状态,所以parent
节点的平衡因子更新为0
;
代码如下:
void RotateLR(Node* parent)
{
Node* cur = parent->_left;
Node* curright = cur->_right;
int bf = curright->_bf;
//先以cur(parent->_left)为根节点左旋
//再以parent为根节点右旋
RotateL(parent->_left);
RotateR(parent);
//针对不同curright->_bf的情况进行处理
if (bf == 0)
{
curright->_bf = 0;
cur->_bf = 0;
parent->_bf = 0;
}
else if (bf == -1)
{
curright->_bf = 0;
cur->_bf = 0;
parent->_bf = 1;
}
else if (bf == 1)
{
curright->_bf = 0;
cur->_bf = -1;
parent->_bf = 0;
}
}
左右双旋平衡因子更新总结分析:
分析上面三种平衡因子更新的情况,可以看出左右双旋(先左旋,再右旋)结果的本质是:curright
的左边给了cur
的右边,curleft
的右边给了parent
的左边,cur
节点和parent
分别成为curleft
的左右子树。
注意: ① 双旋是先左(右)旋,然后再右(左)旋,单旋转的原理是一样的,所以可以直接对前面左(右)旋函数调用,然后对右(左)旋函数调用,可增加代码复用性;②双旋平衡因子单独分析进行更新,与单旋转函数可以起到解耦合的作用。
2.3.5AVL树的验证
我们已经实现了AVL树插入操作的功能,为了保证所写的代码正确,我们可以插入数据进行检验。
使用测试AVL树平衡的函数+进行数据的插入检验
int Height()
{
return Height(_root);
}
int Height(Node* root)
{
if (root == nullptr)
{
return 0;
}
int leftHeight = Height(root->_left);
int rightHeight = Height(root->_right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
bool IsBalance()
{
return IsBalance(_root);
}
bool IsBalance(Node* root)
{
if (root == nullptr)
{
return true;
}
int leftHeight=Height(root->_left);
int rightHeight = Height(root->_right);
if (rightHeight - leftHeight != root->_bf)
{
cout << "Exception:" << root->_kv.first << "->" << root->_bf << endl;
assert(false);
return false;
}
return abs(leftHeight - rightHeight) < 2
&& IsBalance(root->_left)
&& IsBalance(root->_right);
}
测试1:
void test()
{
//普通场景
int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
AVLTree<int, int> t;
for (auto e : a)
{
t.insert(make_pair(e, e));
cout << "Insert:" << e << "->" << t.IsBalance() << endl;
}
cout << "所有数据插入成功" << endl;
}
代码运行结果为:
测试2:
void test2()
{
//插入随机数
const int N = 1000000;
vector<int> v;
v.reserve(N);
srand(time(0));
for (size_t i = 0; i < N; i++)
{
v.push_back(rand()+i);
}
AVLTree<int, int> t;
for (auto e : v)
{
t.insert(make_pair(e, e));
}
cout << "所有数据插入成功" << endl;
}
代码运行结果如下:
测试3:
void test2()
{
//插入随机数
const int N = 1000000;
vector<int> v;
v.reserve(N);
srand(time(0));
for (size_t i = 0; i < N; i++)
{
v.push_back(rand()+i);
}
AVLTree<int, int> t;
for (auto e : v)
{
t.insert(make_pair(e, e));
}
cout << "Insert:" << t.IsBalance() << endl;
cout << "所有数据插入成功" << endl;
}
代码运行结果如下:
3.AVL模拟实现源码
#include<iostream>
#include<assert.h>
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;//该节点的平衡因子
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 true;
}
//找到合适的位置插入连接
Node* parent = nullptr;
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
{
//有相同节点的值则插入失败
return false;
}
}
cur = new Node(kv);
//关系链接
if (parent->_kv.first > kv.first)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;
//更新平衡因子
while (parent)
{
if (parent->_left == cur)
{
parent->_bf--;
}
else
{
parent->_bf++;
}
//parent所在子树高度不变,会影响祖先,不需要更新平衡因子,插入结束
if (parent->_bf == 0)
{
break;
}//parent所在子树高度变化,会影响祖先,继续沿着到root的路径更新平衡因子
else if (parent->_bf == 1 || parent->_bf == -1)
{
cur = parent;
parent = parent->_parent;
}//parent所在子树高度变化且不平衡,需要旋转parent所在子树
else if (parent->_bf == 2 || parent->_bf == -2)
{
//插入在parent右子树的右侧,造成parent右边高
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)
{
RotateRL(parent);//右左双旋
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
RotateLR(parent);//左右双旋
}
break;
}
else
{
assert(false);
}
}
}
void RotateL(Node* parent)
{
Node* cur = parent->_right;
Node* curleft = cur->_left;
//curleft成为parent的右边
parent->_right = curleft;
//curleft可能为空
if (curleft)
{
//改变curleft父指针的指向
curleft->_parent = parent;
}
//parent成为cur的左边
cur->_left = parent;
//记录parent的父节点指针
Node* ppnode = parent->_parent;
//改变parent父指针的指向
parent->_parent = cur;
//判断parent节点是否为根节点
if (parent == _root)
{
//cur成为根节点
_root = cur;
cur->_parent = nullptr;
}
else
{
//cur链接parent的父节点
if (ppnode->_left == parent)
{
ppnode->_left = cur;
}
else if (ppnode->_right == parent)
{
ppnode->_right = cur;
}
cur->_parent = ppnode;
}
parent->_bf = cur->_bf = 0;
}
void RotateR(Node* parent)
{
Node* cur = parent->_left;
Node* curright = cur->_right;
//curright链接成为parent的左子树
parent->_left = curright;
if (curright)
{
//curright不为空情况进行,改变其父指针指向
curright->_parent = parent;
}
//parent链接成为cur的右子树
cur->_right = parent;
//记录parent父指针
Node* ppnode = parent->_parent;
//让parent父指针指向cur
parent->_parent = cur;
//cur与原parent指向父节点链接
if (parent == _root)
{
//parent为根节点的情况
_root = cur;
cur->_parent = nullptr;
}
else
{
//parent为局部子树根节点
//判断parent原先为局部左子树还是局部右子树
if (ppnode->_left == parent)
{
ppnode->_left = cur;
}
else
{
ppnode->_right = cur;
}
cur->_parent = ppnode;
}
//更新parent\cur的平衡因子为0
parent->_bf = cur->_bf = 0;
}
void RotateRL(Node* parent)
{
Node* cur = parent->_right;
Node* curleft = cur->_left;
int bf = curleft->_bf;//旋转过程中,curleft的bf发生变化,需要提前记录
//先以cur(parent->_right)为根节点右旋
//再以parent为根节点左旋
RotateR(cur);
RotateL(parent);
if (bf == 0)
{
curleft->_bf = 0;
cur->_bf = 0;
parent->_bf = 0;
}
else if (bf == 1)
{
curleft->_bf = 0;
cur->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1)
{
curleft->_bf = 0;
cur->_bf = 1;
parent->_bf = 0;
}
else
{
assert(false);
}
}
void RotateLR(Node* parent)
{
Node* cur = parent->_left;
Node* curright = cur->_right;
int bf = curright->_bf;
//先以cur(parent->_left)为根节点左旋
//再以parent为根节点右旋
RotateL(parent->_left);
RotateR(parent);
if (bf == 0)
{
curright->_bf = 0;
cur->_bf = 0;
parent->_bf = 0;
}
else if (bf == -1)
{
curright->_bf = 0;
cur->_bf = 0;
parent->_bf = 1;
}
else if (bf == 1)
{
curright->_bf = 0;
cur->_bf = -1;
parent->_bf = 0;
}
}
int Height()
{
return Height(_root);
}
int Height(Node* root)
{
if (root == nullptr)
{
return 0;
}
int leftHeight = Height(root->_left);
int rightHeight = Height(root->_right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
bool IsBalance()
{
return IsBalance(_root);
}
bool IsBalance(Node* root)
{
if (root == nullptr)
{
return true;
}
int leftHeight=Height(root->_left);
int rightHeight = Height(root->_right);
if (rightHeight - leftHeight != root->_bf)
{
cout << "Exception:" << root->_kv.first << "->" << root->_bf << endl;
assert(false);
return false;
}
return abs(leftHeight - rightHeight) < 2
&& IsBalance(root->_left)
&& IsBalance(root->_right);
}
private:
Node* _root = nullptr;
};
4.总结
①AVL树的删除(了解)
因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不过与删除不同的时,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。
② AVL树的性能
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即 l o g 2 ( N ) log_2 (N) log2(N)。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。