当前位置: 首页 > news >正文

C++进阶-AVL树(平衡二叉查找树)(难度较高)

目录

1.预备知识

2.AVL的概念

3.AVL树基本结构的实现

4.AVL树的插入(最主要)

4.1大概过程

4.2平衡因子的更新

4.2.1更新原则

4.2.2更新停止条件

4.3初步代码

5.AVL树的旋转(最难)

5.1旋转的原则

5.2右单旋

5.3左单旋

5.5左右双旋

5.6右左双旋

5.7AVLTree实现Insert的全部代码

6.AVL树其他功能的实现

6.1查找的实现

6.2中序遍历的实现

6.3计算AVL树的高度

6.4判断是否为AVL树(或判断是否平衡)

6.5计算AVL树的大小

7.最终代码

8.AVL树的删除

9.总结


1.预备知识

要学习AVL树首先要了解二叉搜索树,可以见我之前的博客:

C++进阶-二叉搜索树(二叉排序树)_c++二叉树详解-CSDN博客文章浏览阅读1k次,点赞40次,收藏22次。二叉搜索树难度相对于原来的各种知识要考虑的情况更多,后面的AVL树和红黑树难度更高,手动实现的更难,不过以后面试和笔试中要考到这些实现的注意事项,所以一定不要觉得这篇二叉搜索树难就不学了,后面的手动模拟实现map和set难度更上一层楼,所以任重而道远。好了,这讲内容就到这里,下讲将讲解:C++进阶-set,喜欢的可以一键三连哦,下讲再见!!!二叉树的简单讲解(实现详见下两讲)https://blog.csdn.net/2401_86446710/article/details/145887950?_c++二叉树详解 https://blog.csdn.net/2401_86446710/article/details/149307955?spm=1011.2415.3001.10575&sharefrom=mp_manage_link

2.AVL的概念

(1)AVL树是最先发明的⾃平衡⼆叉查找树,AVL可以是⼀颗空树,或者具备下列性质的⼆叉搜索树:它的左右⼦树都是AVL树,且左右⼦树的⾼度差的绝对值不超过1。AVL树是⼀颗⾼度平衡搜索⼆叉树,通过控制⾼度差去控制平衡。

(2)为什么称之为AVL树: AVL树得名于它的发明者G. M. Adelson-Velsky和E. M. Landis是两个前苏联的科学家,他们在1962年的论⽂《An algorithm for the organization of information》中发表了它。

(3)AVL树实现引⼊了平衡因⼦(balance factor)的概念,每个结点都有⼀个平衡因⼦,任何结点的平衡因⼦等于右⼦树的⾼度减去左⼦树的⾼度,也就是说任何结点的平衡因⼦等于0/1/-1,虽然AVL树并不是必须要平衡因⼦,但是有了平衡因⼦可以更⽅便我们去进⾏观察和控制树是否平衡,就像⼀个⻛向标⼀样。

(4)思考⼀下为什么AVL树是⾼度平衡搜索⼆叉树,要求⾼度差不超过1,⽽不是⾼度差是0呢?0不是更好的平衡吗?这是因为有些AVL树本身是一定达不到高度差为0的,比如有两个结点的二叉搜索树,那么就只能一个为根结点,另外一个为根结点的左孩子或右孩子,而这个时候根结点的右子树或左子树高度为0,而左子树或右子树的高度为1,是肯定不能达到高度差为0的。所以说AVL树并不一定达到完美的平衡,但是一定能达到最好的平衡!

(5)AVL树整体结点数量和分布和完全⼆叉树类似,⾼度可以控制在 logN ,那么增删查改的效率也可以控制在 O(logN) ,相⽐⼆叉搜索树有了本质的提升。

如以下的图就是AVL树,其中的结点上的-1/0/1都是平衡因子

3.AVL树基本结构的实现

我们实现AVL树的结点的时候要实现成key-value类型,因为这样是最符合我们AVL树的结构的,也就是说,我们的模板参数要有两个,一个为K(代表key),一个为V(代表value),同时要在二叉搜索树实现的基础上还要有一个pair用来访问key和value,此外,我们还要增加一个parent指针,因为之后要控制平衡因子,还要增加一个用来存储平衡因子的int类型的变量。

通过这些解释,我们能设计出以下结构:

