目录
- 1.AVL树的概念
- 2.AVL树模拟实现
- 2.1AVL树节点的定义
- 2.2AVL的插入
- 2.3AVL树的旋转
- 2.3.1左单旋
- 2.3.2右单旋
- 2.3.3右左双旋
- 2.3.3.1旋转情况分析
- 2.3.3.2平衡因子更新分析
- 2.3.4右左双旋
- 2.3.4.1旋转情况分析
- 2.3.4.2平衡因子更新分析
- 2.3.5AVL树的验证
- 3.AVL模拟实现源码
- 4.总结
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父指针指向curparent->_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的平衡因子为0parent->_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父指针指向curparent->_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的平衡因子为0parent->_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树,但一个结构经常修改,就不太适合。