DS进阶:AVL树

DS进阶:AVL树
 Hello大家好! 很高兴与大家见面! 给生活添点快乐,开始今天的编程之路。



我的博客:<但愿.

我的专栏:C语言题目精讲算法与数据结构C++

欢迎点赞,关注

目录

一 AVL树概念

     1.1AVL树的引入

     1.2AVL树的概念

二 AVL树的实现

      2.1AVL树的节点定义

      2.2AVL树的插入

      2.3AVL树的插入导致不平衡的解决方法旋转(4种最重要)

             2.3.1旋转的原则

             2.3.2单旋

                          2.3.2.1右单旋

                                      2.3.2.1.1右单旋的整体思路

                                      2.3.2.1.2右单旋的代码完善

                           2.3.2.2左单旋

                                       2.3.2.2.1左单旋的整体思路

                                       2.3.2.2.2左单旋的代码完善

               2.3.3双旋

                            2.3.3.1左右双旋

                                       2.3.3.1.1左右双旋的整体思路

                                       2.3.3.1.2左右双旋的代码完善

                            2.3.3.2右左双旋

                                       2.3.3.2.1右左双旋的整体思路

                                       2.3.3.2.2左右双旋的代码完善

     2.4AVL树插入的整体代码

     2.5 AVL树的查找

     2.6 AVL树检测(了解)

                  2.6.1AVL树求树高度的方法

                  2.6.2AVL树的平衡检测(检查平衡因子)

                  2.6.3 AVL树检测(了解)

                               2.6.3.1步骤1:检查是否为搜索二叉树

                               2.6.3.2步骤2:检查是否平衡(平衡因子)

   2.7AVL树的整体代码

   2.8AVL树的性能

一    AVL树概念

1.1AVL树的引入

二叉搜索树(BST)虽然在一定程度上提高了查找的效率,但如果数据有序或有序搜索二叉树将退化为单支树时,此时的效率就相当于在遍历所有节点在查找(时间复杂度从logN变成N),效率低下。因此,为解决这个问题两位前苏联的科学家G. M. Adelson-Velsky和E. M. Landis,他们发明了一种解决方法即AVL树

1.2AVL树的概念

•AVL树是最先发明的⾃平衡⼆叉搜索树,AVL是⼀颗空树,或者具备下列性质的⼆叉搜索树:它的左右⼦树都是AVL树,且左右⼦树的⾼度差(平衡因子)的绝对值不超过1。AVL树是⼀颗⾼度平衡搜索⼆叉树,通过控制⾼度差去控制平衡。•平衡因子左右⼦树的⾼度差,每个结点都有⼀个平衡因⼦,任何结点的平衡因⼦等于右⼦树的⾼度减去左⼦树的⾼度(这里我们采用这个,其实这个东西不是死的,求的方法变了后续自己会变通即可),也就是说任何结点的平衡因⼦等于0/1/-1, 注意AVL树并不是必须要平衡因⼦,但是有了平衡因⼦可以更⽅便我们去进⾏观察和控制树是否平衡(这里只是采用这种形式)。AVL树整体结点数量和分布和完全⼆叉树类似,⾼度可以控制在 ,那么增删查改的效率也可以控制在 ,相⽐⼆叉搜索树有了本质的提升

这里有个问题为什么AVL树是⾼度平衡搜索⼆叉树,要求⾼度差不超过1,⽽不是⾼度差是0呢?0不是更好的平衡吗? 其实一些情况下⾼度差是不可能等于0的例如当节点个数是2个和4个的时候其最优情况就是1。

二  AVL树的实现

首先还是在这里说明一下,这里我采用平衡因子的版本来模拟实现AVL树(AVL树实现方法有多种),并且采用平衡因⼦等于右⼦树的⾼度减去左⼦树的⾼度(每个人习惯不同只要保证平衡因⼦是两颗子树的高度差即可,自己定义的东西改变了对应的地方会变通即可)

2.1AVL树的节点定义

注意AVL树是一个<key,value>类型的平衡搜索二叉树(即节点的数据类似是pair类型),由于AVL每插入或者删除一个节点,都会影响节点高度,此时就有可能影响其父节点左右子树的高度差(平衡因子)所以大多数情况下都需要向上调整平衡因子,所以这里实现采用三叉链的形式(_left、_right、_parent【父节点,方便调节平衡因子】),并且引入平衡因子,目的就是通过平衡因子来判断该树是否平衡(前面讲过AVL树的何结点的平衡因⼦等于0/1/-1,当AVL中的某个节点的平衡因子的绝对值>1时【这里是2或者-2,因为此时就要开始调整,所以平衡因子绝对值不可能大于2】),当该树不平衡就要进行旋转调整