#include<assert.h>
#include<iostream>
using namespace std;
//AVLTree结点定义
template<class K,class V>
struct AVLTreeNode
{pair<K, V> _kv;AVLTreeNode<K, V>* _left;AVLTreeNode<K, V>* _right;AVLTreeNode<K, V>* _parent;//平衡因子int _bf;//构造函数AVLTreeNode(pair<K,V> kv):_kv(kv),_left(nullptr),_right(nullptr),_parent(nullptr),_bf(0){ }
};
//AVLTree的定义
template<class K,class V>
class AVLTree
{typedef AVLTreeNode<K, V> Node;
public://进行任意操作
private:Node* _root = nullptr;
};

以后我们将在AVLTree的public部分进行操作。

4.AVL树的插入(最主要)

4.1大概过程

(1)插⼊⼀个值按⼆叉搜索树规则进⾏插⼊;

(2)新增结点以后,只会影响祖先结点的⾼度,也就是可能会影响部分祖先结点的平衡因⼦,所以更新从新增结点->根结点路径上的平衡因⼦,实际中最坏情况下要更新到根,有些情况更新到中间就可以停⽌了,具体情况我们下⾯再详细分析;

(3)更新平衡因⼦过程中没有出现问题(_bf=0/1/-1),则插⼊结束;

(4)更新平衡因⼦过程中出现不平衡,对不平衡⼦树旋转,旋转后本质调平衡的同时,本质降低了⼦树的⾼度,不会再影响上⼀层,所以插⼊结束。

4.2平衡因子的更新

4.2.1更新原则

(1)平衡因子=右子树的高度-左子树的高度;

(2)只有子树的高度变化才会影响当前结点的平衡因子;

(3)插入结点,可能会增加高度,如果新增结点在parent的右⼦树,parent的平衡因⼦++;如果新增结点在parent的左⼦树,parent平衡因⼦--;

(4)parent所在⼦树的⾼度是否变化决定了是否会继续往上更新;

4.2.2更新停止条件

更新停止主要是看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⼦树的⾼度,恢复到插⼊结点以前的⾼度。所以旋转后也不需要继续往上更新,插⼊结束。

(4)不断更新,更新到根,根的平衡因⼦是1或-1也不再继续往上更新了。

4.3初步代码

通过以上两点,我们可以写出初步代码:

bool Insert(const pair<K, V>& kv)
{//如果AVL树是空树,那么就直接创建完根结点就结束if (_root == nullptr){_root = new Node(kv);return true;}Node* parent = nullptr;Node* cur = _root;//按照二叉搜索树的相同逻辑进行插入操作(不更新平衡因子)while (cur){//我们要比较的是二者的first大小,所以要:cur->_kv.first与kv.first(新插入的结点)//新插入的结点更大,往右子树走if (cur->_kv.first < kv.first){//要更新parent,之后要更新平衡因子parent = cur;cur = cur->_right;}//新插入的结点更小,往左子树走else if (cur->_kv.first > kv.first){parent = cur;cur = cur->_left;}//不能插入相同的值的话,我们就只能return false;else{return false;}}//这个时候插入结束代表cur=nullptr//这个时候cur的位置就是新增结点的位置cur = new Node(kv);//这个时候我们还需要更新cur与parent的关系if (parent->_kv.first > kv.first){//为左孩子parent->_left = cur;}else{//为右孩子parent->_right = cur;}//记得把cur->_parent=parent;cur->_parent = parent;//更新平衡因子//循环结束条件就是parent==nullptr(也可以写成cur==_root)时while (parent){//先调整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 = cur->_parent;}//平衡因子为2/-2,需要进行旋转操作else if (parent->_bf == 2 || parent->_bf == -2){//旋转操作//之后代码写的地方//旋转后结果直接结束循环break;}//代表之前平衡因子更新有问题,之前AVL树就已经不平衡了else{assert(false);}}//到这里也是插入成功的return true;
}

5.AVL树的旋转(最难)

5.1旋转的原则

(1)保持搜索树的规则;

(2)让旋转的树从不满⾜变平衡,其次降低旋转树的⾼度。

其中旋转分为四种:左单旋/右单旋/左右双旋/右左双旋。

我将根据各种旋转对应的情况来分析。

5.2右单旋

插入新结点之前:

其中a、b、c的高度都为h(h>=0)。

