✨✨小新课堂开课了,欢迎欢迎~✨✨
🎈🎈养成好习惯,先赞后看哦~🎈🎈
所属专栏:C++:由浅入深篇
小新的主页:编程版小新-CSDN博客
前言:
前面我们已经介绍了二叉搜索树,今天我们要学习的AVL是一颗高度平衡的二叉搜索树,如何让一棵树保持平衡呢?欲知后事,请看下文。
一.AVL树的概念
AVL树是最先发明的平衡二叉搜索树,AVL是一颗空树,或者具备下列性质的二叉搜索树:
1.它的左右子树都是AVL树
2.它的左右子树的高度差的绝对值不超过1
AVL树是一颗高度平衡的二叉搜索树,通过控制高度差去控制平衡。
二.平衡因子的概念
AVL树实现这里我们引入一个平衡因子(balance factor)的概念,每个结点都有一个平衡因子,平衡因子是指一个节点的右子树高度与左子树高度之差,也就是说任何结点的平衡因子等于0/1/-1(左右子树的高度差的绝对值不超过1)
AVL树并不是必须要平衡因子,但是有了平衡因子可以更方便我们去进行观察和控制树是否平衡,就像一个风向标一样。
思考一下为什么AVL树是高度平衡二叉搜索树,要求高度差不超过1,而不是高度差是0呢?0不是更好的平衡吗?画画图分析我们发现,不是不想这样设计,而是有些情况是做不到高度差是0的。比如一棵树是2个结点,4个结点等情况下,无法做到高度差是0。
平衡因子以右子高度减去左子树高度为例。
AVL树整体结点数量和分布和完全二叉树类似,高度可以控制在logN ,那么增删查改的效率也可以控制在logN ,相比二叉搜索树有了本质的提升。
三.AVL树的实现
3.1AVL树的结构
template<class K, class V>
struct AVLTreeNode
{// 需要parent指针,后续更新平衡因子可以看到pair<K, V> _kv;AVLTreeNode<K, V>* _left;//左子树AVLTreeNode<K, V>* _right;//右子树AVLTreeNode<K, V>* _parent;//父亲节点int _bf; // 平衡因子AVLTreeNode(const pair<K, V>& kv):_kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _bf(0){}
};
template<class K, class V>
class AVLTree
{typedef AVLTreeNode<K, V> Node;
public://...
private:Node * _root = nullptr;
};
3.2AVL树的插入
3.2.1 AVL树插入一个值的大概过程
1. 插入一个值按二叉搜索树规则进行插入。
2. 新增结点以后,只会影响祖先结点的高度,也就是可能会影响部分祖先结点的平衡因子。
更新从新增结点->根结点路径上的平衡因子,实际中最坏情况下要更新到根,有些情况更新到中间就可以停止了,具体情况我们下面再详细分析。
3. 更新平衡因子过程中没有出现问题,则插入结束。
4. 更新平衡因子过程中出现不平衡,对不平衡子树进行旋转。
旋转后将子树调平衡的同时降低了子树的高度,不会再影响上一层,所以插入结束。
总结:
3.2.2平衡因子的更新
更新原则:
• 平衡因子 = 右子树高度-左子树高度
• 只有子树高度变化才会影响当前结点平衡因子
• 插入结点,会增加高度,所以新增结点在parent的右子树,parent的平衡因子++,新增结点在parent的左子树,parent平衡因子--
• parent所在子树的高度是否变化决定了是否会继续往上更新
示例一:
示例二:
更新停止条件:
1• 更新后parent的平衡因子等于0,更新中parent的平衡因子变化为-1->0 或者 1->0,说明更新前parent子树一边高一边低,新增的结点插入在低的那边,插入后parent所在的子树高度不变,不会影响parent的父亲结点的平衡因子,更新结束。
2• 更新后parent的平衡因子等于1 或 -1,更新前更新中parent的平衡因子变化为0->1 或者 0->-1,说明更新前parent子树两边一样高,新增的插入结点后,parent所在的子树一边高一边低,parent所在的子树符合平衡要求,但是高度增加了1,会影响parent的父亲结点的平衡因子,所以要继续向上更新。
3• 更新后parent的平衡因子等于2 或 -2,更新前更新中parent的平衡因子变化为1->2 或者 -1->-2,说明更新前parent子树一边高一边低,新增的插入结点在高的那边,parent所在的子树高的那边更高了,破坏了平衡,parent所在的子树不符合平衡要求,需要旋转处理。
旋转的目标有两个:
1、把parent子树旋转平衡。
2、降低parent子树的高度,恢复到插入结点以前的高度。(插入之前是符合AVL树的要求的)
所以旋转后也不需要继续往上更新,插入结束。
3.2.3插入节点及其更新平衡因子
把上面的更新过程理解清楚了,代码的实现就简单了。
//插入
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->_right;}else if (cur->_kv.first > kv.first){//往左走parent = cur;cur = cur->_left;}else{return false;}}//插入cur = new Node(kv);if (parent->_kv.first < kv.first){parent->_right = cur;}else{parent->_left = cur;}//链接父亲cur->_parent = parent;//控制平衡//更新平衡因子while (parent){//新增节点在parent的左子树if (cur == parent->_left){parent->_bf--;//平衡因子--}else{//新增节点在parent的右子树parent->_bf++;//平衡因子++}//检查是否需要向上更新祖先的平衡因子if (parent->_bf == 0){//更新结束break;}else if (parent->_bf == 1 || parent->_bf == -1){//新增的插入结点后,parent所在的子树符合平衡要求//但是高度增加了1,会影响parent的父亲结点的平衡因子,所以要继续向上更新cur = parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2){//新增的插入结点在高的那边,parent所在的子树高的那边更高了// 破坏了平衡,parent所在的子树不符合平衡要求// 需要旋转处理break;//旋转之后,就更新结束了}else{assert(false);}}return true;
}
3.3旋转
3.3.1 旋转的原则
旋转原则:
1. 保持搜索树的规则
2. 让旋转的树从不平衡变平衡,其次降低旋转树的高度
旋转总共分为四种,左单旋/右单旋/左右双旋/右左双旋。
说明:下面的图中,有些结点我们给的是具体值,如10和5等结点,这里是为了方便讲解,实际中是什么值都可以,只要大小关系符合搜索树的规则即可。
3.3.2右单旋
本图1展示的是10为根的树,有a/b/c抽象为三棵高度为h的子树(h>=0),a/b/c均符合AVL树的要求。10可能是整棵树的根,也可能是一个整棵树中局部的子树的根(在实现代码时,这里就是一个需要考虑的小细节)。
这里图一a/b/c是高度为h的子树,是一种概括抽象表示,他代表了所有右单旋的场景,实际右单旋形态有很多种,具体图2/图3/图4/图5进行了详细描述。
其中,parent是失衡节点,其左子树被命名为subL,其左子树的右子树被命名为subLR。
旋转规则如下:
1.将subLR链接到parent的左边。
2.将parent链接到subL的右边
3.将parent与sunL的平衡因子置为0
示例一:
示例二:
示例三:
示例四:
如果去讨论插入前a/b/c是高度为4的AVL子树的话,情况更是多的数不胜数,不过我们不必纠结这个问题,因为我们已经给出了如图一所示的旋转模版,所有的情况都包含在内,我们只要掌握右旋的旋转规则就能解决问题。
实践:右单旋代码实现
void RotateR(Node* parent)
{//旋转Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;//更改_parent的指向if (subLR)//可能高度为0,如情况1{subLR->_parent = parent;}//提前记录parent的父亲节点,因为parent也可能是一整棵树中局部子树的根Node* Pparent = parent->_parent;subL->_right = parent;parent->_parent = subL;//parent之前可能是整棵树的根,也可能是一整棵树中局部的子树的根if (parent == _root){_root = subL;subL->_parent = nullptr;}else{//如果parent之前只是一颗树中局部的子树的根//我们就需要提前记录parent的_parentif (Pparent->_left == parent){Pparent->_left = subL;}else{Pparent->_right = subL;}subL->_parent = Pparent;//更新平衡因子subL->_bf = 0;parent->_bf = 0;}
}
3.3.3左单旋
本图6展示的是10为根的树,有a/b/c抽象为三棵高度为h的子树(h>=0),a/b/c均符合AVL树的要求。10可能是整棵树的根,也可能是一个整棵树中局部的子树的根。这里a/b/c是高度为h的子树,是一种概括抽象表示,他代表了所有左单旋的场景,实际左单旋形态有很多种,具体跟上面右旋类似。
其中,parent是失衡节点,其右子树被命名为subR,其右子树的左子树被命名为subRL。
旋转规则如下:
1.将subRL链接到parent的右边
2.将parent链接到subR的左边
3.将parent与subR的平衡因子置为0
我们不具体的向右旋那样,具体讨论了,下面我们去看几个例子,熟悉一下左旋的过程。
可以发现旋转之后,即保持了搜索树的规则又让旋转的树从不平衡变平衡,其次降低旋转树的高度。
实践:左单旋代码实现
void RotateL(Node* parent){//旋转Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;//更新_parent的指向if (subRL)//可能高度为0{subRL->_parent = parent;}// //提前记录parent的父亲节点,因为parent之前也可能是一整棵树中局部的子树的根Node* Pparent = parent->_parent;subR->_left = parent;parent->_parent = subR;//parent之前可能是整棵树的根,也可能是一整棵树中局部的子树的根if (parent == _root){_root = subR;subR->_parent = nullptr;}else{//如果parent之前只是一颗树中局部的子树的根//我们就需要提前记录parent的_parentif (Pparent->_left == parent){Pparent->_left = subR;}else{Pparent->_right = subR;}subR->_parent = Pparent;//更新平衡因子subR->_bf = 0;parent->_bf = 0;}}
3.3.4左右双旋
通过图7和图8可以看到,左边高时,如果插入位置不是在a子树,而是插入在b子树,b子树高度从h变成h+1,引发旋转,右单旋无法解决问题,右单旋后,我们的树依旧不平衡。右单旋解决的纯粹的左边高,左单旋解决的纯粹的右边高,但是插入在b子树中,10为跟的子树不再是单纯的左边高,对于10是左边高,但是对于5是右边高,需要用两次旋转才能解决,以5为旋转点进行一个左单旋,以10为旋转点进行⼀个右单旋,这棵树这棵树就平衡了。
由图可以更加直观的看到,像这种不是纯粹的一边高的情况,只靠左旋/右旋是不能解决问题的。不仅树不平衡,我们在更新平衡因子的时候,也有错误。
那我们该如何是好呢?
对于这种情况我们需要先左单旋,再右单旋的方式,其中,失衡节点为parent,其左子树节点为subL,而有左子树的右子树为subLR。
其调整规则如下:
1.先对subL进行左旋
2.再对parent 进行右旋
3.调整平衡因子
下面我们具体看个示例。
但是新增节点的位置不同,平衡因子的更新也会有差异,这里我们分为三种情况。
1.当subLR = -1时,如上图所示,调整后subL = 0;subLR = 0;parent = 1。
2.当subLR= 0时,如下图所示,调整后subL = 0;subLR = 0;parent = 0。
3.当subLR = 1时,如下图所示,调整后subL = -1;subLR = 0;parent = 0。
实践:左右双旋代码实现
void RotateLR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;RotateL(parent->_left);RotateR(parent);if (bf == 0){subL->_bf = 0;subLR->_bf = 0;parent->_bf = 0;}else if (bf == -1){subL->_bf = 0;subLR->_bf = 0;parent->_bf = 1;}else if (bf == 1){subL->_bf = -1;subLR->_bf = 0;parent->_bf = 0;}else{assert(false);}
}
3.3.5右左双旋
和左右双旋的情况类似,当出现不是纯粹的某一边高时,仅仅使用一次单旋是解决不了问题的。例如对12而言是左边高,对8而言是右边高。我们就要先对12进行右旋,再对8进行左旋才能解决问题。
对于这种情况我们需要先右单旋,再左单旋的方式,设失衡节点为parent,其右子树节点为subR,而有右子树的左子树为subRL。
其调整规则如下:
先对subR进行右旋
再对parent进行左旋
调整平衡因子
我们针对上面的例子,具体实践看看。
但是新增节点的位置不同,平衡因子的更新也会有差异,这里我们分为三种情况。
1.当subRL = -1时,如上图所示,调整后subR = 1;subRL = 0;parent = 0。
2.当subRL = 0时,如下图所示,调整后subR = 0;subRL = 0;parent = 0。
3.当subRL = 1时,如下图所示,调整后subR = 0;subRL = 0;parent = -1。
实践:右左双旋代码实现
void RotateRL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;RotateR(parent->_right);RotateL(parent);if (bf == 0){subR->_bf = 0;subRL->_bf = 0;parent->_bf = 0;}else if (bf == -1){subR->_bf = 1;subRL->_bf = 0;parent->_bf = 0;}else if (bf == 1){subR->_bf = 0;subRL->_bf = 0;parent->_bf = -1;}else{assert(false);}
}
3.3.6插入完整代码
//插入
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->_right;}else if (cur->_kv.first > kv.first){//往左走parent = cur;cur = cur->_left;}else{return false;}}//插入cur = new Node(kv);if (parent->_kv.first < kv.first){parent->_right = cur;}else{parent->_left = cur;}//链接父亲cur->_parent = parent;//控制平衡//更新平衡因子while (parent){//新增节点在parent的左子树if (cur == parent->_left){parent->_bf--;//平衡因子--}else{//新增节点在parent的右子树parent->_bf++;//平衡因子++}//检查是否需要向上更新祖先的平衡因子if (parent->_bf == 0){//更新结束break;}else if (parent->_bf == 1 || parent->_bf == -1){//新增的插入结点后,parent所在的子树符合平衡要求//但是高度增加了1,会影响parent的父亲结点的平衡因子,所以要继续向上更新cur = parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2){//新增的插入结点在高的那边,parent所在的子树高的那边更高了// 破坏了平衡,parent所在的子树不符合平衡要求// 需要旋转处理if (parent->_bf == -2 && cur->_bf ==-1){RotateR(parent);}else if (parent->_bf == 2 && cur->_bf == 1){RotateL(parent);}else if (parent->_bf == -2 && cur->_bf == 1){RotateLR(parent);}else if (parent->_bf == 2 && cur->_bf == -1){RotateRL(parent);}else{assert(false);}break;//旋转之后,就更新结束了}else{assert(false);}}return true;
}
3.4AVL树的查找
和二叉搜索树的查找逻辑一样,搜索效率为 O(logN)。
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;
}
3.5AVL树平衡检测
我们实现的AVL树是否合格,我们通过检查左右子树高度差的的程序进行反向验证,同时检查一下结点的平衡因子更新是否出现了问题。
int _Hight(Node* root){if (root == nullptr){return 0;}int LeftHight = _Hight(root->_left);int RightHight = _Hight(root->_right);return LeftHight > RightHight ? LeftHight + 1 : RightHight+1;}bool _IsBalanceTree(Node* root){//空树if (root == nullptr){return true;}int LeftHight = _Hight(root->_left);int RightHight = _Hight(root->_right);int diff = RightHight - LeftHight;if (abs(diff) >= 2){cout << root->_kv.first << "高度差异常" << endl;return false;}if (root->_bf != diff){cout << root->_kv.first << "平衡因子异常" << endl;return false;}// root的左和右如果都是AVL树,则该树一定是AVL树return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);}
四.源码
#pragma once
#include<iostream>
#include<assert.h>
#include<utility>using namespace std;template<class K, class V>
struct AVLTreeNode
{// 需要parent指针,后续更新平衡因子可以看到pair<K, V> _kv;AVLTreeNode<K, V>* _left;//左子树AVLTreeNode<K, V>* _right;//右子树AVLTreeNode<K, V>* _parent;//父亲节点int _bf; // 平衡因子AVLTreeNode(const pair<K, V>& kv):_kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _bf(0){}
};template<class K, class V>
class AVLTree
{typedef AVLTreeNode<K, V> Node;public:AVLTree(){}AVLTree(const AVLTree& t){_root = copy(t._root);}Node* copy(Node* root){if (root == nullptr)return nullptr;Node* newnode = new Node(root->_kv);// 递归地拷贝原始根节点的左子树,并将结果赋给新节点的左指针newnode->_left = copy(root->_left);// 递归地拷贝原始根节点的右子树,并将结果赋给新节点的右指针newnode->_right = copy(root->_right);// 新节点的父节点默认为空newnode->_parent = nullptr;// 如果新节点的左子节点存在,设置其父节点为新节点if (newnode->_left){newnode->_left->_parent = newnode;}// 如果新节点的右子节点存在,设置其父节点为新节点if (newnode->_right){newnode->_right->_parent = newnode;}// 返回新树的根节点指针return newnode;}AVLTree<K, V>& operator=(const AVLTree <K, V> t){this->swap(_root, t._root);return *this;}~AVLTree(){Destroy(_root);}void Destroy(Node*& root){if (root == nullptr){return;}Destroy(root->_left);Destroy(root->_right);delete root;root = nullptr;}//插入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->_right;}else if (cur->_kv.first > kv.first){//往左走parent = cur;cur = cur->_left;}else{return false;}}//插入cur = new Node(kv);if (parent->_kv.first < kv.first){parent->_right = cur;}else{parent->_left = cur;}//链接父亲cur->_parent = parent;//控制平衡//更新平衡因子while (parent){//新增节点在parent的左子树if (cur == parent->_left){parent->_bf--;//平衡因子--}else{//新增节点在parent的右子树parent->_bf++;//平衡因子++}//检查是否需要向上更新祖先的平衡因子if (parent->_bf == 0){//更新结束break;}else if (parent->_bf == 1 || parent->_bf == -1){//新增的插入结点后,parent所在的子树符合平衡要求//但是高度增加了1,会影响parent的父亲结点的平衡因子,所以要继续向上更新cur = parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2){//新增的插入结点在高的那边,parent所在的子树高的那边更高了// 破坏了平衡,parent所在的子树不符合平衡要求// 需要旋转处理if (parent->_bf == -2 && cur->_bf ==-1){RotateR(parent);}else if (parent->_bf == 2 && cur->_bf == 1){RotateL(parent);}else if (parent->_bf == -2 && cur->_bf == 1){RotateLR(parent);}else if (parent->_bf == 2 && cur->_bf == -1){RotateRL(parent);}else{assert(false);}break;//旋转之后,就更新结束了}else{assert(false);}}return true;}void RotateR(Node* parent){//旋转Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;//更改_parent的指向if (subLR)//可能高度为0,如情况1{subLR->_parent = parent;}//提前记录parent的父亲节点,因为parent也可能是一整棵树中局部子树的根Node* Pparent = parent->_parent;subL->_right = parent;parent->_parent = subL;//parent之前可能是整棵树的根,也可能是一整棵树中局部的子树的根if (parent == _root){_root = subL;subL->_parent = nullptr;}else{//如果parent之前只是一颗树中局部的子树的根//我们就需要提前记录parent的_parentif (Pparent->_left == parent){Pparent->_left = subL;}else{Pparent->_right = subL;}subL->_parent = Pparent;//更新平衡因子subL->_bf = 0;parent->_bf = 0;}}void RotateL(Node* parent){//旋转Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;//更新_parent的指向if (subRL)//可能高度为0{subRL->_parent = parent;}// //提前记录parent的父亲节点,因为parent之前也可能是一整棵树中局部的子树的根Node* Pparent = parent->_parent;subR->_left = parent;parent->_parent = subR;//parent之前可能是整棵树的根,也可能是一整棵树中局部的子树的根if (parent == _root){_root = subR;subR->_parent = nullptr;}else{//如果parent之前只是一颗树中局部的子树的根//我们就需要提前记录parent的_parentif (Pparent->_left == parent){Pparent->_left = subR;}else{Pparent->_right = subR;}subR->_parent = Pparent;//更新平衡因子subR->_bf = 0;parent->_bf = 0;}}void RotateLR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;RotateL(parent->_left);RotateR(parent);if (bf == 0){subL->_bf = 0;subLR->_bf = 0;parent->_bf = 0;}else if (bf == -1){subL->_bf = 0;subLR->_bf = 0;parent->_bf = 1;}else if (bf == 1){subL->_bf = -1;subLR->_bf = 0;parent->_bf = 0;}else{assert(false);}}void RotateRL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;RotateR(parent->_right);RotateL(parent);if (bf == 0){subR->_bf = 0;subRL->_bf = 0;parent->_bf = 0;}else if (bf == -1){subR->_bf = 1;subRL->_bf = 0;parent->_bf = 0;}else if (bf == 1){subR->_bf = 0;subRL->_bf = 0;parent->_bf = -1;}else{assert(false);}}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;}int Height(){return _Height(this->_root);}int Size(){return _Size(this->_root);}/* bool isBalanceTree(){return IsBalanceTree(this->_ root);}*/void inOrder(){InOrder(this->_root);cout << endl;}private:void InOrder(Node* root){if (root == nullptr){return;}InOrder(root->_left);cout << root->_kv.first << ":" << root->_kv.second << endl;InOrder(root->_right);}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 IsBalanceTree(Node* root){//空树if (root == nullptr){return true;}int LeftHeight = _Height(root->_left);int RightHeight = _Height(root->_right);int diff = RightHeight - LeftHeight;if (abs(diff) >= 2){cout << root->_kv.first << "高度差异常" << endl;return false;}if (root->_bf != diff){cout << root->_kv.first << "平衡因子异常" << endl;return false;}// root的左和右如果都是AVL树,则该树一定是AVL树return IsBalanceTree(root->_left) && IsBalanceTree(root->_right);}int _Size(Node* root){if (root == nullptr)return 0;return _Size(root->_left) + _Size(root->_right) + 1;}private:Node* _root = nullptr;
};
总结:
AVL的旋转难题,我们已经解决了,下面我们就要进入红黑树了,红黑树也是一个平衡二叉搜索树,就让我们一起去看看是怎么回事吧。
感谢各位大佬的观看,创作不易,还请各位大佬点赞支持~