template<class K,class V> struct AVLTreeNode { //注意这里每个节点的数据类型是一个pair类型 pair<K, V> _kv; //注意这里每个节点,包含其父节点,方便后续更新平衡因子 AVLTreeNode<K, V>* _left; AVLTreeNode<K, V>* _right; AVLTreeNode<K, V>* _parent; int _bf;//平衡因子,注意这里的类型不是size_t类型是因为平衡因子可能<0; AVLTreeNode(const pair<K,V>& kv) :_kv(kv) ,_left(nullptr) ,_right(nullptr) ,_parent(nullptr) ,_bf(0) { } };

2.2AVL树的插入

AVL树其基础还是一颗搜索二叉树(BST),所以首先AVL树的插入本质也是BST的逻辑,只不过这里在每个节点增加了平衡因子来控制平衡,所以我们在按照BST的逻辑插入节点之后还要向上调整平衡因子(由于插入一个节点只会影响其父节点的平衡因子[祖先],所以这里只要向上调整即可)

【AVL树的插入整体思路】

1. 插⼊⼀个值按⼆叉搜索树规则进⾏插⼊(AVL树的基础还是一颗搜索二叉树),需要注意的是这里每个节点新增了_paremt(父节点),所以后面注意更新父节点即可。 2. 新增结点以后,只会影响祖先结点的⾼度,也就是可能会影响部分祖先结点的平衡因⼦,所以更新从新增结点->根结点路径上的平衡因⼦,实际中最坏情况下要更新到根,有些情况更新到中间就可以停⽌了,具体情况我们下⾯再详细分析。3. 更新平衡因⼦过程中没有出现问题,则插⼊结束,更新平衡因⼦过程中出现不平衡,对不平衡⼦树旋转,旋转后本质调平衡的同时,本质降低了⼦树的⾼度,不会再影响上⼀层,所以插⼊结束。

更新平衡因子的方法(逻辑):

我们这里采用平衡因⼦等于右⼦树的⾼度减去左⼦树的⾼度。

所以这里可以定义两个节点cur(插入节点) 和 parent(插入节点的父节点),在判断cur是parent的左孩子节点还是右孩子节点。根据平衡因子的定义:如果cur是parent的右孩子节点,那么parent的平衡因子(_bf)就++;如果cur是parent的左孩子节点,那么parent的平衡因子(_bf)就--。



【更新原则】
•平衡因⼦ = 右⼦树⾼度-左⼦树⾼度•只有⼦树⾼度变化才会影响当前结点平衡因⼦。•插⼊结点,会增加⾼度,所以新增结点在parent的右⼦树,parent的平衡因⼦++,新增结点在parent的左⼦树,parent平衡因⼦--•parent所在⼦树的⾼度是否变化决定了是否会继续往上更新

   而现在需要思考的问题就是,什么情况下需要去进行不断向上更新平衡因子、旋转调整平衡、结束。因此我们需要分情况讨论

注意是否要向上更新,要看parent树的高度是否发生变化,因为新增一个节点会导致子树的高度增大,但是不一定会导致父树的高度发生变化(例如将一个节点作为根节点,他又右孩子,此时为它插入一个左孩子,虽然此时左子树的高度发生变化,但是父树的高度未发生变化。



1、如果调整过后,parent平衡因子的绝对值为1,说明调整之前的parent平衡因子为0,即左右高度是相等的,此时变成1说明树的高度变了,因此需要继续向上调整。



2、如果调整过后,parent平衡因子的绝对值为2,说明调整之前的parent平衡因子绝对值为1,说明子树已经严重不平衡并且破坏了AVL树的规则,此时我们就要进行旋转。旋转过后就可以结束循环了



3、如果调整过后,panert平衡因子的绝对值是0,说明调整之前的parent平衡因子的绝对值是1,这说明之前的高度是不平衡的,插入之后反而变得更平衡了,此时就可以结束循环了。

       根据上面的这些规则,我们现将整个架子先搭建起来,然后再去研究当bf的绝对值为2的时候应该怎么去进行旋转。

 bool Insert(const pair<K, V>& kv) { //1.1搜先按搜索二叉树的规则插入数据 //如果为空树,新节点就是根 if (_root == nullptr) { _root = new Node(kv); return true; } //如果不为空树,找到插入节点的位置 Node* parent = nullptr; Node* cur = _root; while (cur) { if (cur->_kv.first < kv.first) //如果kv比cur大,到右子树去 { parent = cur; cur = cur->_right; } else if (cur->_kv.first > kv.first)//如果kv比cur小,到左子树去 { parent = cur; cur = cur->_left; } else//相等 { return false; } } //插入节点 cur = new Node(kv); //判断插入的是parent的左孩子还是右孩子 if (parent->_kv.first < kv.first) { parent->_right = cur; } else { parent->_left = cur; } cur->_parent = parent;//注意这里一定要对应改变父节点 //因为父亲节点给我们带来了便利(更好更新平衡因子)就一定要对应改变父节点 //更新平衡因子-判断是结束,还是继续向上调整,还是要旋转调节平衡 while (parent) { if (cur == parent->_right) { parent->_bf++; } else if (cur == parent->_left) { parent->_bf--; } //判断是结束,还是继续向上调整,还是要旋转调节平衡 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) { //旋转调节平衡,四种情况 //1左边高右单旋(单纯的一边高,看平衡因子要知道(同号) 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); } break; } else { assert(false); } } return true; }

2.3AVL树的插入导致不平衡的解决方法旋转(4种最重要)

2.3.1旋转的原则

1. 保持搜索树的规则(即不要旋转完之后都不是搜索二叉树了,此时就很麻烦) 2. 让旋转的树从不满⾜变平衡,其次降低旋转树的⾼度旋转总共分为四种,左单旋/右单旋/左右双旋/右左双旋。

2.3.2单旋

2.3.2.1右单旋

左边高(左子树的高度大于右子树的高度,注意这里是单纯的一边高【判断方法其实很简单,parent和subL的平衡因子同号】),本来就左子树的高度比右子树的高度大一,当新插入的节点在较高的左子树的左侧,此时根节点(parent)的平衡因子等于-2,subL的平衡因子==-1此时就要进行右单旋(因为是左边高,所以要把根节点往右下拉这里把这种方式叫右单旋)。

2.3.2.1.1右单旋的整体思路
•上图10为根的树,有a/b/c抽象为三棵⾼度为h的⼦树(h>=0),a/b/c均符合AVL树的要求。10(parent)可能是整棵树的根,也可能是⼀个整棵树中局部的⼦树的根。这⾥a/b/c是⾼度为h的⼦树,是⼀种概括抽象表⽰(这里用概括抽象表⽰的原因是自己后面可以假设h是1/2/3/4等等分别讨论,随着h的增大情况变得很多倍数增大,虽然它们情况不一样,但是思路是一样的,所以这里用这种方法表示),他代表了所有右单旋的场景,实际右单旋形态有很多种。•在a⼦树中插⼊⼀个新结点,导致a⼦树的⾼度从h变成h+1,不断向上更新平衡因⼦,导致10(parent)的平衡因⼦从-1变成-2,10(parent)为根的树左右⾼度差超过1,违反平衡规则。10(parent)为根的树左边太⾼了,需要往右边旋转,控制两棵树的平衡。•旋转核⼼步骤,因为5(subL) < b⼦树的值 < 10(parent),将b变成10(parent)的左⼦树,10(parent)变成5(subL)的右⼦树,5(subL)变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的⾼度恢复到了插⼊之前的h+2,符合旋转原则。如果插⼊之前10(parent)整棵树的⼀个局部⼦树,旋转后不会再影响上⼀层,插⼊结束了。•旋转过程中遇到的问题:1 这里不仅要动左右孩子,还要动其父节点(很重要,因为parent父节点既然给我们带来了便利【平衡因子的更新】也就意味着这里每动一个节点就要动其父节点,此时动其父节点时要注意一棵树的根节点无父节点,所以这里要判断是否为树的根节点(分类讨论)。2 ac都不可能为空,但是b可能为空,所以后续将b连解到parent(父节点)左节点时改变b的parent时一定要判断是否为空(为空不能解引用)。3 注意parent,subL,subLR的平衡因子其中哪几个节点的平衡因子要变(因为abc子树我们并没有动所以这里不用管它们的平衡因子)。

【图解】

2.3.2.1.2右单旋的代码完善
//1右单旋-单纯的一边高只进行一种旋转即可调节平衡 void RotateR(Node* parent) { Node* subL = parent->_left; Node* subLR = subL->_right; parent->_left = subLR;//将b(subLR)变成parent的左节点 if(subLR)//同步更新(bsubLR)的父节点,因为b(subLR)可能为空,由于这里要进行解引用所以要判断是否为空 subLR->_parent = parent; //由于这里更新的是一颗子树所以旋转前要记录parent的前驱节点(父节点)ppnode Node* ppnode = parent->_parent; subL->_right = parent;//将parend变成subL的右节点 parent->_parent = subL;//subL变成树的新根节点 //由于这里将subL变成树的新根节点,前面我们还没更新subL的父节点 //由于根节点的父节点为空,此时就要特殊处理 //所以这里要分类讨论,当parent(原根节点)是真正的根节点是,subL的父节点为空 //当parent(原根节点)只是一颗子树的根节点时,此时要判断parent时其前驱节点(ppnode)的左右节点中的哪一个方便将subL和其前驱节点连起来 if (parent == _root)//当parent(原根节点)是真正的根节点是,subL的父节点为空 { _root = subL; subL->_parent = nullptr; } else//当parent(原根节点)只是一颗子树的根节点时 { if (ppnode->_left == parent) { ppnode->_left = subL; } else { ppnode->_right = subL; } subL->_parent = ppnode; } //更新平衡因子 subL->_bf = parent->_bf = 0; }

2.3.2.2左单旋

右边高(右子树的高度大于左子树的高度,注意这里是单纯的一边高【判断方法其实很简单,parent和subR的平衡因子同号】),本来就右子树的高度比左子树的高度大一,当新插入的节点在较高的右子树的右侧,此时根节点(parent)的平衡因子等于2,subR的平衡因子==1此时就要进行左单旋(因为是右边高,所以要把根节点往左下拉这里把这种方式叫右单旋)。

2.3.2.2.1左单旋的整体思路

其实左单旋的整体思路和右单旋的思路差不多只是要改变节点指向的几个节点不同。

•上图10为根的树,有a/b/c抽象为三棵⾼度为h的⼦树(h>=0),a/b/c均符合AVL树的要求。10可能是整棵树的根,也可能是⼀个整棵树中局部的⼦树的根。这⾥a/b/c是⾼度为h的⼦树,是⼀种概括抽象表⽰(这里用概括抽象表⽰的原因是自己后面可以假设h是1/2/3/4等等分别讨论,随着h的增大情况变得很多倍数增大,虽然它们情况不一样,但是思路是一样的,所以这里用这种方法表示),他代表了所有右单旋的场景,实际右单旋形态有很多种。•在a⼦树中插⼊⼀个新结点,导致a⼦树的⾼度从h变成h+1,不断向上更新平衡因⼦,导致10(parent)的平衡因⼦从1变成2,10(parent)为根的树左右⾼度差超过1,违反平衡规则。10(parent)为根的树右边太⾼了,需要往左边旋转,控制两棵树的平衡。•旋转核⼼步骤,因为10(parent) < b⼦树的值 < 15(subR),将b变成10(parent)的右⼦树,10(parent)变成15(subR)的左⼦树,15(subR)变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的⾼度恢复到了插⼊之前的h+2,符合旋转原则。如果插⼊之前10(parent)整棵树的⼀个局部⼦树,旋转后不会再影响上⼀层,插⼊结束了。•旋转过程中遇到的问题:1 这里不仅要动左右孩子,还要动其父节点(很重要,因为parent父节点既然给我们带来了便利【平衡因子的更新】也就意味着这里每动一个节点就要动其父节点,此时动其父节点时要注意一棵树的根节点无父节点,所以这里要判断是否为树的根节点(分类讨论)。2 ac都不可能为空,但是b可能为空,所以后续将b连解到parent(父节点)右节点时改变b的parent时一定要判断是否为空(为空不能解引用)。3 注意parent,subR,subRL的平衡因子其中哪几个节点的平衡因子要变(因为abc子树我们并没有动所以这里不用管它们的平衡因子)。

【图解】

2.3.2.2.2左单旋的代码完善
//2左单旋-单纯的一边高只进行一种旋转即可调节平衡 void RotateL(Node* parent) { Node* subR = parent->_right; Node* subRL = subR->_left; parent->_right = subRL;////将b(subRL)变成parent的右节点 if (subRL)//同步更新(bsubRL)的父节点,因为b(subRL)可能为空,由于这里要进行解引用所以要判断是否为空 subRL->_parent = parent; Node* ppnode = parent->_parent;//由于这里更新的是一颗子树所以旋转前要记录parent的前驱节点(父节点)ppnode subR->_left = parent;//将parend变成subR的右节点 parent->_parent = subR;////subR变成树的新根节点 //由于这里将subR变成树的新根节点,前面我们还没更新subR的父节点 //由于根节点的父节点为空,此时就要特殊处理 //所以这里要分类讨论,当parent(原根节点)是真正的根节点是,subR的父节点为空 //当parent(原根节点)只是一颗子树的根节点时,此时要判断parent时其前驱节点(ppnode)的左右节点中的哪一个方便将subR和其前驱节点连起来 if (parent == _root)//当parent(原根节点)是真正的根节点是,subR的父节点为空 { _root = subR; subR->_parent = nullptr; } else//当parent(原根节点)只是一颗子树的根节点时 { if (ppnode->_right == parent) { ppnode->_right = subR; } else { ppnode->_left = subR; } subR->_parent = ppnode; } //更新平衡因子 parent->_bf = subR->_bf = 0; }

2.3.3双旋

2.3.3.1左右双旋

本来就左子树的高度比右子树的高度大一,当新插入的节点在较高的左子树的右侧,此时根节点(parent)的平衡因子等于-2,subL的平衡因子等于1【判断方法其实很简单,parent和subL的平衡因子异号】,此时就要进行双旋(因为不是单纯的一边高,所以只进行单旋【记住单旋只是解决单纯的一边高】无法解决这一问题),其实双旋本质是进行两次单旋,进行一次单旋可以转化成单纯的一边高此时在进行一次单旋就可以解决这一问题。那此时进行哪种模式的双旋呢?这里记住双旋的本质(进行两次单旋,进行一次单旋将其转化成单纯的一边高此时在进行一次单旋即可)又因为parent的平衡因子等于-2,subL的平衡因子等于1(即先左边高在右边高),所以应该先对subL进行左单旋才可将其转化为一边高,在对parent进行右单旋即可[所以这里叫左右双旋]。

2.3.3.1.1左右双旋的整体思路

•  由于这里不是单纯的一边高所以只进行单旋时无法实现的,这里的核心思路是先进行一次左单旋将其旋转成一边高,在进行一次右单旋即可。这里要注意的问题和单旋一样,只是更新平衡因子是一个难点。



•前面我们已经讲了两种单旋,这里的难点在于怎么区分不同情况,对于不同情况旋转后节点平衡因子的变化。

    这里我总结一下(下面右图解)我们可以把高度为h的子树分为一个根节点+高度为(h-1)的子树。

    当h==0时此时新增节点就是根节点(subLR)此时旋转后parent,subL,subLR的平衡因子==0

    当h>=1时,插入之前subLR的平衡因子==0,当新增节点在subLR的左子树时subLR的平衡因子==-1;当新增节点在subLR的右子树时subLR的平衡因子==1。这里两种情况旋转后平衡因子的变化不同,所以要区分两种情况(这里通过subLR的平衡因子进行区分)

当subLR的平衡因子==1时:subL的平衡因子==-1,parent、subLR的平衡因子==0。

当subLR的平衡因子==-1时:parent的平衡因子==1,subL、subLR的平衡因子==0。


【图解】

用h(高度)表示的抽象图

2.3.3.1.2左右双旋的代码完善
//2左右双旋-不是单纯的一边高此时只进行一种旋转无法调节平衡 void RotateLR(Node* parent) { //旋转前,先记录相应的节点 Node* subL = parent->_left; Node* subLR = subL->_right; int bf = subLR->_bf;//这里记录subLR的平衡因子,是为了区分插入位置,方便更新平衡因子 RotateL(parent->_left);//先对subL进行左旋 RotateR(parent);//在对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); } }

2.3.3.2右左双旋

2.3.3.2.1右左双旋的整体思路

整体思路和左右双旋差不多这里就不讲解了。

2.3.3.2.2左右双旋的代码完善
//2右左双旋-不是单纯的一边高此时只进行一种旋转无法调节平衡 void RotateRL(Node* parent) { //旋转前,先记录相应的节点 Node* subR = parent->_right; Node* subRL = subR->_left; int bf = subRL->_bf;//这里记录subLR的平衡因子,是为了区分插入位置,方便更新平衡因子 RotateR(parent->_right);//先对subR进行右旋 RotateL(parent);//在对parent进行左旋 //更新平衡因子 if (bf == 0) { subR->_bf = 0; subRL->_bf = 0; parent->_bf = 0; } else 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 { assert(false); } }

2.4AVL树插入的整体代码

class AVLTree { typedef AVLTreeNode<K, V> Node; public: //1插入 bool Insert(const pair<K, V>& kv) { //1.1搜先按搜索二叉树的规则插入数据 //如果为空树,新节点就是根 if (_root == nullptr) { _root = new Node(kv); return true; } //如果不为空树,找到插入节点的位置 Node* parent = nullptr; Node* cur = _root; while (cur) { if (cur->_kv.first < kv.first) //如果kv比cur大,到右子树去 { parent = cur; cur = cur->_right; } else if (cur->_kv.first > kv.first)//如果kv比cur小,到左子树去 { parent = cur; cur = cur->_left; } else//相等 { return false; } } //插入节点 cur = new Node(kv); //判断插入的是parent的左孩子还是右孩子 if (parent->_kv.first < kv.first) { parent->_right = cur; } else { parent->_left = cur; } cur->_parent = parent;//注意这里一定要对应改变父节点 //因为父亲节点给我们带来了便利(更好更新平衡因子)就一定要对应改变父节点 //更新平衡因子-判断是结束,还是继续向上调整,还是要旋转调节平衡 while (parent) { if (cur == parent->_right) { parent->_bf++; } else if (cur == parent->_left) { parent->_bf--; } //判断是结束,还是继续向上调整,还是要旋转调节平衡 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) { //旋转调节平衡,四种情况 //1左边高右单旋(单纯的一边高,看平衡因子要知道(同号) 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); } break; } else { assert(false); } } return true; } private: * parent) { //旋转前,先记录相应的节点 Node* subR = parent->_right; Node* subRL = subR->_left; int bf = subRL->_bf;//这里记录subLR的平衡因子,是为了区分插入位置,方便更新平衡因子 RotateR(parent->_right);//先对subR进行右旋 RotateL(parent);//在对parent进行左旋 //更新平衡因子 if (bf == 0) { subR->_bf = 0; subRL->_bf = 0; parent->_bf = 0; } else 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 { assert(false); } } } 

2.5 AVL树的查找

这里提供搜索二叉树(BST)的查找逻辑即可,只需要注意AVL树节点的数据类型是pair类型,怎么得到对应的key即可。

Node* Find(const pair<K, V>& kv) { Node* cur = _root; while (cur) { if (cur->_kv.first < kv.first) { cur = cur->_right; } else if (cur->_kv.first > kv.first) { cur = cur->_left; } else { return cur; } } return nullptr; }

2.6 AVL树检测(了解)

AVL树平衡检测是通过每个节点的平衡因子进行判断的,即每个节点的左右子树的高度差,所以这里要通过求树高度的方法。

2.6.1AVL树求树高度的方法

首先我们需要封装一个计算树高度的函数(因为要访问私有成员,所以将实现函数定义成私有,调用函数定义从公有)。需要用到后序遍历。

public: int Height() { return _Height(_root); cout << endl; } private: int _Height(Node* root) { if (root == nullptr) return 0; int leftHeight = _Height(root->_left); int rightHeight = _Height(root->_right); //树的高度=左右子树中高度大的+根节点的高度(1) return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1; }

2.6.2AVL树的平衡检测(检查平衡因子)

平衡因子是我们自己去调整的,所以我们最关键的还是去判断我们的左右子树的高度差绝对值(递归)是否<2 ,即这里我们不能进行遍历查找每个结点的平衡因子,因为我们自己也不能确定前面自己给每个结点的平衡因子是否正确。

     需要注意的是,我们必须确保每一个子树都满足AVL树的性质,所以调用完一次isbalance之后,还得去继续判断他的左子树和右子树!!!  这样才能说明他是一颗平衡树!!

public: bool IsBalanceTree() { return _IsBalanceTree(_root); } private: //平衡检测 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); }

2.6.3 AVL树检测(了解)

由于AVL是是一颗高度平衡的搜索二叉树,即要满足是搜索二叉树的前提下,是否是一颗平衡搜索二叉树。所以这里分为两个步骤:检查是否为搜索二叉树、检查是否平衡(平衡因子)

2.6.3.1步骤1:检查是否为搜索二叉树

这个比较容易,我们可以直接通过一个中序遍历,如果打印出来之后得到的是一个有序序列,说明这就是一个二叉搜索树。但是接下来我们还得判断他是否是平衡树。

void Inorder()//中序遍历接口 { _Inorder(_root); cout << endl; } void _Inorder(Node*root) { if (root == nullptr) return; _Inorder(root->_left); cout << root->_kv.first << " "; _Inorder(root->_right); }
2.6.3.2步骤2:检查是否平衡(平衡因子)

上面2.6.2有

2.7AVL树的整体代码

#pragma once #include<assert.h> template<class K,class V> struct AVLTreeNode { //注意这里每个节点的数据类型是一个pair类型 pair<K, V> _kv; //注意这里每个节点,包含其父节点,方便后续更新平衡因子 AVLTreeNode<K, V>* _left; AVLTreeNode<K, V>* _right; AVLTreeNode<K, V>* _parent; int _bf;//平衡因子,注意这里的类型不是size_t类型是因为平衡因子可能<0; 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: //1插入 bool Insert(const pair<K, V>& kv) { //1.1搜先按搜索二叉树的规则插入数据 //如果为空树,新节点就是根 if (_root == nullptr) { _root = new Node(kv); return true; } //如果不为空树,找到插入节点的位置 Node* parent = nullptr; Node* cur = _root; while (cur) { if (cur->_kv.first < kv.first) //如果kv比cur大,到右子树去 { parent = cur; cur = cur->_right; } else if (cur->_kv.first > kv.first)//如果kv比cur小,到左子树去 { parent = cur; cur = cur->_left; } else//相等 { return false; } } //插入节点 cur = new Node(kv); //判断插入的是parent的左孩子还是右孩子 if (parent->_kv.first < kv.first) { parent->_right = cur; } else { parent->_left = cur; } cur->_parent = parent;//注意这里一定要对应改变父节点 //因为父亲节点给我们带来了便利(更好更新平衡因子)就一定要对应改变父节点 //更新平衡因子-判断是结束,还是继续向上调整,还是要旋转调节平衡 while (parent) { if (cur == parent->_right) { parent->_bf++; } else if (cur == parent->_left) { parent->_bf--; } //判断是结束,还是继续向上调整,还是要旋转调节平衡 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) { //旋转调节平衡,四种情况 //1左边高右单旋(单纯的一边高,看平衡因子要知道(同号) 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); } break; } else { assert(false); } } return true; } //查找 Node* Find(const pair<K, V>& kv) { Node* cur = _root; while (cur) { if (cur->_kv.first < kv.first) { cur = cur->_right; } else if (cur->_kv.first > kv.first) { cur = cur->_left; } else { return cur; } } return nullptr; } bool IsBalanceTree() { return _IsBalanceTree(_root); } void InOrder() { _InOrder(_root); cout << endl; } int Height() { return _Height(_root); cout << endl; } int Size() { return _Size(_root); cout << endl; } private: 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; } int _Size(Node* root) { return root == nullptr ? 0 : _Size(root->_left) + _Size(root->_right) + 1; } void _InOrder(Node* root) { if (root == nullptr) { return; } _InOrder(root->_left); cout << root->_kv.first << " "; _InOrder(root->_right); } //平衡检测 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); } //四种旋转 //1右单旋-单纯的一边高只进行一种旋转即可调节平衡 void RotateR(Node* parent) { Node* subL = parent->_left; Node* subLR = subL->_right; parent->_left = subLR;//将b(subLR)变成parent的左节点 if(subLR)//同步更新(bsubLR)的父节点,因为b(subLR)可能为空,由于这里要进行解引用所以要判断是否为空 subLR->_parent = parent; //由于这里更新的是一颗子树所以旋转前要记录parent的前驱节点(父节点)ppnode Node* ppnode = parent->_parent; subL->_right = parent;//将parend变成subL的右节点 parent->_parent = subL;//subL变成树的新根节点 //由于这里将subL变成树的新根节点,前面我们还没更新subL的父节点 //由于根节点的父节点为空,此时就要特殊处理 //所以这里要分类讨论,当parent(原根节点)是真正的根节点是,subL的父节点为空 //当parent(原根节点)只是一颗子树的根节点时,此时要判断parent时其前驱节点(ppnode)的左右节点中的哪一个方便将subL和其前驱节点连起来 if (parent == _root)//当parent(原根节点)是真正的根节点是,subL的父节点为空 { _root = subL; subL->_parent = nullptr; } else//当parent(原根节点)只是一颗子树的根节点时 { if (ppnode->_left == parent) { ppnode->_left = subL; } else { ppnode->_right = subL; } subL->_parent = ppnode; } //更新平衡因子 subL->_bf = parent->_bf = 0; } //2左单旋-单纯的一边高只进行一种旋转即可调节平衡 void RotateL(Node* parent) { Node* subR = parent->_right; Node* subRL = subR->_left; parent->_right = subRL;////将b(subRL)变成parent的右节点 if (subRL)//同步更新(bsubRL)的父节点,因为b(subRL)可能为空,由于这里要进行解引用所以要判断是否为空 subRL->_parent = parent; Node* ppnode = parent->_parent;//由于这里更新的是一颗子树所以旋转前要记录parent的前驱节点(父节点)ppnode subR->_left = parent;//将parend变成subR的右节点 parent->_parent = subR;////subR变成树的新根节点 //由于这里将subR变成树的新根节点,前面我们还没更新subR的父节点 //由于根节点的父节点为空,此时就要特殊处理 //所以这里要分类讨论,当parent(原根节点)是真正的根节点是,subR的父节点为空 //当parent(原根节点)只是一颗子树的根节点时,此时要判断parent时其前驱节点(ppnode)的左右节点中的哪一个方便将subR和其前驱节点连起来 if (parent == _root)//当parent(原根节点)是真正的根节点是,subR的父节点为空 { _root = subR; subR->_parent = nullptr; } else//当parent(原根节点)只是一颗子树的根节点时 { if (ppnode->_right == parent) { ppnode->_right = subR; } else { ppnode->_left = subR; } subR->_parent = ppnode; } //更新平衡因子 parent->_bf = subR->_bf = 0; } //2左右双旋-不是单纯的一边高此时只进行一种旋转无法调节平衡 void RotateLR(Node* parent) { //旋转前,先记录相应的节点 Node* subL = parent->_left; Node* subLR = subL->_right; int bf = subLR->_bf;//这里记录subLR的平衡因子,是为了区分插入位置,方便更新平衡因子 RotateL(parent->_left);//先对subL进行左旋 RotateR(parent);//在对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); } } //2右左双旋-不是单纯的一边高此时只进行一种旋转无法调节平衡 void RotateRL(Node* parent) { //旋转前,先记录相应的节点 Node* subR = parent->_right; Node* subRL = subR->_left; int bf = subRL->_bf;//这里记录subLR的平衡因子,是为了区分插入位置,方便更新平衡因子 RotateR(parent->_right);//先对subR进行右旋 RotateL(parent);//在对parent进行左旋 //更新平衡因子 if (bf == 0) { subR->_bf = 0; subRL->_bf = 0; parent->_bf = 0; } else 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 { assert(false); } } private: Node* _root = nullptr; }; 

2.8AVL树的性能

      AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log2N。但是如果要对AVL树做一些结构修改的操
作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,
有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数
据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

本篇文章就到此结束,欢迎大家订阅我的专栏,欢迎大家指正,希望有所能帮到读者更好了解C++STL知识 ,觉得有帮助的还请三联支持一下~后续会不断更新算法与数据结构相关知识,我们下期再见。


 

Read more

第十六届蓝桥杯省赛(软件类真题)C/C++ 大学A组

第十六届蓝桥杯省赛(软件类真题)C/C++ 大学A组

大纲: A.寻找质数 B:黑白棋 题目&解析&代码 A题 题目解析 本题的目标是枚举质数并计数,直到数到第2025个。由于2025不算太大,第2025个质数大约在17000~18000之间,完全可以在合理时间内通过简单枚举得到。 解题步骤: 从2开始遍历每个整数,判断它是否是质数。 质数判断采用试除法:对于一个数n,只需检查从2到√n的所有整数是否能整除n。若存在能整除的数,则n不是质数;否则是质数。 每找到一个质数,计数器加1。 当计数器达到2025时,输出当前的质数并结束。 优化点: 除了2以外,偶数不可能是质数,因此可以跳过偶数判断(直接步进2)。 在isPrime函数中,可以先处理特殊情况(n<2返回false),然后单独判断偶数,再对奇数进行试除,步进也可以设为2。 C++ 参考代码 以下代码实现了上述算法,并输出第2025个质数。 cpp

By Ne0inhk
C++ 模板进阶:特化、萃取与可变参数模板

C++ 模板进阶:特化、萃取与可变参数模板

C++ 模板进阶:特化、萃取与可变参数模板 💡 学习目标:掌握模板进阶技术的核心用法,理解模板特化的深层应用、类型萃取的实现原理,以及可变参数模板的灵活使用,提升泛型编程的实战能力。 💡 学习重点:模板特化的进阶场景、类型萃取工具的设计与应用、可变参数模板的展开技巧、折叠表达式的使用方法。 一、模板特化进阶:处理复杂类型场景 💡 模板特化不只是针对单一类型的定制,还能处理指针、引用、数组等复杂类型,实现更精细的类型适配逻辑。 1.1 指针类型的模板特化 通用模板默认处理普通类型,我们可以为指针类型单独编写特化版本,实现指针专属的逻辑。 #include<iostream>#include<string>usingnamespace std;// 通用模板:处理普通类型template<typenameT>classTypeProcessor{public:staticvoidprocess(T data){ cout

By Ne0inhk

JSP 文件上传详解

JSP 文件上传详解 引言 在Web开发中,文件上传是一个常见的功能,它允许用户将文件从客户端发送到服务器。Java Server Pages(JSP)作为一种强大的服务器端技术,也支持文件上传功能。本文将详细讲解JSP文件上传的实现过程,包括技术原理、实现步骤和注意事项。 技术原理 JSP文件上传主要依赖于HTTP协议的multipart/form-data编码类型。这种编码类型允许表单中包含文件类型的输入字段。当用户提交表单时,浏览器会将表单数据以文件的形式发送到服务器。 服务器端使用Java的javax.servlet包中的HttpServletRequest和HttpServletResponse对象来接收这些文件。同时,javax.servlet包中的javax.servlet.http模块提供了Part接口,用于访问上传的文件内容。 实现步骤 以下是使用JSP实现文件上传的基本步骤: 1. 创建HTML表单 首先,我们需要创建一个HTML表单,其中包含一个文件类型的输入字段。以下是一个简单的示例: <form action="upload.jsp"

By Ne0inhk

Java + AI 混合编程落地实施方案(保姆级)

Java + AI 混合编程落地实施方案(保姆级) 你希望基于已掌握的Java技术栈,结合AI能力实现混合编程,避免重新学习Python的高成本,这份方案会从技术选型、环境搭建、核心流程、代码实现、部署落地全流程拆解,并用图示清晰呈现整体架构,确保零基础也能落地。 一、核心需求确认 你的核心诉求是:复用Java技术栈,低成本接入AI能力(大模型/机器学习),实现可落地的Java+AI混合编程,核心目标是“Java为主、AI为辅”,不依赖Python开发AI模块,而是通过标准化接口调用成熟AI服务。 二、整体架构设计(图示) HTTP/GRPC 本地调用 数据预处理 模型推理 API调用 私有化推理 本地模型训练/推理 TensorFlow模型调用 Java应用层 AI能力网关层 Java原生AI库 开源大模型服务 (如LLaMA/通义千问Java部署版) 云厂商AI API (阿里云/

By Ne0inhk