插入新结点之后:

新增结点之后,a的高度变成了h+1,导致了parent的平衡因子变成了-2,这种情况我们就要改变成这样:

我们只关心变化指针的指向的部分,比如:10的左孩子原来是5结果现在指向了5的右孩子,此外,5和10的两个关系改变了,5从10的左孩子变成了10的父亲,10从5的父亲变成了5的右孩子,且这个时候5和10的平衡因子都变成了0。

这个时候我们还要考虑一下,原来10的parent存不存在,如果存在,那么就需要改变10的parent的孩子指向,且还要判断10原来是parent的左孩子还是右孩子,这个时候就要多判断一步了,如果说没有,就没必要考虑。

我们另5这个结点为subL,则b这个结点为subLR,则这个时候可以:

在Inert函数加上:

//右单旋
if (parent->_bf == -2 && cur->_bf == -1)
{//我们之后的右左和左右单旋可能要用到这个右单旋,所以先写成函数形式RotateR(parent);
}

在AVLTree类中加上:

//右单旋
void RotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;//10的左孩子变成5的右孩子parent->_left = subLR;//5的右孩子变成10subL->_right = parent;//改变每个孩子的父母指向//h可能为0(即b和c可能不存在)//所以要先判断一下//b存在if (subLR){//b的父亲变成10subLR->_parent = parent;}//其次还可能parent->_parent==nullptr//即parent==_root时,就要直接改变5的父亲是nullptr即可//否则,我们就要改变10的原父亲的孩子指向Node* parentParent = parent->_parent;parent->_parent = subL;if (parent == _root){//把5的父亲置为nullptrsubL->_parent = nullptr;//并把_root置为subL;_root = subL;}else{//原来的10结点是其父亲的左孩子if (parentParent->_left == parent){//原来的10结点的左孩子变成5parentParent->_left = subL;}else{//原来的10结点的右孩子变成5parentParent->_right = subL;}//5的父亲变成原来的10结点的父亲subL->_parent = parentParent;}//改变二者的平衡因子(subL、parent)subL->_bf = parent->_bf = 0;
}

这个右单旋的改变如果不结合图形来理解,那么最终会导致这些代码看不懂的,建议各位看代码之前先要理解一下如何旋转的!

5.3左单旋

新增结点之前:

其中a、b、c的高度为h(h>=0)。

新增结点之后会变成:

也就是说:当parent->_bf==2 并且当cur->_bf==1时,这种情况就要用到左单旋!

这个时候我们左单旋最终的结果是:

同样,我们需要观察其指向的变化:

首先10的右孩子从15变成了b,15的左孩子从b变成了10,如果h在不为0的情况下(即b不为nullptr),还需要改变b的父亲指向成10,然后还要注意:10的父亲从原来的父亲变成了15,15的父亲从原来的10变成了10原来的父亲,如果10原来的父亲存在的情况下(10原来的父亲不是nullptr),也需要改变10原来的父亲的孩子指向,这个完全可以类似于右单旋,最后再改变15和10的平衡因子为0。

在Insert中加:

//左单旋
else if (parent->_bf == 2 && cur->_bf == 1)
{RotateL(parent);
}

我们可以令10为parent,15为subR,b为subRL,则在AVLTree类中加:

//左单旋
void RotateL(Node* parent)
{//15结点Node* subR = parent->_right;//bNode* subRL = subR->_left;//先改变孩子的指向//15的右孩子变成bparent->_right = subRL;//再改变b的父亲//如果高度h不为0才进行操作if (subRL){subRL->_parent = parent;}//10的左孩子变成15subR->_left = parent;//我们先存储起来parent->_parent,不然很容易绕晕Node* parentParent = parent->_parent;//改变10的父亲为15parent->_parent = subR;//如果parentPrent不存在即parent==_root时if (parent == _root){//15的父亲变成nullptrsubR->_parent = nullptr;//并把_root置为subR_root = subR;}else{//10为原来为父亲的左孩子if (parentParent->_left == parent){//则10原来的父亲的左孩子变成了15parentParent->_left = subR;}else{parentParent->_right = subR;}//还要记得改变15的父亲为parentParentsubR->_parent = parentParent;}//改变平衡因子subR->_bf = parent->_bf = 0;
}

这个可以根据右单旋进行改变指向,但是一定不要遗漏什么!

