【C++】AVL树(动图详解)
文章目录
- 一、前言
- 二、AVL树的概念(引入bf)
- 三、AVL节点树的定义
- 四、AVL树的基本框架
- 五、AVL树的旋转
- 5.1左单旋(新节点插入较高右子树的右侧---右右:左单旋)
- 例一(h==0)
- 例二(h==1)
- 例三(抽象图)
- 代码讲解
- 1.更新双亲节点
- 2.处理局部子树问题
- 3.更新平衡因子
- 4.代码汇总
- 代码总结(俩孩子三双亲)
- 5.2左单旋(新节点插入较高左子树的左侧---左左:右单旋)
- 例一(h==0)
- 例二(h==1)
- 例三(抽象图)
- 代码总结(代码解释见左单旋)
- 5.3左右双旋(新节点插入较高左子树的右侧---左右:先左单旋再右单旋)
- 例一(h==0)
- 例二(h==1)
- 例三(抽象图)
- 代码讲解
- 5.4右左双旋(新节点插入较高右子树的左侧---右左:先右单旋再左单旋)
- 抽象图
- 六、AVL树的插入
- 七、AVL树相关的一些测试
- 八、完整代码
- AVL.h
- AVL.cpp
- 九、难点总结
- 旋转的四个原则
- 在进行旋转处理后就无需继续往上更新平衡因子了
- 旋转口诀是插入较高... 不要把这个较高给忽略掉
- 写代码的时候,头脑要清楚,不要忘了回溯parent的值(向上分配)
- 插入代码更新平衡因子的条件是while(parent)
- 对于单旋,利用俩孩子三双亲原则;对于双旋,难点在于平衡因子更新,得画出插入前和旋转后的图分析平衡因子(三种情况)
- 双旋得提前记录subLR或者subRL的bf(旋转完了都找不到了)
- 单旋得提前记录parent的_parent,也就是代码中的ppNode
- 单旋的时候,解决局部子树问题时,总会忘记向上分配
一、前言
在上一篇文章中对 set、multiset、map、multimap 进行了简单的介绍,在其文档介绍中发现,这几个容器都有一个共同点:其底层都是借助二叉搜索树来实现的。但是==二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成为单支树==,时间复杂度会退化成 O(N) ,因此 set、map 等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。那么今天就让我们对 set 和 map 的底层结构一探究竟。
二、AVL树的概念(引入bf)
二叉搜索树虽然可以缩短查找的效率,但是如果数据有序或者接近有序,二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率就会变得低下。
因此,两位俄罗斯的数学家 G.M.Adelson-Velskii 和E.M.Landis 在 1962 年发明了一种解决上述问题的方法:当向二叉搜索树中插入新节点后,如果能保证每个节点的左右子树高度之差的绝对值不超过 1(超过 1 需要对树中的结点进行旋转,后续会讲,也被叫做动手术),即可降低树的高度,从而减少平均搜索长度。
一颗 AVL 树或者是空树,或者是具有以下性质的二叉搜索树:
- 它的左右子树都是 AVL 树。
- 左右子树高度之差(简称平衡因子)的绝对值不超过 1(-1、0、1)。
下面是一个AVL树加上平衡因子(balance factor)的图,其中比节点大的放在右边,比节点小的放在左边,平衡因子的计算是通过右子树的高度-左子树的高度:
小Tips:如果一颗二叉搜索树是高度平均的,它就是 AVL 树。如果它有 n 个结点,其高度可保持在 O(log2^n),搜索时间复杂度为 O(log2^n)。平衡二叉搜索树中平衡并不是说要让平衡因子始终保持 0,这是做不到的,以两个结点为例,根节点的平衡因子只可能是 1 或者 -1,不可能是 0。二叉搜索树在实际中基本不太可能实现完全平衡,但是理论上可以,即满二叉树。后面我们学的多叉平衡搜索树在现实中可以做到完全平衡。
三、AVL节点树的定义
我们接下来就准备手撕 AVL 树了,关键点在于俩个地方:
- 二叉搜索树的插入
- 更新平衡因子
template<class K, class V>
struct AVLTreeNode
{AVLTreeNode(const pair<K, V>& kv = pair<K, V>()):_kv(kv),_left(nullptr),_right(nullptr),_parent(nullptr),_bf(0){}pair<K, V> _kv;//存储key和valueAVLTreeNode<K, V>* _left = nullptr;//指向左孩子AVLTreeNode<K, V>* _right = nullptr;//指向右孩子AVLTreeNode<K, V>* _parent = nullptr;//指向父亲结点int _bf = 0;//平衡因子
};
节点为什么置空?
在AVL树中有很多的节点需要封装,而且对于没有值的节点一定要置空,将来我们单旋的时候有一个小插曲 if(SUbL) 如果我们初始化的时候没有置空的话,这个条件的书写很明显就是不对的
为什么引入parent节点?
使AVL树变成三叉链,是为了方便将来沿着路径一直向上更新,当然有利有弊:往回找的时候方便了,修改的时候麻烦了,除了修改孩子,还得修改双亲
平衡因子的引入:
最重要的还是平衡因子的封装加入AVL树节点定义的大家庭
四、AVL树的基本框架
template<class K, class V>class AVLTree{typedef AVLTreeNode<K, V> Node;private:Node* _root = nullptr;};
我们把上面结构体定义出来的AVLTreeNode<K, V>重新命名成 Node 方便之后命名轻松
私有变量是我们的根节点 _root
五、AVL树的旋转
正常来说,应该先写插入再写旋转,但是由于插入代码中,更新平衡因子的时候需要调用旋转的接口,在不同的情况有不同的旋转方式,但是我们还不了解旋转,所以我书写的时候先书写的是旋转
首先,凡事皆有原则,我们的旋转也不例外:
- 子树左右高度不超过1
- 旋转过程中必须保持它是搜索树
- 更新调整孩子节点的平衡因子
- 降低这颗子树的高度
这个原则必须牢记于心,这样的话更有利于帮助我们理解后续的四种旋转
5.1左单旋(新节点插入较高右子树的右侧—右右:左单旋)
左单旋的定义:新节点插入较高右子树的右侧—右右:左单旋
动图中,我们的a,b,c分别代表==子树==,并不是节点,所以接下来我们举俩个具体的a,b,c例子,以及抽象图的总结
例一(h==0)
例二(h==1)
由于我们上方说到,我们的旋转都是满足我们的旋转四大原则的,杭哥在讲这个例子的时候说:b(也就是45)放在30的右边是正好满足二叉搜索树,但是我想难道60左边放25不可以吗?
后来我想了想,发现60的左侧的数字,一定是(30,60),而不是(0,60),所以b一定放在30右侧满足旋转规则这个问题告一段落
例三(抽象图)
等学到后期,看抽象图的困难程度就会大大降低,在本图中,我们有很多变量,都是为了和接下来的代码进行匹配
ppNode:担心旋转树为子树,从而旋转时丢失根与上方联系所创造出来的节点
parent:是旋转的函数参数,也是这个子树(或者树)旋转前的根
SubR:parent的右侧
SubRL:parent的右侧的左侧
以上如此多节点的设置,正是为了保证节点的丢失,以及三叉链中不光更新孩子,还得更新父亲的特性
代码讲解
根据上方小羊精美的图,我们不难写出以下代码:
void RotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right=subRL;subR->_left=parent;
}
这段代码是我自己在没理解旋转之前写的天花板代码,但是还有很多问题我并没有考虑:
1. 我只更新了孩子节点,也就是说我只做了向下分配
2. 我的parent节点已经丢失,所以也得向上分配(这里有个小插曲)
3. 如果这个树是局部子树怎么办?
4. AVL 树最根本的平衡因子更新没有体现
根据以上问题,我们还得继续写出代码
1.更新双亲节点
根据抽象图,我们可以看出:b的_parent节点变成了parent也就是30,parent 的 _parent 节点变成了 SUbR ,也就是30的双亲变成了60
subRL->_parent = parent;
parent->_parent = subR;
这个时候小插曲来了:我们的b是一个子树,如果子树b为空,它还有双亲节点吗?所以我们得用if条件判断一下
但是为什么parent不用判断是否为空? 实则在抽象图中就可以明白,为什么赋予这俩个变量30和60,就是说明parent和SubR不可为空,否则旋转问题无法继续讨论
if (subRL!=nullptr)
{subRL->_parent = parent;
}
parent->_parent = subR;
2.处理局部子树问题
由于我们的parent节点在旋转的过程中已经不是该树的根了,所以一开始初始化的时候,记得记录30的双亲节点
void RotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;Node* ppNode=parent->_parent;//这里parent->_right=subRL;subR->_left=parent;//双亲if (subRL!=nullptr){subRL->_parent = parent;}parent->_parent = subR;
}
这里分俩种情况:
- 是独立子树,那么就更新_root节点,并且将其 _parent 节点置空
- 如果是局部子树,若为ppNode左子树,就让其left连接SubR;若为ppNode右子树,就让其right连接SubR
//判断是否为局部子树 需要在parent被修改之前 写出ppNodeif(ppNode==nullptr){//说明是完整的树_root=subR;_root->_parent=nullptr;}else{if(ppNode->_left==parent)//虽然parent被旋转了 但是上层的ppNode还是连着parent节点 哈哈 当年的自己提出这个问题 现在的我自己解决了{ppNode->_left=subR;}else{ppNode->_right=subR;}//这里还有一句话}
这个时候,else语句中还是只有向下分配,所以别忘了加上subR->_parent = ppNode;
3.更新平衡因子
30和60的平衡因子全部为0,也就是parent和SubR的_bf为0
parent->_bf = subR->_bf = 0;
4.代码汇总
void RotateL(Node* parent)
{//俩个孩子 三个双亲 好好品这句话//1.更新孩子节点(向下分配)Node* subR = parent->_right;Node* subRL = subR->_left;Node* ppNode=parent->_parent;//为了避免是局部子树parent->_right=subRL;subR->_left=parent;//2.更新双亲节点(往回分配)if(subRL!=nullptr)//往回分配的小插曲 空节点没有双亲节点{subRL->_parent=parent;}parent->_parent=subR;//3.判断是否为局部子树 需要在parent被修改之前 写出ppNodeif(ppNode==nullptr){//说明是完整的树_root=subR;_root->_parent=nullptr;}else{if(ppNode->_left==parent)//虽然parent被旋转了 但是上层的ppNode还是连着parent节点 {ppNode->_left=subR;}else{ppNode->_right=subR;}subR->_parent = ppNode;}//4.更新平衡因子parent->_bf=subR->_bf=0;//更新平衡因子
}
代码总结(俩孩子三双亲)
我们在写AVL旋转的时候,一定要格局打开:我们在旋转树内的节点的时候,殊不知根节点已经岌岌可危,所以这就是局部子树的问题,而且向下分配完了别忘了向上分配,三叉链使得往上找轻松了,但也使得旋转时维护树的成本变高了
AVL的代码逻辑可以理解成俩个孩子三个双亲:因为我们的局部子树其实也是双亲问题
同时别忘了b子树的小插曲,而且局部子树 else 的时候也容易忘记向上分配,同时最后也别忘了平衡因子的更新
5.2左单旋(新节点插入较高左子树的左侧—左左:右单旋)
例一(h==0)
例二(h==1)
例三(抽象图)
代码总结(代码解释见左单旋)
void RotateR(Node* parent){//俩个孩子 三个双亲 好好品这句话//1.更新孩子节点(向下分配)Node* subL=parent->_left;Node* subLR=subL->_right;Node* ppNode=parent->_parent;//为了避免是局部子树parent->_left=subLR;subL->_right=parent;//2.更新双亲节点(往回分配)if(subLR!=nullptr)//往回分配的小插曲 空节点没有双亲节点{subLR->_parent=parent;}parent->_parent=subL;//3.判断是否为局部子树 需要在parent被修改之前 写出ppNodeif(ppNode==nullptr){//说明是完整的树_root=subL;_root->_parent=nullptr;}else{if(ppNode->_left==parent)//虽然parent被旋转了 但是上层的ppNode还是连着parent节点 哈哈 当年的自己提出这个问题 现在的我自己解决了{ppNode->_left=subL;}else{ppNode->_right=subL;}subL->_parent = ppNode;//局部子树else总会忘记向上分配}//4.更新平衡因子parent->_bf=subL->_bf=0;//更新平衡因子}
5.3左右双旋(新节点插入较高左子树的右侧—左右:先左单旋再右单旋)
先说结论:我们需要对parent->left进行左单旋 再对parent进行右单旋即可
为什么我们需要引入左右单旋这个场景呢?我们的普通单旋为什么解决不了问题了,请看以下这些情况
例一(h==0)
先把弯的缕直,再进行单旋
例二(h==1)
例三(抽象图)
代码讲解
此时我们可以基本写出以下代码:
void RotateLR(Node* parent){// Node* subL=parent->_left;// Node* subLR=subL->_right;RotateL(parent->_left);RotateR(parent);}
但是非常遗憾的是:我们的抽象图看似汇集了所有情况,实则只有60节点左侧插入的情况,但是左右双旋的平衡因子更新本质上是有三种情况的:
- 60的左节点插入
- 60的右节点插入
- 60本身就是插入节点
我们将这个状态的SubLR的平衡因子记录下来为bf 因为旋转完之后,我们很难考察是哪个位置插入了节点,所以在旋转之前记录bf
从以上情况分析平衡因子就不再吃力了,完整代码如下:
void RotateLR(Node* parent){Node* subL=parent->_left;Node* subLR=subL->_right;int bf=subLR->_bf;//为了判读平衡因子改变的三种情况RotateL(parent->_left);RotateR(parent);if(bf==-1)//subLR左子树插入{subL->_bf=0;subLR->_bf=0;parent->_bf=1;}if(bf==1)//subLR右子树插入{subL->_bf=-1;subLR->_bf=0;parent->_bf=0;}if(bf==0)//本身就是新节点被插入{subL->_bf=0;subLR->_bf=0;parent->_bf=0;}}
总结:分析平衡因子的时候,应该同时对照着插入前和插入俩次旋转完成后的图像去判断平衡因子的大小
5.4右左双旋(新节点插入较高右子树的左侧—右左:先右单旋再左单旋)
先对parent的right进行右单旋,再对parent进行左单旋即可
在这个例子中,我们只绘出抽象图,以方便对照着写出代码,不在举出详细例子
抽象图
接着,我们需要讨论出旋转之前的以及插入到60的左,60的右,包括60是新节点的三种情况的平衡因子,这里不再赘述,类似左右双旋
void RotateRL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;RotateR(parent->_right);RotateL(parent);if (bf == 1){subR->_bf = 0;subRL->_bf = 0;parent->_bf = -1;}else if (bf == -1){subR->_bf = 1;subRL->_bf = 0;parent->_bf = 0;}else if (bf == 0){subR->_bf = 0;subRL->_bf = 0;parent->_bf = 0;}else{assert(false);}}
六、AVL树的插入
AVL树插入结点时有以下三个步骤:
- 按照二叉搜索树的插入方法,找到待插入位置。
- 找到待插入位置后,将待插入结点插入到树中。
- 更新平衡因子,如果出现不平衡,则需要进行旋转。
因为AVL树本身就是一棵二叉搜索树,因此寻找结点的插入位置是非常简单的,按照二叉搜索树的插入规则:
- 待插入结点的key值比当前结点小就插入到该结点的左子树。
- 待插入结点的key值比当前结点大就插入到该结点的右子树。
- 待插入结点的key值与当前结点的key值相等就插入失败。
如此进行下去,直到找到与待插入结点的key值相同的结点判定为插入失败,或者最终走到空树位置进行结点插入。
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;cur->_parent = parent;}else{parent->_left = cur;cur->_parent = parent;}
}
以上这段代码只是单纯的插入代码,如果我们不更新平衡因子的话,那么AVL存在的意义也就没有了,所以我们得更新平衡因子
与二叉搜索树插入结点不同的是,AVL树插入结点后需要更新树中结点的平衡因子,因为插入新结点后可能会影响树中某些结点的平衡因子。
由于一个结点的平衡因子是否需要更新,是取决于该结点的左右子树的高度是否发生了变化,因此插入一个结点后,该结点的祖先结点的平衡因子可能需要更新。
所以我们插入结点后需要倒着往上更新平衡因子,更新规则如下:
- 新增结点在parent的右边,parent的平衡因子 ++
- 新增结点在parent的左边,parent的平衡因子 --。
每更新完一个结点的平衡因子后,都需要进行以下判断:(注:这里的parent不是最上面那个节点,新增节点的祖先是parent)
- 如果parent的平衡因子等于-1或者1,表明还需要继续往上更新平衡因子。
- 如果parent的平衡因子等于0,表明无需继续往上更新平衡因子了。
- 如果parent的平衡因子等于-2或者2,表明此时以parent结点为根结点的子树已经不平衡了,需要进行旋转处理。(得动手术)
判断理由说明:
parent更新后的平衡因子 | 分析 |
---|---|
-1或1 | 只有0经过 ++或-- 操作后会变成-1/1,说明新结点的插入使得parent的左子树或右子树增高了,即改变了以parent为根结点的子树的高度,从而会影响parent的父结点的平衡因子,因此需要继续往上更新平衡因子。 |
0(恰好插入较矮的那边) | 只有-1/1经过 ++或-- 操作后会变成0,说明新结点插入到了parent左右子树当中高度较矮的一棵子树,插入后使得parent左右子树的高度相等了,此操作并没有改变以parent为根结点的子树的高度,从而不会影响parent的父结点的平衡因子,因此无需继续往上更新平衡因子。 |
-2或2 | 此时parent结点的左右子树高度之差的绝对值已经超过1了,不满足AVL树的要求,因此需要进行旋转处理。 |
注意: parent的平衡因子在插入更新之前只可能是-1/0/1(AVL树中每个结点的左右子树高度之差的绝对值不超过1)。 |
而在最坏情况下,我们更新平衡因子时会一路更新到根结点。例如下面这种情况:(所以我们的代码得写成while(parent))
三叉链结构的优势体现: 由于我们插入结点后需要倒着往上进行平衡因子的更新,所以我们将AVL树结点的结构设置为了三叉链结构,这样我们就可以通过父指针找到其父结点,进而对其平衡因子进行更新。当然,我们也可以不用三叉链结构,可以在插入结点时将路径上的结点存储到一个栈当中,当我们更新平衡因子时也可以通过这个栈来更新祖先结点的平衡因子,但是相对较麻烦。
若是在更新平衡因子的过程当中,出现了平衡因子为-2/2的结点,这时我们需要对以该结点为根结点的树进行旋转处理,而旋转处理分为四种,在进行分类之前我们首先需要进行以下分析:
我们将插入结点称为cur,将其父结点称为parent,那么我们更新平衡因子时第一个更新的就是parent结点的平衡因子,更新完parent结点的平衡因子后,若是需要继续往上进行平衡因子的更新,那么我们必定要执行以下逻辑:
cur = parent;
parent = parent->_parent;
这里我想说明的是:当parent的平衡因子为-2/2时,cur的平衡因子必定是-1/1而不会是0。
理由如下:
若cur的平衡因子是0,那么cur一定是新增结点,而不是上一次更新平衡因子时的parent,否则在上一次更新平衡因子时,会因为parent的平衡因子为0而停止继续往上更新。
那么我们按照cur是新增节点来进行推导,就会发现有bug:
如果cur是新增结点的话,其父结点的平衡因子更新后一定是-1/0/1,而不可能是-2/2,因为新增结点最终会插入到一个空树当中,在新增结点插入前,其父结点的状态有以下两种可能:
- 其父结点是一个左右子树均为空的叶子结点,其平衡因子是0,新增结点插入后其平衡因子更新为-1/1。
- 其父结点是一个左子树或右子树为空的结点,其平衡因子是-1/1,新增结点插入到其父结点的空子树当中,使得其父结点左右子树当中较矮的一棵子树增高了,新增结点后其平衡因子更新为0。
综上所述,当parent的平衡因子为-2/2时,cur的平衡因子必定是-1/1而不会是0。
根据此结论,我们可以将旋转处理分为以下四类:
- 当parent的平衡因子为-2,cur的平衡因子为-1时,进行右单旋。(左左)
- 当parent的平衡因子为-2,cur的平衡因子为1时,进行左右双旋。(左子树右侧)
- 当parent的平衡因子为2,cur的平衡因子为-1时,进行右左双旋。(右子树左侧)
- 当parent的平衡因子为2,cur的平衡因子为1时,进行左单旋。(右右)
并且,在进行旋转处理后就无需继续往上更新平衡因子了,因为旋转后树的高度变为插入之前了,即树的高度没有发生变化,也就不会影响其父结点的平衡因子了。
加上更新平衡因子后的插入完整代码:
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;cur->_parent = parent;}else{parent->_left = cur;cur->_parent = parent;}// 1、更新平衡因子while (parent) // parent为空,也就更新到根{// 新增在右,parent->bf++;// 新增在左,parent->bf--;if (cur == parent->_left){parent->_bf--;}else{parent->_bf++;}// 是否继续更新依据:子树的高度是否变化// 1、parent->_bf == 0说明之前parent->_bf是 1 或者 -1// 说明之前parent一边高一边低,这次插入填上矮的那边,parent所在子树高度不变,不需要继续往上更新// 2、parent->_bf == 1 或 -1 说明之前是parent->_bf == 0,两边一样高,现在插入一边更高了,// parent所在子树高度变了,继续往上更新// 3、parent->_bf == 2 或 -2,说明之前parent->_bf == 1 或者 -1,现在插入严重不平衡,违反规则// 就地处理--旋转// 旋转:// 1、让这颗子树左右高度不超过1// 2、旋转过程中继续保持他是搜索树// 3、更新调整孩子节点的平衡因子// 4、让这颗子树的高度跟插入前保持一致if (parent->_bf == 0){break;}else if (parent->_bf == 1 || parent->_bf == -1){cur = parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2){// 旋转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){RotateLR(parent);}else if (parent->_bf == 2 && cur->_bf == -1){RotateRL(parent);}else{assert(false);}break;}else{assert(false);}}return true;}
七、AVL树相关的一些测试
AVL 树是在二叉搜索树的基础上加入了平衡的限制,因此要验证 AVL 树,可以分为两步:
- 验证其为二叉搜索树。如果中序遍历可以得到一个有序的序列,就说明为二叉搜索树。
- 验证其为平衡树。每个结点左右子树高度差的绝对值不超过 1 。其次检查结点的平衡因子是否计算正确。
public://中序void Inorder(){return _Inorder(_root);}//检测平衡因子bool IsBlance(){return _IsBalance(_root);}
private://中序void _Inorder(Node* root){if (root == nullptr){return;}_Inorder(root->_left);cout << root->_kv.first << ' ';_Inorder(root->_right);return;}//求树的高度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(Node* root){if (root == nullptr){return true;}int leftHeight = Height(root->_left);int rightHeight = Height(root->_right);if (rightHeight - leftHeight != root->_bf){cout << "平衡因子异常:" << root->_bf << endl;return false;}return abs(leftHeight - rightHeight) < 2&& _IsBalance(root->_left)&& _IsBalance(root->_right);}
/*test.c*/
int main()
{int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };wcy::AVLTree<int, int> t;for (auto e : a){t.Insert(make_pair(e, e));t.Inorder();cout << endl;if (t.IsBlance()){cout << "成功插入:" << e << endl;}else{cout << e << "插入失败" << endl;}}return 0;
}
八、完整代码
AVL.h
#include <assert.h>
#include <time.h>
#include<algorithm>
#include<iostream>
using namespace std;
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; // balance factorAVLTreeNode(const pair<K, V>& kv):_kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _bf(0){}
};template<class K, class V>
struct 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->_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;cur->_parent = parent;}else{parent->_left = cur;cur->_parent = parent;}// 1、更新平衡因子while (parent) // parent为空,也就更新到根{// 新增在右,parent->bf++;// 新增在左,parent->bf--;if (cur == parent->_left){parent->_bf--;}else{parent->_bf++;}// 是否继续更新依据:子树的高度是否变化// 1、parent->_bf == 0说明之前parent->_bf是 1 或者 -1// 说明之前parent一边高一边低,这次插入填上矮的那边,parent所在子树高度不变,不需要继续往上更新// 2、parent->_bf == 1 或 -1 说明之前是parent->_bf == 0,两边一样高,现在插入一边更高了,// parent所在子树高度变了,继续往上更新// 3、parent->_bf == 2 或 -2,说明之前parent->_bf == 1 或者 -1,现在插入严重不平衡,违反规则// 就地处理--旋转// 旋转:// 1、让这颗子树左右高度不超过1// 2、旋转过程中继续保持他是搜索树// 3、更新调整孩子节点的平衡因子// 4、让这颗子树的高度跟插入前保持一致if (parent->_bf == 0){break;}else if (parent->_bf == 1 || parent->_bf == -1){cur = parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2){// 旋转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){RotateLR(parent);}else if (parent->_bf == 2 && cur->_bf == -1){RotateRL(parent);}else{assert(false);}break;}else{assert(false);}}return true;}
/*----------------------------------------------旋转------------------------------------------------*/void RotateL(Node* parent){//俩个孩子 三个双亲 好好品这句话//1.更新孩子节点(向下分配)Node* subR = parent->_right;Node* subRL = subR->_left;Node* ppNode=parent->_parent;//为了避免是局部子树parent->_right=subRL;subR->_left=parent;//2.更新双亲节点(往回分配)if(subRL!=nullptr)//往回分配的小插曲 空节点没有双亲节点{subRL->_parent=parent;}parent->_parent=subR;//3.判断是否为局部子树 需要在parent被修改之前 写出ppNodeif(ppNode==nullptr){//说明是完整的树_root=subR;_root->_parent=nullptr;}else{if(ppNode->_left==parent)//虽然parent被旋转了 但是上层的ppNode还是连着parent节点 {ppNode->_left=subR;}else{ppNode->_right=subR;}subR->_parent = ppNode;}//4.更新平衡因子parent->_bf=subR->_bf=0;//更新平衡因子}void RotateR(Node* parent){//俩个孩子 三个双亲 好好品这句话//1.更新孩子节点(向下分配)Node* subL=parent->_left;Node* subLR=subL->_right;Node* ppNode=parent->_parent;//为了避免是局部子树parent->_left=subLR;subL->_right=parent;//2.更新双亲节点(往回分配)if(subLR!=nullptr)//往回分配的小插曲 空节点没有双亲节点{subLR->_parent=parent;}parent->_parent=subL;//3.判断是否为局部子树 需要在parent被修改之前 写出ppNodeif(ppNode==nullptr){//说明是完整的树_root=subL;_root->_parent=nullptr;}else{if(ppNode->_left==parent)//虽然parent被旋转了 但是上层的ppNode还是连着parent节点 哈哈 当年的自己提出这个问题 现在的我自己解决了{ppNode->_left=subL;}else{ppNode->_right=subL;}subL->_parent = ppNode;}//4.更新平衡因子parent->_bf=subL->_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==-1)//subLR左子树插入{subL->_bf=0;subLR->_bf=0;parent->_bf=1;}if(bf==1)//subLR右子树插入{subL->_bf=-1;subLR->_bf=0;parent->_bf=0;}if(bf==0)//本身就是新节点被插入{subL->_bf=0;subLR->_bf=0;parent->_bf=0;}}void RotateRL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;RotateR(parent->_right);RotateL(parent);if (bf == 1){subR->_bf = 0;subRL->_bf = 0;parent->_bf = -1;}else if (bf == -1){subR->_bf = 1;subRL->_bf = 0;parent->_bf = 0;}else if (bf == 0){subR->_bf = 0;subRL->_bf = 0;parent->_bf = 0;}else{assert(false);}}void Inorder(){_Inorder(_root);}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 lh = Height(root->_left);int rh = Height(root->_right);return lh > rh ? lh + 1 : rh + 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 <<root->_kv.first<<"平衡因子异常" << endl;return false;}return abs(rightHeight - leftHeight) < 2&& IsBalance(root->_left)&& IsBalance(root->_right);}private:Node* _root = nullptr;
};//void TestAVLTree()
//{
// //int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
// //int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
// //int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
// AVLTree<int, int> t;
// for (auto e : a)
// {
// t.Insert(make_pair(e, e));
// }
//
// t.Inorder();
//
// cout << t.IsBalance() << endl;
//}void TestAVLTree()
{srand(time(0));const size_t N = 10000;AVLTree<int, int> t;for (size_t i = 0; i < N; ++i){size_t x = rand();t.Insert(make_pair(x, x));//cout << t.IsBalance() << endl;}//t.Inorder();cout << t.IsBalance() << endl;
}
AVL.cpp
#include"AVL.h"
#include<iostream>
#include<algorithm>
using namespace std;
/*test.c*/
int main()
{int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };AVLTree<int, int> t;for (auto e : a){t.Insert(make_pair(e, e));t.Inorder();cout << endl;if(t.IsBalance()){cout << "成功插入:" << e << endl;}else{cout << e << "插入失败" << endl;}}return 0;
}
九、难点总结
旋转的四个原则
- 子树左右高度不超过1
- 旋转过程中必须保持它是搜索树
- 更新调整孩子节点的平衡因子
- 降低这颗子树的高度
在进行旋转处理后就无需继续往上更新平衡因子了
因为旋转后树的高度变为插入之前了,即树的高度没有发生变化,也就不会影响其父结点的平衡因子了
旋转口诀是插入较高… 不要把这个较高给忽略掉
写代码的时候,头脑要清楚,不要忘了回溯parent的值(向上分配)
插入代码更新平衡因子的条件是while(parent)
对于单旋,利用俩孩子三双亲原则;对于双旋,难点在于平衡因子更新,得画出插入前和旋转后的图分析平衡因子(三种情况)
双旋得提前记录subLR或者subRL的bf(旋转完了都找不到了)
单旋得提前记录parent的_parent,也就是代码中的ppNode
单旋的时候,解决局部子树问题时,总会忘记向上分配
希望给大家带来帮助!!
相关文章:

【C++】AVL树(动图详解)
文章目录 一、前言二、AVL树的概念(引入bf)三、AVL节点树的定义四、AVL树的基本框架五、AVL树的旋转5.1左单旋(新节点插入较高右子树的右侧---右右:左单旋)例一(h0)例二(h1ÿ…...

「Verilog学习笔记」用3-8译码器实现全减器
专栏前言 本专栏的内容主要是记录本人学习Verilog过程中的一些知识点,刷题网站用的是牛客网 分析 首先列出3-8译码器和全减器的真值表 全减器真值表如下 3-8译码器真值表如下 timescale 1ns/1nsmodule decoder_38(input E ,input A0 …...

rocketmq: MQClientException: No route info of this topic
可能是broker没连接到(或配置)name server有关...

【Vue全家桶 合集 关注收藏】
【Vue全家桶】全面了解学习并实践总结Vue必备知识点 写在前面 🤗 这里是SuperYi Vue全家桶合集站! 🌻 人海茫茫,感谢这一秒你看到这里。希望我的文章对你的有所帮助! 🌟 愿你在未来的日子,保持…...

react+video.js h5自定义视频暂停图标
目录 参考网址 效果图,暂停时显示暂停图标,播放时隐藏暂停图标 代码说明,代码传入url后,可直接复制使用 VideoPausedIcon.ts 组件 VideoCom.tsx Video.module.less 参考网址 在Video.js播放器中定制自己的组件 - acgtofe 效…...

CentOS和Ubuntu中防火墙相关命令
CentOS和Ubuntu中防火墙相关命令 1、CentOS7中防火墙相关命令2、Ubuntu中防火墙相关命令 1、CentOS7中防火墙相关命令 在CentOS 7中,与防火墙相关的命令主要包括firewalld命令。以下是一些常用的firewalld命令: 查看firewalld服务状态: syst…...

学习笔记5——对象、直接内存、执行引擎,string
学习笔记系列开头惯例发布一些寻亲消息 链接:https://baobeihuijia.com/bbhj/contents/3/192486.html 创建对象的步骤 对象对应的类是否被加载,链接(链接到真实的内存地址),初始化(类初始化)…...

【node】如何在打包前进行请求等操作npm run build
举例,在运行 npm run build 之前将路由表传递给后端,可以采取以下步骤: 创建一个脚本文件,例如 generateRoutes.js,用于生成路由表文件。 在该脚本文件中,导入路由配置文件和后端要接收路由表的接口。 使…...

鸿蒙4.0真机调试踩坑
传言鸿蒙next版本将不再兼容Android,所以领导安排做下鸿蒙开发的调研工作。 鸿蒙开发指南其实已经非常的友好了。但是鸿蒙开发本身还是有些坑要踩,这篇文章主要讲了鸿蒙真机调试问题。 目前手上的真机为华为 nova6,处理器为麒麟990.鸿蒙系统…...

中文撰稿好用软件推荐TexPage(似于Overleaf)
由于本人用惯了overleaf所以找到了一个与他功相似的也同样是利用tex写文章。唯一的区别可能也就是overleaf只支持英文,而TexPage中英文都支持。关键是不花钱,好用好用好用,用起来! 平台网址:https://www.texpage.com/…...

AD教程 (十七)3D模型的创建和导入
AD教程 (十七)3D模型的创建和导入 对于设计者来讲,现在3DPCB比较流行,3DPCB,除了美观之外,做3D的最终的一个目的,是为了去核对结构,就是我们去做了这么一个PCB之后,如果说…...

企业微信获取第三方应用凭证
上一篇介绍了如何配置通用开发参数及通过url回调验证, 本篇将通过服务商后台配置关联小程序应用配置和获取第三方凭证及如何配置企业可信IP。 当然上篇配置的回调设置也不会白费,在下方的指令和数据回调会用到。 第三方应用开发流程 官方企业微信第三方…...

增删改查mysql
查询 -- 查询表结果-- 查看 当前数据库下的表show tables;-- 查看指定的表desc tb_emp; -- td_emp 是表名-- 查看 数据库的见表语句show create table tb_emp; 修改 -- 修改表结构 -- 修改 为表 tb_emp 添加字段 qq varchar(11) alter table tb_emp add qq varchar(11) …...

【vue】下载导出excel
下载导出excel 首先使用的tdesign框架,要导出后端返回的数据流excel 遇见的问题 下载的文件,里边的内容是undefined 观察报错 一看就知道并不是后端的报错,后端不可能是undefined 在强烈的好奇心驱动下,看了下接口࿰…...

c#正则表达式
using System.Text.RegularExpressions; namespace demo1 {/// <summary>/// 正则表达式(Regular Expression)是一种文本模式,包括普通字符(例如,a~z的字母)和特殊字符(称为“…...

C#密封类和密封成员
密封类和密封成员需要使用 sealed 修饰符,他可以防止当前类被继承或者防止派生类在继承的过程中重写某个方法。 与abstract抽象修饰符类似,sealed 修饰符不仅可用来修饰class,同样也可以修饰类成员。如果sealed关键词用在class上,…...

三、Eureka注册中心
目录 一、作用及调用方式 二、搭建eureka注册中心 三、注册user-service和order-service 四、新增实例 五、服务拉取 六、总结 一、作用及调用方式 在服务提供者启动时,它会向eureka注册中心提供自己的信息,并每30秒进行一次刷新eureka注册中心保存…...

java线程池动态调节功能实现
java线程池动态调节功能实现 功能背景ThreadPoolExecutor配置自定义可变容LinkedBlockingQueuecontroller接口测试结果 功能背景 由于线程池的参数配置是一个比较难准确配置好, 如果需要进行配置修改, 就会对配置进行修改,再进行部署,影响效率, 或者应用场景的变化,导致固定的…...

KT148A语音芯片使用串口uart本控制的完整说明_包含硬件和指令举例
一、功能简介 KT148A肯定是支持串口的,有客户反馈使用一线还是不方便,比如一些大型的系统不适合有延时的操作,所以更加倾向于使用uart控制,这里我们也给出解决方案 延伸出来另外一个版本,KT158A 注意次版本芯片还是…...

kubectl 本地远程链接k8s多个集群,远程管控多集群,查看日志 部署服务(windows版)
文章目录 一、前言二、windows上安装kubectl和mobaxterm2.1 准备安装包2.2 安装kubectl2.3 链接k8s集群2.4 查看某一个pod的容器日志2.5 切换context 上下文配置,实现在多个k8s集群间动态切换 一、前言 现如今是一个万物皆上云 的时代,各种云层出不穷&am…...

wireshark打开tcpdump抓的包 vwr: Invalid data length runs past the end of the record
tcpdump -i any -n -s0 > t.pcap 使用此命令在Debian系统上抓包,下载到PC,用wireshark打开时报错: 后来发现写入文件时使用 -w 是没问题的,原因还不清楚。 tcpdump -i any -n -s0 -w t.pcap...

Python爬虫教程:从入门到实战
更多Python学习内容:ipengtao.com 大家好,我是涛哥,今天为大家分享 Python爬虫教程:从入门到实战,文章3800字,阅读大约15分钟,大家enjoy~~ 网络上的信息浩如烟海,而爬虫(…...

C++实现高频设计模式
面试能说出这几种常用的设计模式即可 1.策略模式 1.1 业务场景 大数据系统把文件推送过来,根据不同类型采取不同的解析方式。多数的小伙伴就会写出以下的代码: if(type"A"){//按照A格式解析 }else if(type"B"){//按照B格式解析 …...

opencv(2): 视频采集和录制
视频采集 相关API VideoCapture()cap.read(): 返回两个值,第一个参数,如果读到frame,返回 True. 第二个参数为相应的图像帧。cap.release() VideoCapture cv2.VideoCapture(0) 0 表示自动检测,如果在笔记本上运行&…...

SpringBoot+EasyExcel设置excel样式
方式一:使用注解方式设置样式 模板可通过HeadFontStyle、HeadStyle、ContentFontStyle、ContentStyle、HeadRowHeight ContentRowHeight等注解设置excel单元格样式; //字体样式及字体大小 HeadFontStyle(fontName "宋体",fontHeightInPoints…...

自定义View之Measure(二)
measure 用来测量 View 的宽和高,它的流程分为 View 的 measure 流程和 ViewGroup 的measure流程,只不过ViewGroup的measure流程除了要完成自己的测量,还要遍历地调用子元素的measure()方法。 上一回说到performMeasur…...

SQL注入学习--GTFHub(布尔盲注+时间盲注+MySQL结构)
目录 布尔盲注 手工注入 笔记 Boolean注入 # 使用脚本注入 sqlmap注入 使用Burpsuite进行半自动注入 时间盲注 手工注入 使用脚本注入 sqlmap注入 使用Burpsuite进行半自动注入 MySQL结构 手工注入 sqlmap注入 笔记 union 联合注入,手工注入的一般步骤 …...

Kubernetes学习-概念2
参考:关于 cgroup v2 | Kubernetes 关于 cgroup v2 在 Linux 上,控制组约束分配给进程的资源。 kubelet 和底层容器运行时都需要对接 cgroup 来强制执行为 Pod 和容器管理资源, 这包括为容器化工作负载配置 CPU/内存请求和限制。 Linux 中…...

StyleGAN:彻底改变生成对抗网络的艺术
一、介绍 多年来,人工智能领域取得了显着的进步,其中最令人兴奋的领域之一是生成模型的发展。这些模型旨在生成与人类创作没有区别的内容,例如图像和文本。其中,StyleGAN(即风格生成对抗网络)因其创建高度逼…...

黑马程序员微服务第四天课程 分布式搜索引擎1
分布式搜索引擎01 – elasticsearch基础 0.学习目标 1.初识elasticsearch 1.1.了解ES 1.1.1.elasticsearch的作用 elasticsearch是一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容 例如: …...