AVL树超详解上
前言
学习过了二叉树以及二叉搜索树后(不了解二叉搜索树的朋友可以先看看这篇博客,二叉搜索树详解-CSDN博客),我们在一般情况下对于二叉搜索树的插入与查询时间复杂度都是O(lgN),是十分快的,但是在一些特殊的情况下,二叉搜索树会退化成类似链表的存在,如下图。此时时间复杂度就恶化到了O(N)。
为了解决二叉搜索树的恶化,伟大的科学家们发明了AVL树与红黑树,我们今天了解的就是其中一种解决方法AVL树。
AVL树定义
两位俄罗斯的数学家G.M.Adelson-Velskii 和E.M.Landis在1962年 发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右 子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。AVL树就是根据他们的名字来命名的。
AVL树的定义与二叉搜索树类似。
1.它的左右子树都是AVL树
2.左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
3. AVL树的左边所有非空结点值小于根结点, AVL树的右边所有非空结点值大于根结点
在每次的插入或者删除后,都尽量保持二叉树的平衡,可以将AVL树看成一种平衡的二叉搜索树。为什么高度差的绝对值要不超过1,而不是等于0呢?原因也十分简单,假如有2个结点,我们不可能高度差为0,只能保持较为平衡的状态,如下图。
只有当结点个数刚好为2^n+1时才可以完全平衡,如下图。所以为了通用性,不只在结点个数特殊的情况下使用,规定左右子树高度差最大可以为一。
AVL树判断
下面我们来看几个事例判断是否是AVL树。
上图是AVL树么?我们首先判断他是不是二叉搜索树,其中序遍历为10 12 15 17,故为二叉搜索树,然后判断有没有平衡。此时就要依据平衡因子。
为了后序论述的方便我们规定平衡因子为右子树的高度减去左子树的高度(左子树的高度减去右子树的高度也可以,只需要在旋转操作时改下判断)。我们便可以在上面的图上分别标出他们的平衡因子.如下图。
通过上图我们可以发现以15为根结点的AVL树是平衡的,但以10为根结点的AVL树是不平衡的.其平衡因子的绝对值大于1.所以上面的二叉树不是AVL树。我们根据图像也可以明显的观察出右子树高度比左子树大很多,给人一种不平衡的感觉。
下面我们再看一个事例
上述的二叉树是AVL树么?首先依旧是看他的中序遍历结果4 9 10 12 15 17,有序所以为二叉搜索树,接下来看他是否平衡。
通过标出他们的平衡因子我们发现他们的绝对值都不大于1,所以平衡为AVL树。
二叉树中序遍历技巧
第一步是描点,在所有结点下面描个点,如下图
第二步是描边,用一条线,将二叉搜索树从根结点开始描一圈,如下图
然后将线上的按照出现的顺序依次写出来1 3 4 6 7 8 10 13 14,这样就可以得到一个二叉树的中序遍历结果了,前序遍历就是在结点左边描点,后序遍历就是在结点右边描点,大家感兴趣可以尝试,在这里就不多赘述了。
AVL树实现
AVL树结点
通过前面的定义,我们可以知道AVL树就是一种特殊的二叉树,我们就可以充分的利用之前的果实帮助我们完成AVL树。
template<class T>
struct BSTNode
{BSTNode(T k= T()):_left(nullptr), _right(nullptr), _key(k){}BSTNode* _left;BSTNode* _right;T _key;
};
二叉搜索树的一个结点定义如上,我们定义了左右指针加上当前节点的值,但AVL树判断平衡要频繁使用平衡因子,我们就可以设置一个平衡因子变量,其次在后面旋转操作的时候,我们要经常访问当前结点的父亲结点,就可以再增加一个指针指向父亲结点。
这样每一个结点就如下图,便于我们操作。
于是我们便可以定义出如下结点,和AVL类。这里为了方便将AVLTNode<T>类型取了别名node。
template<class T>
struct AVLTNode
{AVLTNode(T k = T()):_left(nullptr), _right(nullptr), _key(k), _parent(nullptr),_bf(0){}AVLTNode* _left;//左子结点AVLTNode* _right;//右子结点AVLTNode* _parent;//父亲结点T _key;//有效值int _bf;//平衡因子 balance factor
};
template<class T>
class AVLTree
{
private:typedef AVLTNode<T> node;public:private:node* _root;
};
AVL树构造函数
这里我们就写几个基础的构造函数,核心是后面的插入,删除操作。
AVLTree()
{_root = nullptr;
}
AVLTree(int k)
{_root = new node(k);
}
AVL树析构函数
有了构造函数自然少不了析构函数了。AVL树的内存是用new在堆上开辟出来的,我们必须要些析构函数来主动的释放内存,否则会造成内存泄漏。
由于AVL树也是二叉树,我们可以采用递归式的销毁,先释放左子树再释放右子树,最后释放当前结点。
但是析构函数本身不支持传递参数,无法用于递归销毁,我们就可以重新定义个子函数帮助我们销毁。其中子函数最好放在private修饰符下,这样就只可以再类内访问该函数,类外面就使用不了。
~AVLTree(){AVLTreeDestory(_root);_root = nullptr;}private:void AVLTreeDestory(node* _root){if (_root == nullptr)return;AVLTreeDestory(_root->_left);AVLTreeDestory(_root->_right);delete _root;}
AVL树插入
一种数据结构,就是一种数据存储与管理的方式。我们实现这种数据结构必定离不开四大经典功能,增删查改,不过在AVL树种改边改变key基本不会用,就去掉这种功能。AVL树主要用于数据的查找,时间复杂度为O(lgN),效率十分快。
接下来我们就来实现数据的插入。
我们定义插入函数主体如下,返回bool值,如果插入成功就返回true,这次实现的AVL树是不支持相同数字插入的,所以如果插入元素在AVL树中就返回false。
bool insert(T k)
{}
在插入前,我们首先要找到插入的位置在哪里。假如我们在下图中插入13
我们可以将13与根结点的值比较大小,比他大就在根结点的右子树找,比他小就在根结点的左子树找,依次往下遍历找,我们可以发现13最后要插入到12的右子树上。如下图。
bool insert(T k)
{//二叉搜索树没有结点if (_root == nullptr){_root = new node(k);return true;}//找到合适的位置node* parent = nullptr;node* cur = _root;while (cur){//提前存下父节点parent = cur;//k大于结点值就去右边,小于就去左边if (cur->_key < k)cur = cur->_right;else if (cur->_key > k)cur = cur->_left;//一般的二叉搜索树不允许出现相同的数字elsereturn false;}//先判断再插入if (parent->_key > k){parent->_left = new node(k); cur = parent->_left;cur->_parent = parent;}else{parent->_right = new node(k);cur = parent->_right;cur->_parent = parent;}return true;
}
平衡因子变化
由此我们便可以在AVL树中插入一个数,但此时还没有结束,当我们插入一个数的时候,AVL数种节点的平衡因子会发生改变,所以我们要修改平衡因子。变化如下图
我们发现只有插入点13的父亲一族平衡因子会发生改变,其他的结点不会改变。于是在修改平衡因子的时候我们只需要不断遍历当前结点的父亲结点即可,不需要全部遍历一遍,此时我们在每一个结点中加的父亲指针就大大简化了代码。
我们可以发现如果插入的结点在12的右边,那么12的平衡因子就加一,如果插入的结点在12的左边,那么12的平衡因子就减一。原因很简单,我们规定平衡因子等于右子树的高度减左子树的高度,在12的那边插入,那边子树的高度就加一,从而造成12的平衡因子变化。
此时我们需要对于12的平衡因子进行判断
1.平衡因子为0
如果12的平衡因子为0,说明此时插入时原来较矮的一边子树,此时以12为根结点的AVL树平衡,就不需要继续往上更新父亲的平衡因子了。因为以12为根结点的AVL树高度无变化,不可能引起父亲的平衡因子变化。例如将上图在加个11结点。
2.平衡因子为1或-1
此时我们知道当前结点由原来的平衡,转变为了较不平衡的状态,高度发生了变化,一边高,一边低,此时就需要不断的向上更新父亲的平衡因子了.例如对于12的父亲结点15来说,12为根结点的AVL树高度发生了变化,那么15的平衡因子也需要发生变化。此时15的变化也就可以类似判断12是15的左节点还是右节点,我们规定平衡因子等于右子树的高度减左子树的高度,那么在左结点就减一,右节点就加一。
上述两种情况实现的代码大致如下。
//修改平衡因子//1.根节点的父亲结点为空,最坏情况走到根结点结束//2.插入完成后刚好parent,cur指向需要的位置while (parent){if (parent->_left == cur)parent->_bf--;elseparent->_bf++;//没有引起高度变化if (parent->_bf == 0)break;//引起可以接受的高度变化else if (parent->_bf == 1 || parent->_bf == -1){cur = parent;parent = parent->_parent;}}
3.平衡因子为2或-2
此时我们的结点插入在了原本就不平衡的树上,并且还插入到了原来较高的那一边子树上,使AVL树更加的不平衡,违背了AVL树的定义,此时我们就需要进行旋转来改变平衡因子。
左单旋
例如我们在下面左图的AVL树中加入18结点,对于17结点来说他是可以接受的不平衡就继续往上更新父亲结点的平衡因子。
cur结点来自parent的右边,所以15的平衡因子较原来的加一,为1在可接受范围内,就继续往上更新。
同理可以求的当前的parent的平衡因子为2,cur的平衡因子为1,即以10为根结点的AVL树不平衡。我们此时需要进行旋转操作。
经过G.M.Adelson-Velskii 和E.M.Landis两位伟大的数学家研究发现,此时只需要将AVL树左旋即可。
将根节点10从左边向下旋转,成为cur的左子树,将cur原来的左子树改为parent的右子树,此时旋转过后,parent与cur的平衡因子都为0.由此我们便通过一次旋转让原本不平衡的AVL树再次平衡起来。
注意左单旋的条件是parent的平衡因子为2,cur的平衡因子为1.新结点插入在c子树上
此时我们就可以,将上述图像抽象出来,更加的理解左单旋。如下图
旋转之后我们还要判断他是否是AVL树,我们通过图片分析可以知道,a,b,c三个子树没变化,为AVL子树,parent,cur结点的左右子树高度相同,故他们的平衡因子都为0,符合要求。
接着我们要判断旋转后的树是不是二叉搜索树。此时我们就可以根据定义来看。首先根据未旋转图像我们可以判断出 a<30, 30< b<60, c>60.然后再旋转后b子树跑到了30节点的右子树上面,满足30右子树大于30条件,此时以30为根结点的二叉树是二叉搜索树,再以60为根结点来看, 以30为根结点的二叉搜索树 所有元素小于60,满足60的左子树所有值小于60,所以综上所述,旋转后的二叉树是AVL树。
由上图分析我们就可以单独创建一个左旋函数,在这个函数里面对AVL树进行旋转。上述是以根节点为例画图理解,如果parent结点不是根结点,是某段子树与之分析一样,除了以parent为根结点AVL树变化,其他部分不变。在这里就不过多赘述了。()
最终分析完成后代码的结果如下。
void RotateL(node* parent)
{node* subR = parent->_right;node* subRL = parent->_right->_left;node* parentparent = parent->_parent;//parent为根节点特殊处理if (parentparent == nullptr){subR->_parent = nullptr;_root = subR;}else{//parent为某段子树if (parentparent->_left == parent)parentparent->_left = subR;elseparentparent->_right = subR;subR->_parent = parent->_parent;}parent->_right = subRL;if (subRL)subRL->_parent = parent;subR->_left = parent;parent->_parent = subR;parent->_bf = 0;subR->_bf = 0;
}
于是我们就可以将上面,上面的代码修改如下。直接调用封装好的函数。
//修改平衡因子//1.根节点的父亲结点为空,最坏情况走到根结点结束//2.插入完成后刚好parent,cur指向需要的位置while (parent){if (parent->_left == cur)parent->_bf--;elseparent->_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){if (parent->_bf == 2 && cur->_bf == 1){RotateL(parent);}}else{assert(false);}}
右单旋
右单旋与左单旋十分的类似,我们采用左单旋是因为右子树看起来偏大,高度高,同理当左子树高度高时,我们就可以考虑用右单旋。
下面我们同样看个示例,与上个示例类似,只不过此时我们插入3
然后不断向上循环判断平衡因子。
当我们再向上遍历时,此时的parent的平衡因子为-2,我们此时就要对他进行旋转,只有将他旋转为正常的AVL树才可以继续向上遍历。
我们就可以以9为parent结点开始右旋,如下图。
将结点9从右边向下旋转,成为cur的右子树,将cur原来的左子树改为parent的左子树(没有就指向空),此时旋转过后,parent与cur的平衡因子都为0.由此我们便通过一次旋转让原本不平衡的AVL树再次平衡起来。
同样注意右单旋的条件是parent的平衡因子为-2,cur的平衡因子为-1.新结点插入在a子树上
旋转后4结点的平衡因子为0,9的平衡因子为0.我们可以抽象为如下图(为了方便同样假定parent结点为根结点,像本例parent为某段子树根节点的分析与整个树根结点分析基本相同)
与左旋相同,我们要判断旋转之后他是否是AVL树,我们通过图片分析可以知道,a,b,c三个子树没变化,为AVL子树,parent,cur结点的左右子树高度相同,故他们的平衡因子都为0,符合要求。
接着我们要判断旋转后的树是不是二叉搜索树。此时我们就可以根据定义来看。首先根据未旋转图像我们可以判断出 a<15, 15< b<30, c>30.然后再旋转后b子树跑到了30节点的左子树上面,满足30左子树小于30条件,此时以30为根结点的二叉树是二叉搜索树,再以15为根结点来看, 以30为根结点的二叉搜索树 所有元素大于15,满足60的右子树所有值大于15,所以综上所述,旋转后的二叉树是AVL树。
由上图分析我们就可以单独创建一个右旋函数,在这个函数里面对AVL树进行旋转。经过上述的分析,最终分析完成后代码的结果如下。
void RotateR(node* parent)
{node* parentparent = parent->_parent;node* subL = parent->_left;node* subLR = subL->_right;subL->_right = parent;if (parentparent == nullptr){subL->_parent = nullptr;_root = subL;}else{subL->_parent = parentparent;if (parentparent->_left == parent)parentparent->_left = subL;elseparentparent->_right = subL;}parent->_left = subLR;if (subLR)subLR->_parent = parent;parent->_parent = subL;parent->_bf = 0;subL->_bf = 0;
}
那么我们根据平衡因子判断就可以扩充为如下代码。注意我们最后加了个else断言,这是用来测试的,一旦我们的平衡因子出现不是0 1 -1 2 -2的值说明我们之前的处理是有问题的,可以提醒我们修改。
//没有引起高度变化
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
{assert(false);
}
右左旋转
光有上面两个是不够的,我们在实际的插入中还会有其他的情况。
例如在下图中插入12结点就会导致原本较为平衡的AVL树失去平衡,10结点的平衡因子为2,就需要调整AVL树使其保持平衡。
此时我们对10进行左旋是解决不了问题的,如下图。旋转后二叉树任然不平衡,此时我们就需要多次旋转,进行右左双旋。
右左双旋。就是先以15为根结点进行右旋如下图,此时AVL树就变为第一种左单旋的情况,再以10为根结点进行左旋即可。
同样注意右左双旋的条件是parent的平衡因子为2,cur的平衡因子为-1.新结点插入在b或c子树上
与上述一样我们可以画出抽象图理解。不过此时相较于之前有些特殊,如下图我们只能推断出a,d子树的高度为h,我们只知道以40为根结点的二叉树高度为h+1,但对于具体b,c的子树高度是不知道的,我们此时就需要分类讨论了。
假如b的高度为h-1,c的高度为h,则他们的变化如下图(不熟悉旋转操作的可以先看下左右单旋,双旋只不过进行两次单旋罢了。)
此时我们根据左右子树的高度再更新他们的平衡因子,如下图
假如b的高度为h,c的高度为h-1,经过上述相同的变化如下图.
注意此时的平衡因子发生了变化,待会写代码时要额外注意。
假如b的高度为h,c的高度为h,经过上述相同的变化如下图.此时就为刚开始举得示例情况,b,c为空子树
此时的平衡因子都为0.由此我们便认识完了右左双旋的所有情况,就可以同样封装的函数写了,我们注意到右左双旋实际上就是之前的左单旋和右单旋的组合,就可以复用之前的代码,不过最后要注意平衡因子会发生变化。
最后代码如下
void RotateRL(node* parent){node* subR = parent->_right;node* subRL = subR->_left;int bf = subRL->_bf;//先右旋RotateR(subR);//再左旋RotateL(parent);//更新平衡因子,b,c 子树的高度不同,就可以通过,subRL的平衡因子反应if (bf == 0){parent->_bf = subR->_bf = subRL->_bf = 0;}else if (bf == 1){parent->_bf = -1;subR->_bf = subRL->_bf = 0;}else if (bf == -1){parent->_bf = subRL->_bf = 0;subR->_bf = 1;}else{assert(false);}}
左右旋转
有右左双旋,自然也有左右双旋, 例如在下图中插入9结点就会导致原本较为平衡的AVL树失去平衡,10结点的平衡因子为-2,就需要调整AVL树使其保持平衡。
同样我们对他进行右旋是解决不了问题的。
我们要对他进行两次旋转,第一次以8为根结点左旋,第二次以10为根结点右旋
同样注意左右双旋的条件是parent的平衡因子为-2,cur的平衡因子为1.新结点插入在b或c子树上
与上述一样我们可以画出抽象图理解。不过此时相较于之前有些特殊,如下图我们只能推断出a,d子树的高度为h,我们只知道以40为根结点的二叉树高度为h+1,但对于具体b,c的子树高度是不知道的,我们此时就需要分类讨论了。与右左双旋的情况十分相似。
假如b的高度为h,c的高度为h-1,经过上述的变化如下图.
此时要注意的是平衡因子发生变化。根据左右子树的高度即可求出
假如b的高度为h,c的高度为h,经过上述相同的变化如下图.同时再求出平衡因子,此时就为刚开始举得示例情况,b,c为空子树
假如b的高度为h-1,c的高度为h,经过上述相同的变化如下图..同时再求出平衡因子
有了上面的分析,我们就可以写出左右旋转的函数了。
void RotateLR(node* parent){node* subL = parent->_left;node* subLR = subL->_right;int bf = subLR->_bf;//先左旋RotateL(subL);//再右旋RotateR(parent);//更新平衡因子,b,c 子树的高度不同,就可以通过,subRL的平衡因子反应if (bf == 0){parent->_bf = subL->_bf = subLR->_bf = 0;}else if (bf == 1){subL->_bf = -1;parent->_bf = subLR->_bf = 0;}else if (bf == -1){parent->_bf = 1;subL->_bf = subLR->_bf = 0;}else{assert(false);}}
插入函数的全部代码如下。
bool insert(T k)
{//二叉搜索树没有结点if (_root == nullptr){_root = new node(k);return true;}//找到合适的位置node* parent = nullptr;node* cur = _root;while (cur){//提前存下父节点parent = cur;//k大于结点值就去右边,小于就去左边if (cur->_key < k)cur = cur->_right;else if (cur->_key > k)cur = cur->_left;//一般的二叉搜索树不允许出现相同的数字elsereturn false;}//先判断再插入if (parent->_key > k){parent->_left = new node(k); cur = parent->_left;cur->_parent = parent;}else{parent->_right = new node(k);cur = parent->_right;cur->_parent = parent;}//修改平衡因子//1.根节点的父亲结点为空,最坏情况走到根结点结束//2.插入完成后刚好parent,cur指向需要的位置while (parent){if (parent->_left == cur)parent->_bf--;elseparent->_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){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);}}
注意在我们旋转后就将循环结束了,不需要再向上更新平衡因子了。这是因为旋转后子树的高度与我们插入前的高度一样,所以不会引起父亲结点平衡因子的变化。
到此我们的插入函数就讲完了!!真的十分不容易,感谢你能看到这。也请点点赞和关注。
AVL树测试
到这里我们就完成了AVL树的插入,构造和析构函数,当然还有删除,删除我打算放在下一篇文章再开始写,我们写完了插入必定少不了测试,尤其是这么复杂的AVL树插入,写错某个小点就会导致代码错误,我们就可以针对刚才写的代码进行测试,检验我们代码是否正确,错了就修改。
首先我们写的主要代码是插入,我们就可以检测插入后的二叉树是不是AVL树,如果不是就改BUG。虽然改BUG十分的烦人,但我们还是要静下心,慢慢改。
判断中序是否有序
我们可以先判断中序遍历结果,判断是不是二叉搜索树,再判断高度差。
与前面的问题一样,我们无法在类外部访问根结点,就需要再设置个子函数。采用先遍历左子树,根,然后遍历右子树就可以得到中序遍历结果了。
void Inorder(){_Inorder(_root);}private:void _Inorder(node* root){if (root == nullptr)return;_Inorder(root->_left);cout << root->_key << " ";_Inorder(root->_right);}
于是我们就可以写出第一个测试用例,结果如下是正确的
void test1()
{//vector<int> a= { 16, 3, 7, 11, 9, 26, 18, 14, 15 };vector<int> a = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };AVLTree<int> t;for (int i = 0; i < a.size(); i++){t.insert(a[i]);}t.Inorder();}
我们可以在测试一组,结果也是没有问题的
上述测试用例基本可以说明我们的中序是没有问题的了,接下来就是判断左右子树高度了。
平衡因子判断
判断平衡因子我们要经常求子树的高度,我们就可以先写求树高度的函数。如下
int height(node* root)
{if (root == nullptr)return 0;int left = height(root->_left);int right = height(root->_right);return left > right ? left + 1 : right + 1;
}
接着我们就可以递归的判断AVL树是否平衡,先求出当前结点平衡因子,判断是否合规,然后判断左子树和右子树。
void IsBalance(){if(_IsBalance(_root))cout << "是AVL树" << endl;elsecout << "不是AVL树" << endl;}private:bool _IsBalance(node* root){if (root == nullptr)return true;int left = height(root->_left);int right = height(root->_right);int bf = right - left;if (bf != root->_bf || bf > 1 || bf < -1)return false;return _IsBalance(root->_left) && _IsBalance(root->_right);}
于是我们就可以写出以下测试代码
void test2()
{vector<int> a= { 16, 3, 7, 11, 9, 26, 18, 14, 15 };//vector<int> a = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };AVLTree<int> t;for (int i = 0; i < a.size(); i++){if (a[i] == 7){int c = 0;}t.insert(a[i]);cout << a[i] << "当前状态:";t.IsBalance();}//t.IsBalance();
}
运行结果如下,说明我们的代码基本正确。
到处就可以确定我们的程序基本没有问题了,是正确的。如有错误欢迎大家指正(最终的代码大家以附录为准,前面复制过程中可能会出现错误。)
今天的代码到这里就结束了,欢迎大家指出错误。
附录代码
头文件代码
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;template<class T>
struct AVLTNode
{AVLTNode(T k = T()):_left(nullptr), _right(nullptr), _key(k), _parent(nullptr),_bf(0){}AVLTNode* _left;//左子结点AVLTNode* _right;//右子结点AVLTNode* _parent;//父亲结点T _key;//有效值int _bf;//平衡因子 balance factor
};template<class T>
class AVLTree
{typedef AVLTNode<T> node;
public:AVLTree(){_root = nullptr;}AVLTree(int k){_root = new node(k);}~AVLTree(){AVLTreeDestory(_root);_root = nullptr;}bool insert(T k){//二叉搜索树没有结点if (_root == nullptr){_root = new node(k);return true;}//找到合适的位置node* parent = nullptr;node* cur = _root;while (cur){//提前存下父节点parent = cur;//k大于结点值就去右边,小于就去左边if (cur->_key < k)cur = cur->_right;else if (cur->_key > k)cur = cur->_left;//一般的二叉搜索树不允许出现相同的数字elsereturn false;}//先判断再插入if (parent->_key > k){parent->_left = new node(k); cur = parent->_left;cur->_parent = parent;}else{parent->_right = new node(k);cur = parent->_right;cur->_parent = parent;}//修改平衡因子//1.根节点的父亲结点为空,最坏情况走到根结点结束//2.插入完成后刚好parent,cur指向需要的位置while (parent){if (parent->_left == cur)parent->_bf--;elseparent->_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){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);}}return true;}void Inorder(){_Inorder(_root);}void IsBalance(){if(_IsBalance(_root))cout << "是AVL树" << endl;elsecout << "不是AVL树" << endl;}private:int height(node* root){if (root == nullptr)return 0;int left = height(root->_left);int right = height(root->_right);return left > right ? left + 1 : right + 1;}bool _IsBalance(node* root){if (root == nullptr)return true;int left = height(root->_left);int right = height(root->_right);int bf = right - left;if (bf != root->_bf || bf > 1 || bf < -1)return false;return _IsBalance(root->_left) && _IsBalance(root->_right);}void _Inorder(node* root){if (root == nullptr)return;_Inorder(root->_left);cout << root->_key << " ";_Inorder(root->_right);}void AVLTreeDestory(node* _root){if (_root == nullptr)return;AVLTreeDestory(_root->_left);AVLTreeDestory(_root->_right);delete _root;}void RotateL(node* parent){node* subR = parent->_right;node* subRL = parent->_right->_left;node* parentparent = parent->_parent;//parent为根节点特殊处理if (parentparent == nullptr){subR->_parent = nullptr;_root = subR;}else{//parent为某段子树if (parentparent->_left == parent)parentparent->_left = subR;elseparentparent->_right = subR;subR->_parent = parent->_parent;}parent->_right = subRL;if (subRL)subRL->_parent = parent;subR->_left = parent;parent->_parent = subR;parent->_bf = 0;subR->_bf = 0;}void RotateR(node* parent){node* parentparent = parent->_parent;node* subL = parent->_left;node* subLR = subL->_right;subL->_right = parent;if (parentparent == nullptr){subL->_parent = nullptr;_root = subL;}else{subL->_parent = parentparent;if (parentparent->_left == parent)parentparent->_left = subL;elseparentparent->_right = subL;}parent->_left = subLR;if (subLR)subLR->_parent = parent;parent->_parent = subL;parent->_bf = 0;subL->_bf = 0;}void RotateRL(node* parent){node* subR = parent->_right;node* subRL = subR->_left;int bf = subRL->_bf;//先右旋RotateR(subR);//再左旋RotateL(parent);//更新平衡因子,b,c 子树的高度不同,就可以通过,subRL的平衡因子反应if (bf == 0){parent->_bf = subR->_bf = subRL->_bf = 0;}else if (bf == 1){parent->_bf = -1;subR->_bf = subRL->_bf = 0;}else if (bf == -1){parent->_bf = subRL->_bf = 0;subR->_bf = 1;}else{assert(false);}}void RotateLR(node* parent){node* subL = parent->_left;node* subLR = subL->_right;int bf = subLR->_bf;//先左旋RotateL(subL);//再右旋RotateR(parent);//更新平衡因子,b,c 子树的高度不同,就可以通过,subRL的平衡因子反应if (bf == 0){parent->_bf = subL->_bf = subLR->_bf = 0;}else if (bf == 1){subL->_bf = -1;parent->_bf = subLR->_bf = 0;}else if (bf == -1){parent->_bf = 1;subL->_bf = subLR->_bf = 0;}else{assert(false);}}node* _root;
};
源文件代码
#include"AVLTree.h"
#include<vector>void test1()
{//vector<int> a= { 16, 3, 7, 11, 9, 26, 18, 14, 15 };vector<int> a = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };AVLTree<int> t;for (int i = 0; i < a.size(); i++){t.insert(a[i]);}t.Inorder();}void test2()
{//vector<int> a= { 16, 3, 7, 11, 9, 26, 18, 14, 15 };vector<int> a = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };AVLTree<int> t;for (int i = 0; i < a.size(); i++){if (a[i] == 14){int c = 0;}t.insert(a[i]);cout << a[i] << "当前状态:";t.IsBalance();}//t.IsBalance();
}int main()
{test2();return 0;
}
相关文章:

AVL树超详解上
前言 学习过了二叉树以及二叉搜索树后(不了解二叉搜索树的朋友可以先看看这篇博客,二叉搜索树详解-CSDN博客),我们在一般情况下对于二叉搜索树的插入与查询时间复杂度都是O(lgN),是十分快的,但是在一些特殊…...
spring boot 实现token验证登陆状态
1、添加maven依赖到pom.xml <dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.5</version></dependency><dependency><groupId>io.jsonwebtoken</groupId>…...
【.NET全栈】ASP.NET开发Web应用——用户控件和绘图
文章目录 前言一、用户控件1、创建用户控件2、使用用户控件3、在web.config中注册用户控件4、用户控件中公开属性5、用户控件事件6、动态加载用户控件 二、动态绘图1、基本绘图2、绘制一个自定义的图片3、在Web页面放置自定义图片4、图片格式和质量5、一个Web绘图示例程序 前言…...

一行Python代码实现数据清洗的18种方法
目录 1. 去除字符串两边空格 2. 转换数据类型 3. 大小写转换 4. 移除列表中的重复元素 5. 快速统计元素出现次数 6. 字符串分割成列表 7. 列表合并 8. 数据填充 9. 提取日期时间 10. 字符串替换 11. 快速排序 12. 提取数字 13. 空值处理(假设是列表&am…...
Java API练习 (1) (2024.7.20)
Date类 package APIExercise20240720; import java.util.Date; // 导包,Date是util下的 public class Date20240720 {public static void main(String[] args) {Date nowTime new Date(); // 得到当前系统时间System.out.println(nowTime);Date startTime new Da…...

JavaScript之WebAPIs-BOM
目录 BOM操作浏览器一、Window对象1.1 BOM(浏览器对象模型)1.2 定时器-延时函数1.3 js执行机制1.4 location对象1.5 navigator对象1.6 history对象 二、本地存储三、补充数组中的map方法数组中的join方法数组中的forEach方法(重点)数组中的filter方法(重…...
Math Reference Notes: 数学思想和方法
文章目录 1. 数学思想1.1 数形结合思想1.2 转化思想1.3 分类讨论思想1.4 整体思想 2. 数学方法2.1 配方法2.2 因式分解法2.3 待定系数法2.4 换元法2.5 构造法2.6 等积法2.7 反证法2.8 判别式法 1. 数学思想 1.1 数形结合思想 定义:将数与形(代数与几何…...

Spring Cloud GateWay(4.1.4)
介绍 该项目提供了一个建立在 Spring 生态系统之上的 API 网关,包括:Spring 6、Spring Boot 3 和 Project Reactor。Spring Cloud Gateway 旨在提供一种简单而有效的方法来路由到 API,并为其提供跨领域关注点,例如:安…...

基于PHP+MYSQL开发制作的趣味测试网站源码
基于PHPMYSQL开发制作的趣味测试网站源码。可在后台提前设置好缘分, 自己手动在数据库里修改数据,数据库里有就会优先查询数据库的信息, 没设置的话第一次查询缘分都是非常好的 95-99,第二次查就比较差 , 所以如果要…...
【微信小程序】wx.navigateTo传参时不能使用const定义的数据类型
2024年7月21日更新 今日调试时发现似乎是因为使用vant-weapp时按照官方提示关闭了style:"v2"导致的此情况,打开之后无法复现该内容,特此提示。 以下是原内容 如题,笔者测试了好久才找到这个bug,想传递的数据是this.d…...

【Android studio环境搭建】Android studio连接夜神模拟器
Android studio连接夜神模拟器 一、 步骤 1.下载好Android Studio和夜神模拟器, 2.打开夜神模拟器,找到其安装目录下的 nox_adb.exe文件 3.右键进入cmd命令打开,管理员权限执行下面命令 PS D:\Program Files\Nox\bin> .\nox_adb.exe connect 127.…...

Qt:26.Qt项目:贪吃蛇游戏
一、项目功能演示: 开始界面可以点击进入游戏。 点击进入游戏之后,切换到选项界面,该界面可以选择游戏难度,回退,以及查询最近一次游戏得分。 游戏具体界面如下。贴图啥的可以自己换,本人审美不咋行&#x…...

通过HTML/CSS 实现各类进度条的功能。
需求:我们在开发中会遇到使用各式各样的进度条,因为当前插件里面进度条各式各样的,为了方便我们定制化的开发和方便修改样式,我们这里使用HTML和CSS样式来进行开发进度条功能。 通过本文学习我们会明白如何使用 HTML/CSS 创建各种…...

Opencv学习项目3——人脸识别
之前我们获取了一张图像的人脸信息,现在我们来使用特征点分析来匹配两张lyf照片的相似度 获取两张图片的人脸信息 import cv2 import face_recognition# 加载图像文件 img1 face_recognition.load_image_file(lyf1.png) img2 face_recognition.load_image_file(l…...
【js自学打卡11】生成器函数(generator函数)的使用总结+代码举例
力扣的js入门免费题刷完了,开始自己找题练练,顺便捡捡知识点 力扣2649 1.思路 一眼递归,但事实证明也可以直接flat手撕。 arr.flat(Infinity) //直接扁平化到最底层涉及到了一些关于生成器和异步编程相关的知识点,学一下。 2.…...

深入了解jdbc-02-CRUD
文章目录 操作和访问数据库Statement操作数据表的弊端sql注入问题PreparedStatement类ResultSet类与ResultSetMetaData类资源的释放批量插入 操作和访问数据库 数据库的调用的不同方式: Statement:用于执行静态 SQL 语句并返回它所生成结果的对象。PreparedStatem…...

《基于 Kafka + Quartz 实现时限质控方案》
📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗 🌻 CSDN入驻不久,希望大家多多支持,后续会继续提升文章质量,绝不滥竽充数…...
浏览器的卡顿与react的解决思路
以下内容是阅读过程中结合自己的思考而诞生的产物,不一定准确,但相反的,可能个人对实际情况有很大的误解。 仅做参考,欢迎指正。 前面提到浏览器显示的其实是渲染流程最后渲染出来的一张图片,而一个行为引起的副作用需…...

XXE:XML外部实体引入
XXE漏洞 如果服务器没有对客户端的xml数据进行限制,且版本较低的情况下,就可能会产生xxe漏洞 漏洞利用流程 1.客户端发送xml文件,其中dtd存在恶意的外部实体引用 2.服务器进行解析 3.服务器返回实体引用内容 危害:任意文件读…...

3D培训大师创新培训体验,加速空调关键组件的高效精准安装
如今,空调系统的复杂性和精密性与日俱增,对专业技术人员的要求也日益提高。尤其是决定空调是否能平稳运行的空调关键组件的装配培训,不再局限于传统的理论讲解和实体模型演示,而是更注重数字化、沉浸式学习。 案例背景 某空调公…...

Linux应用开发之网络套接字编程(实例篇)
服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …...
[2025CVPR]DeepVideo-R1:基于难度感知回归GRPO的视频强化微调框架详解
突破视频大语言模型推理瓶颈,在多个视频基准上实现SOTA性能 一、核心问题与创新亮点 1.1 GRPO在视频任务中的两大挑战 安全措施依赖问题 GRPO使用min和clip函数限制策略更新幅度,导致: 梯度抑制:当新旧策略差异过大时梯度消失收敛困难:策略无法充分优化# 传统GRPO的梯…...

Flask RESTful 示例
目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题: 下面创建一个简单的Flask RESTful API示例。首先,我们需要创建环境,安装必要的依赖,然后…...
【磁盘】每天掌握一个Linux命令 - iostat
目录 【磁盘】每天掌握一个Linux命令 - iostat工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景 注意事项 【磁盘】每天掌握一个Linux命令 - iostat 工具概述 iostat(I/O Statistics)是Linux系统下用于监视系统输入输出设备和CPU使…...

dedecms 织梦自定义表单留言增加ajax验证码功能
增加ajax功能模块,用户不点击提交按钮,只要输入框失去焦点,就会提前提示验证码是否正确。 一,模板上增加验证码 <input name"vdcode"id"vdcode" placeholder"请输入验证码" type"text&quo…...
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别 直接训练提示词嵌入向量的核心区别 您提到的代码: prompt_embedding = initial_embedding.clone().requires_grad_(True) optimizer = torch.optim.Adam([prompt_embedding...

Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
鸿蒙DevEco Studio HarmonyOS 5跑酷小游戏实现指南
1. 项目概述 本跑酷小游戏基于鸿蒙HarmonyOS 5开发,使用DevEco Studio作为开发工具,采用Java语言实现,包含角色控制、障碍物生成和分数计算系统。 2. 项目结构 /src/main/java/com/example/runner/├── MainAbilitySlice.java // 主界…...
C++.OpenGL (14/64)多光源(Multiple Lights)
多光源(Multiple Lights) 多光源渲染技术概览 #mermaid-svg-3L5e5gGn76TNh7Lq {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-3L5e5gGn76TNh7Lq .error-icon{fill:#552222;}#mermaid-svg-3L5e5gGn76TNh7Lq .erro…...
Redis:现代应用开发的高效内存数据存储利器
一、Redis的起源与发展 Redis最初由意大利程序员Salvatore Sanfilippo在2009年开发,其初衷是为了满足他自己的一个项目需求,即需要一个高性能的键值存储系统来解决传统数据库在高并发场景下的性能瓶颈。随着项目的开源,Redis凭借其简单易用、…...