5.5左右双旋

没有增加结点之前的图形为:

a、b、c的高度为h(h>=0)。

插入结点后,变成:

这个时候我们要处理的情况更多了,当h为0时,假设是这样的:

那么就要变成这样:

当h不为0,原图可以(假设)变成:

则这样又延伸出两种情况:

(1)e新增时:

这个时候要先变成:

这就相当于我们把5看成parent的左单旋。

再改变成这样:

这也就相当于把5和a和e看成右单旋中的a,再把10看成parent,完成右单旋操作。

(2)f新增时

会变成如下形式:

我们就可以先变成这样:

再变成这样:

这个对齐方式都是差不多的,也就是说不管怎么样都是需要先进行左单旋,再进行右单旋,所以称之为左右双旋,第一个是传递parent->_left为参数进行的左单旋,第二个是传递parent为参数的右单旋,所以在Insert函数可以这样添加:

//左右双旋
else if (parent->_bf == -2 && cur->_bf == 1)
{RotateLR(parent);
}

如果单纯的调用左单旋函数和右单旋函数这种方式那么就会造成平衡因子混乱的问题,所以我们需要把8的平衡因子分情况讨论(直接令8的平衡因子为bf):

(1)bf=0时,代表就是h=0时,那么这个时候我们观察一下,10和5的平衡因子都变成了0;

(2)bf=1时,代表就是f高度变化时,这个时候我们可以通过观察发现,10的平衡因子为0,5的平衡因子为-1;

(3)bf=-1时,代表就是e高度变化时,这个时候我们可以通过观察发现,10的平衡因子为1,5的平衡因子为0。

我们还是用原来的命名方式,把10作为parent,5作为subL,8作为subLR,这个时候我们可以写出以下代码:

//左右双旋
void RotateLR(Node* parent)
{//将5作为subLNode* subL = parent->_left;//将8作为subLRNode* subLR = subL->_right;//这个时候就要记住subLR->_bf的值了,因为之后左单旋和右单旋会改变平衡因子int bf = subLR->_bf;//左单旋RotateL(parent->_left);//右单旋RotateR(parent);//改变平衡因子if (bf == 0){subL->_bf = 0;parent->_bf = 0;}else if (bf == 1){subL->_bf = -1;parent->_bf = 0;}else if (bf == -1){subL->_bf = 0;parent->_bf = 1;}else{//平衡因子有问题assert(false);}//记得把8的平衡因子也改一下subLR->_bf = 0;
}

5.6右左双旋

在新增结点之前:

其中a、b、c的高度为h(h>=0)。

新增结点之后:

通过观察parent->_bf==2且cur->_bf==-1时满足右左双旋。

右左双旋分为以下情况

(1)h为0时,这个时候可以类似于没新增结点是这样:

新增结点后变成这样:

这个时候我们很容易发现此时的13结点的_bf=0,而这样第一步为:

这也就相当于把15看做parent的右单旋(15没有右孩子),

最后转化成:

这也就相当于把15作为parent的左单旋。

这样画虽然说简单,但是也很难看出来是右左双旋。

h>=1时,我们在插入之前看成如下形式:

这个时候若b(e+f+13)高度变成h+1,那么就有两种情况了。

(2)e的高度变成h时:

那么第一步旋转就会有:

这一步也就相当于我们把15看成parent的右单旋,也就是这样调用:RotateR(parent->_right);

最终变成:

这就相当于调用RotateL(parent);的左单旋。

这样的结果也是把parent->_bf=0;subR->_bf=1;subRL->_bf=0;

(3)把f变成高度为h:

这个时候的subRL->_bf=1;

这个时候我们就需要先旋转成这样:

这个是RotateR(parent->_right);的右单旋。

最后旋转成这样:

这个时候相当于RotateL(parent);的左单旋,此时parent->_bf=-1;subR->_bf=0;subRL->_bf=0;

根据这些分析,最终在Insert函数里增加:

//右左双旋
else if (parent->_bf == 2 && cur->_bf == -1)
{RotateRL(parent);
}
//平衡因子有问题,不平衡
else
{assert(false);
}

在AVLTree类里面加:

void RotateRL(Node* parent)
{//把15作为subRNode* subR = parent->_right;//把13作为subRLNode* subRL = subR->_left;//存储此时13的平衡因子int bf = subRL->_bf;//右单旋RotateR(parent->_right);//左单旋RotateL(parent);//h=0时//所有的平衡因子都为0if (bf == 0){parent->_bf = 0;subR->_bf = 0;}//f增加//情况3else if (bf == 1){parent->_bf = -1;subR->_bf = 0;}//e增加//情况2else if (bf == -1){parent->_bf = 0;subR->_bf = 1;}//AVL树不平衡//平衡因子非法else{assert(false);}//不要忘记subRL->_bf = 0;
}

5.7AVLTree实现Insert的全部代码

bool Insert(const pair<K, V>& kv)
{//如果AVL树是空树,那么就直接创建完根结点就结束if (_root == nullptr){_root = new Node(kv);return true;}Node* parent = nullptr;Node* cur = _root;//按照二叉搜索树的相同逻辑进行插入操作(不更新平衡因子)while (cur){//我们要比较的是二者的first大小,所以要:cur->_kv.first与kv.first(新插入的结点)//新插入的结点更大,往右子树走if (cur->_kv.first < kv.first){//要更新parent,之后要更新平衡因子parent = cur;cur = cur->_right;}//新插入的结点更小,往左子树走else if (cur->_kv.first > kv.first){parent = cur;cur = cur->_left;}//不能插入相同的值的话,我们就只能return false;else{return false;}}//这个时候插入结束代表cur=nullptr//这个时候cur的位置就是新增结点的位置cur = new Node(kv);//这个时候我们还需要更新cur与parent的关系if (parent->_kv.first > cur->_kv.first){//为左孩子parent->_left = cur;}else{//为右孩子parent->_right = cur;}//更新平衡因子//循环结束条件就是parent==nullptr(也可以写成cur==_root)时while (parent){//先调整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 = cur->_parent;}//平衡因子为2/-2,需要进行旋转操作else if (parent->_bf == 2 || parent->_bf == -2){//旋转操作//之后代码写的地方//右单旋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;}//代表之前平衡因子更新有问题,之前AVL树就已经不平衡了else{assert(false);}}//到这里也是插入成功的return true;
}

6.AVL树其他功能的实现

6.1查找的实现

AVL树查找类似于二叉搜索树的查找,所以代码如下:

//AVL树的查找
bool Find(const K& key)
{Node* cur = _root;while (cur){if (cur->_kv.first < key){cur = cur->_right;}else if (cur->_kv.first > key){cur = cur->_left;}else{return true;}}return false;
}

6.2中序遍历的实现

中序遍历本质上是不会需要传参的,但是标准的中序遍历的实现是需要传参的,这里我们可以借助一个辅助函数来完成,我们调用辅助函数来实现(主要是外部无法访问到_root):

//AVL树的中序遍历
//辅助函数
void InOrder()
{_InOrder(_root);cout << endl;
}
//实现函数
void _InOrder(Node* root)
{if (root == nullptr){return;}_InOrder(root->_left);cout << root->_kv.first << " ";_InOrder(root->_right);
}

6.3计算AVL树的高度

在二叉树时,我们知道,想要知道左子树的高度,就是先递归左子树后递归右子树,最终返回二者最大值+1,因为我们还要算进根结点的高度,所以最终代码为:

//计算高度
//辅助函数
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;
}

6.4判断是否为AVL树(或判断是否平衡)

这个函数的实现也是需要传递参数_root的,所以也要借助一个辅助函数。

实现这个功能时,我们需要用abs算法判断平衡因子大小是否在正常范围(abs(bf)<=1为正常),但是有这么多路径,不可能每一条路径都要判断平衡因子是否正常吧!

我们可以先计算出左子树的高度,再计算出右子树的高度,如果二者相差的绝对值大于等于2就一定是不平衡的。光这样是不够的,因为我们判断高度差没有用处,万一平衡因子有问题,那么之后很难解决,所以我们还要额外增加一个判断:右子树-左子树的高度差是否与平衡因子相同,如果不相同就是不平衡的,反之为平衡的。

所以最终代码如下:

//判断AVL树是否平衡
//辅助函数
bool IsBalanceTree()
{return _IsBalanceTree(_root);
}
//实现函数
bool _IsBalanceTree(Node* root)
{// 空树也是AVL树if (nullptr == root)return true;// 计算pRoot结点的平衡因子:即pRoot左右子树的高度差int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);int diff = rightHeight - leftHeight;// 如果计算出的平衡因子与pRoot的平衡因子不相等,或者// pRoot平衡因子的绝对值超过1,则一定不是AVL树if (abs(diff) >= 2){cout << root->_kv.first << "高度差异常" << endl;return false;}if (root->_bf != diff){cout << root->_kv.first << "平衡因子异常" << endl;return false;}// pRoot的左和右如果都是AVL树,则该树一定是AVL树return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}

6.5计算AVL树的大小

我们计算AVL树的大小,可以用一个?:表达式来求得结果,先前面用判断传入的参数是否是nullptr的,如果是,就为0,否则递归左右子树求得大小最后再加1,(加上根结点),所以最终代码如下:

//计算AVL树的大小
//辅助函数
int Size()
{return _Size(_root);
}
//实现函数
int _Size(Node* root)
{return root == nullptr ? 0 : _Size(root->_left) + _Size(root->_right) + 1;
}

7.最终代码

代码非常长,自己写的时候一定要注意,如果是自己敲出的代码记得问deepseek是否有问题。

#include<assert.h>
#include<iostream>
using namespace std;
//AVLTree结点定义
template<class K,class V>
struct AVLTreeNode
{pair<K, V> _kv;AVLTreeNode<K, V>* _left;AVLTreeNode<K, V>* _right;AVLTreeNode<K, V>* _parent;//平衡因子int _bf;//构造函数AVLTreeNode(pair<K,V> kv):_kv(kv),_left(nullptr),_right(nullptr),_parent(nullptr),_bf(0){ }
};
//AVLTree的定义
template<class K,class V>
class AVLTree
{typedef AVLTreeNode<K, V> Node;
public://进行任意操作//右单旋void RotateR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;//10的左孩子变成5的右孩子parent->_left = subLR;//5的右孩子变成10subL->_right = parent;//改变每个孩子的父母指向//h可能为0(即b和c可能不存在)//所以要先判断一下//b存在if (subLR){//b的父亲变成10subLR->_parent = parent;}//其次还可能parent->_parent==nullptr//即parent==_root时,就要直接改变5的父亲是nullptr即可//否则,我们就要改变10的原父亲的孩子指向Node* parentParent = parent->_parent;parent->_parent = subL;if (parent == _root){//把5的父亲置为nullptrsubL->_parent = nullptr;//并把_root置为subL;_root = subL;}else{//原来的10结点是其父亲的左孩子if (parentParent->_left == parent){//原来的10结点的左孩子变成5parentParent->_left = subL;}else{//原来的10结点的右孩子变成5parentParent->_right = subL;}//5的父亲变成原来的10结点的父亲subL->_parent = parentParent;}//改变二者的平衡因子(subL、parent)subL->_bf = parent->_bf = 0;}//左单旋void RotateL(Node* parent){//15结点Node* subR = parent->_right;//bNode* subRL = subR->_left;//先改变孩子的指向//15的右孩子变成bparent->_right = subRL;//再改变b的父亲//如果高度h不为0才进行操作if (subRL){subRL->_parent = parent;}//10的左孩子变成15subR->_left = parent;//我们先存储起来parent->_parent,不然很容易绕晕Node* parentParent = parent->_parent;//改变10的父亲为15parent->_parent = subR;//如果parentPrent不存在即parent==_root时if (parent == _root){//15的父亲变成nullptrsubR->_parent = nullptr;//并把_root置为subR_root = subR;}else{//10为原来为父亲的左孩子if (parentParent->_left == parent){//则10原来的父亲的左孩子变成了15parentParent->_left = subR;}else{parentParent->_right = subR;}//还要记得改变15的父亲为parentParentsubR->_parent = parentParent;}//改变平衡因子subR->_bf = parent->_bf = 0;}//左右双旋void RotateLR(Node* parent){//将5作为subLNode* subL = parent->_left;//将8作为subLRNode* subLR = subL->_right;//这个时候就要记住subLR->_bf的值了,因为之后左单旋和右单旋会改变平衡因子int bf = subLR->_bf;//左单旋RotateL(parent->_left);//右单旋RotateR(parent);//改变平衡因子if (bf == 0){subL->_bf = 0;parent->_bf = 0;}else if (bf == 1){subL->_bf = -1;parent->_bf = 0;}else if (bf == -1){subL->_bf = 0;parent->_bf = 1;}else{//平衡因子有问题assert(false);}//记得把8的平衡因子也改一下subLR->_bf = 0;}void RotateRL(Node* parent){//把15作为subRNode* subR = parent->_right;//把13作为subRLNode* subRL = subR->_left;//存储此时13的平衡因子int bf = subRL->_bf;//右单旋RotateR(parent->_right);//左单旋RotateL(parent);//h=0时//所有的平衡因子都为0if (bf == 0){parent->_bf = 0;subR->_bf = 0;}//f增加//情况3else if (bf == 1){parent->_bf = -1;subR->_bf = 0;}//e增加//情况2else if (bf == -1){parent->_bf = 0;subR->_bf = 1;}//AVL树不平衡//平衡因子非法else{assert(false);}//不要忘记subRL->_bf = 0;}//AVL树的插入bool Insert(const pair<K, V>& kv){//如果AVL树是空树,那么就直接创建完根结点就结束if (_root == nullptr){_root = new Node(kv);return true;}Node* parent = nullptr;Node* cur = _root;//按照二叉搜索树的相同逻辑进行插入操作(不更新平衡因子)while (cur){//我们要比较的是二者的first大小,所以要:cur->_kv.first与kv.first(新插入的结点)//新插入的结点更大,往右子树走if (cur->_kv.first < kv.first){//要更新parent,之后要更新平衡因子parent = cur;cur = cur->_right;}//新插入的结点更小,往左子树走else if (cur->_kv.first > kv.first){parent = cur;cur = cur->_left;}//不能插入相同的值的话,我们就只能return false;else{return false;}}//这个时候插入结束代表cur=nullptr//这个时候cur的位置就是新增结点的位置cur = new Node(kv);//这个时候我们还需要更新cur与parent的关系if (parent->_kv.first > kv.first){//为左孩子parent->_left = cur;}else{//为右孩子parent->_right = cur;}//记得把cur->_parent=parent;cur->_parent = parent;//更新平衡因子//循环结束条件就是parent==nullptr(也可以写成cur==_root)时while (parent){//先调整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 = cur->_parent;}//平衡因子为2/-2,需要进行旋转操作else if (parent->_bf == 2 || parent->_bf == -2){//旋转操作//右单旋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;}//代表之前平衡因子更新有问题,之前AVL树就已经不平衡了else{assert(false);}}//到这里也是插入成功的return true;}//AVL树的查找bool Find(const K& key){Node* cur = _root;while (cur){if (cur->_kv.first < key){cur = cur->_right;}else if (cur->_kv.first > key){cur = cur->_left;}else{return true;}}return false;}//AVL树的中序遍历//辅助函数void InOrder(){_InOrder(_root);cout << endl;}//实现函数void _InOrder(Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_kv.first << " ";_InOrder(root->_right);}//计算高度//辅助函数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;}//判断AVL树是否平衡//辅助函数bool IsBalanceTree(){return _IsBalanceTree(_root);}//实现函数bool _IsBalanceTree(Node* root){// 空树也是AVL树if (nullptr == root)return true;// 计算pRoot结点的平衡因子:即pRoot左右子树的高度差int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);int diff = rightHeight - leftHeight;// 如果计算出的平衡因子与pRoot的平衡因子不相等,或者// pRoot平衡因子的绝对值超过1,则一定不是AVL树if (abs(diff) >= 2){cout << root->_kv.first << "高度差异常" << endl;return false;}if (root->_bf != diff){cout << root->_kv.first << "平衡因子异常" << endl;return false;}// pRoot的左和右如果都是AVL树,则该树一定是AVL树return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);}//计算AVL树的大小//辅助函数int Size(){return _Size(_root);}//实现函数int _Size(Node* root){return root == nullptr ? 0 : _Size(root->_left) + _Size(root->_right) + 1;}
private:Node* _root = nullptr;
};

我这里提供了一个测试代码,以方便你判断自己代码是否有误,代码如下:

// 测试代码
void TestAVLTree1()
{AVLTree<int, int> t;// 常规的测试用例//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };// 特殊的带有双旋场景的测试用例int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };for (auto e : a){t.Insert({ e, e });}t.InOrder();cout << t.IsBalanceTree() << endl;
}

8.AVL树的删除

AVL树的删除相对于插入麻烦了一些,我这里只讲解思路,因为AVL树的删除在面试中和笔试中不常考,所以感兴趣的可以自己实现一下:

(1)按照二叉搜索树的方法进行删除操作;

(2)从删除结点开始往上,更新平衡因子,如果是parent的左子树被删除,平衡因子++;如果是右子树被删除,平衡因子--;

(3)如果平衡因子为0,停止更新;

(4)如果平衡因子为1/-1,继续往上更新;

(5)如果平衡因子为2/-2,则需要进行旋转操作,旋转不是我们新增结点的旋转,至于如何选择需要各位自己去画图理解(ps:本篇博客花了我8个小时的时间,画图已经让煮波微丝了),最好还是用手去画图,电脑上的画图我是为了增强理解而来,电脑画图建议用比较好的画图软件。最后再进行更新平衡因子操作,停止更新。

(6)遇到根结点,如果根结点的平衡因子是1/-1,也停止往上更新。

基本思路和AVL树的插入差不多。

9.总结

AVL树是C++中一个特别抽象的东西,我也只是实现了AVL树的插入操作,一般情况下,如果实现删除操作,代码就差不多700行左右,AVL树要理解最重要的就是画图,只有画图才能真正理解AVL树的旋转操作,特别是手敲代码的时候,可能会遇到很多的错误,包括但不限于:遗漏步骤、条件判断错误、更新平衡因子不全的问题。虽然说我的代码现在是经过测试过没问题的,但是我还是找了很久才找到错误的,这种大程序建议各位能测试一个函数就测试一个函数,否则最后一起测试找错误难度很高!

虽然AVL树难度高,但是只要学会了AVL树,你也会发现AVL树其实也就那样,学C++就是要靠理解其内部逻辑,AVL树这种内部逻辑也就是需要画图来理解的,因为你自己设计出来一个AVL树进行测试那么难度比较高,各位可以根据画图+代码的形式一步一步的了解如何实现AVL树的。

好了,这一讲就到这里结束了,下一讲将进行讲解:C++进阶-红黑树(难度较高),红黑树难度也是比较高的一个,它还比较抽象,但是它又比较重要,所以建议下篇博客不要缺席哦!

喜欢的可以一键三连,下讲再见!

http://www.lryc.cn/news/591735.html

相关文章:

  • 由几道数量关系考题引起的思考
  • 【CodeTop】每日练习 2025.7.17
  • Python类型转换,深浅拷贝
  • 【深度学习】神经网络过拟合与欠拟合-part5
  • DiffPy-CMI详细安装教程
  • ubuntu 22.04 pam 模块设置用户登录失败锁定
  • 网络基础11 上公网--Internet接入技术
  • python的旧时光咖啡厅数据分析管理系统
  • 深入理解CSS定位:绝对定位的包含块机制
  • JUnit5 实操
  • 征程 6 UCP 任务优先级 抢占简介与实操
  • 流程控制( break与continue)
  • Xss-labs 靶场lever1~lever8通关练习
  • windows利用wsl安装qemu
  • HD现代机器人与TESOLLO合作推出工业自动化双臂机器人解决方案
  • 为什么喜欢叫index文件
  • javax.servlet.http.HttpServletResponse;API导入报错解决方案
  • 找不到或无法加载主类 org.gradle.wrapper.GradleWrapperMain
  • 4G模块 A7680通过MQTT协议连接到腾讯云
  • 初试Spring AI实现聊天功能
  • 「Chrome 开发环境快速屏蔽 CORS 跨域限制详细教程」*
  • 基于现代R语言【Tidyverse、Tidymodel】的机器学习方法
  • 关于pytorch虚拟环境及具体bug问题修改
  • 2025 XYD Summer Camp 7.17 模考
  • 【面板数据】上市公司股价同步性数据集-dta+xlsx(2000-2023年)
  • Adobe Acrobat 插件功能、应用与开发
  • 【Spring AI Alibaba实战Demo】通过Spring AI Alibaba接入本地部署的大模型和线上大模型,实现流式简单对话
  • 8.预处理-demo
  • 【DOCKER】-5 镜像仓库与容器编排
  • docker中 contriner 和 images 什么关系