二叉树进阶--二叉搜索树的进一步优化--AVL树 Self-balancing binary search tree

前言:

在上一次的文章中,我们详细介绍了二叉树的进阶树型,即BS树(二叉搜索树),但在文章的结尾,二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,我们迫切需要一个可以解决这个问题同时又不会破坏BS树本身性质的解决方案,这便是我们今天的AVL树。

AVL树的性质:

前言问题的解决方法:

对于前言中的问题,AVL树的解决方案为:
当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度
这样就避免了出现单支树的情况,同时保证了BS树的结构不变。

AVL树的性质:

故我们现在就可以总结出AVL树的一般规律了:
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
1.它的左右子树都是AVL树,同时符合BS树的基本规律
2.左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)

如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 O ( l o g 2 n ) O(log_2 n) O(log2n),搜索时间复杂度O( l o g 2 n log_2 n log2n)。
如图:
在这里插入图片描述
对于这样一棵单支树,我们就可以变成:
在这里插入图片描述
这样就提高了我们的搜索效率!
因此,我们也可以把AVL树简记为:一棵进行了高度平衡的二叉搜索树

!!!AVL树的实现(重点):

对于AVL树的结构和性质我们已经足够了解了,现在就让我们去实现一下AVL树吧:

1.单个节点结构体:

对于AVL树的每一个节点来说,首先我们必备的就是数据,左指针,右指针。其次,由于我们在平衡树的过程中涉及到一个频繁的改变节点指向的问题,因此我们还需要一个指向上一个父亲节点的指针parent,同时,我们还需要对每个节点存储一个平衡因子,以方便对树进行平衡。
因此,我们可以这样构建结构体:

template<class K,class V>
struct AVLTreeNode//构建三叉链,多加一个指向上一个节点的指针_parent
{AVLTreeNode(const pair<K, V>& kv)//构造函数:_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _bf(0){}AVLTreeNode<K, V>* _left;AVLTreeNode<K, V>* _right;AVLTreeNode<K, V>* _parent;//更新平衡因子,父亲指针,指向节点的父亲pair<K, V> _kv;//kv键值对int _bf;//平衡因子,balance factor
};

由于是为了后续的map和set做准备,因此我们这里采取使用键值对pair的方式来当作本次我们的数据的数据类型_kv
注意,别忘了构造函数,这里不能用默认的,因为bf我们是都要从0开始初始化的。

2.AVL树类主体:

对于AVL树而言,我们的主类形式大体如下:

template<class K, class V>
class AVLTree
{typedef struct AVLTreeNode<K, V> Node;
public://在这里实现对应的函数功能//////............
private:Node* _root = nullptr;
}

和二叉树的基本结构一样,我们的成员同样也只需要一个root根节点即可,方便遍历和向下移动等操作。

3.AVL树的插入!!!:

AVL树的插入,就是我们AVL树的精华所在,我们也是在这里进行平衡的,在这里,我大致将插入分为两个部分
1.常规的BS树插入过程
2.平衡树的过程

1.树的插入过程:

AVL树由于其本质和BS树差不多,因此前面的插入过程也大体相似,其步骤逻辑就是:
首先找到对应的位置,然后创建节点在对应的位置插入,这两部分。需要注意的地方是:我们要对空树进行判断,如果是空树就直接root=对应的新节点即可了,其他的同BS树的插入步骤一样。
代码如下:

bool insert(const pair<K,V>& kv)
{//插入过程,与搜索二叉树一样if (_root == nullptr){_root = new Node(kv);return true;}Node* cur = _root;Node* parent = nullptr;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;}}//指针移动完毕,找到我们要插入节点的对应空位,直到nullptr为止,然后开始插入节点cur = new Node(kv);if (parent->_kv.first > kv.first){parent->_left = cur;cur->_parent = parent;//让父亲指针指向插入节点的父亲节点}else if (parent->_kv.first < kv.first){parent->_right = cur;cur->_parent = parent;}//下面就是第二部分平衡树的过程//..............
}

2.树的平衡过程:

当我们插入完节点后,倘若不平衡,那它就是一棵普通的二叉搜索树无疑,因此只有通过接下来的平衡过程,它才会变成一棵我们看到的AVL树。
在这里,我大致将其分为两个部分:
1.调控平衡因子
2.通过平衡因子对树进行调整
我们大体的代码结构如下:
首先我们要说明平衡因子数值的计算方式:
以节点右树的高度为正,节点左树高度为负,通过右减去左来确定对应的平衡因子的数值。

while(parent)//和向上调整一样,看parent的改变效果,直到达到根节点为止,此时cur为根节点,parent为空了
{if (cur == parent->_left){parent->_bf--;//对父亲平衡因子进行调整}else if (cur == parent->_right){parent->_bf++;}//首先是先对我们第一部分创建的parent的平衡因子进行更新,从parent开始持续性地向上检查调整平衡因子,从而做出对应的平衡策略if (parent->_bf == 0){break;}//如果平衡因子为0,说明两边高度差相同,这说明这棵树是平衡的,那就不需要向上调整了,直接结束本次插入即可else if(parent->_bf==1||parent->_bf==-1){cur = parent;parent = parent->_parent;//这里存储的parent,就是为了向上调整因子而准备的}//如果为1/-1,说明存在平衡不平衡的可能性,则继续向上调整else if(parent->_bf==2||parent->_bf==-2)//2或者-2的情况,需要旋转平衡调整{//这里是针对不同情况进行旋转调整的过程}else//这里说明出现大于2的情况了,已经不是AVL树了,不调整直接报错,我们规定平衡因子的绝对值超过2以上就已经不是AVL树了,直接报错即可{assert(false);}
}

3.针对不同情况对树的调整方法:

我们在调控AVL树的时候,会遇到下面的四种情况:
1… 新节点插入较高左子树的左侧—(简记为左左):
如图:
在这里插入图片描述
而我们的目的是将其调整为下面的情况:
在这里插入图片描述
故在这里,我们就采取右单旋的方法即可解决,方法思路如下:
在这里插入图片描述
对于方框的部分,我们不需要去管,只需要对parent,sub1,sub2进行处理即可,最后别忘了对平衡因子进行更新处理即可。
代码如下:

void RotateR(Node* parent)//右单旋旋转调整
{Node* sub1 = parent->_left;Node* sub2 = sub1->_right;Node* PPparent = parent->_parent;parent->_left = sub2;if (sub2){sub2->_parent = parent;}sub1->_right = parent;parent->_parent = sub1;if (_root == parent)//如果为根节点,直接更新root即可,父亲指针指向空{_root = sub1;sub1->_parent = nullptr;}else//如果不为,则依照正常的逻辑进行父亲节点和对应的root节点进行互相指向即可{if (PPparent->_right == parent){PPparent->_right = sub1;}else if (PPparent->_left == parent){PPparent->_left = sub1;}sub1->_parent = PPparent;}parent->_bf = sub1->_bf = 0;
}

在代码这里我们需要注意几个点:
1.sub2是有可能存在为空的情况的,因此我们需要对sub2进行判断,否则很可能会出现空指针访问的情况
2.别忘了平衡因子
3.别忘了对新调整的sub1的parent进行处理,要让其指向的父亲节点和它对应好关系

2.新节点插入较高右子树的右侧—右右:左单旋
在这里插入图片描述
由于其本质和右单旋属于镜面对称的方式,因此我这里直接上代码,其需要注意的点和左单旋一样,我这里直接上代码了:

void RotateL(Node* parent)//左单旋旋转调整
{Node* sub1 = parent->_right;Node* sub2 = sub1->_left;Node* PPparent = parent->_parent;parent->_right = sub2;if (sub2)//注意这里,可能涉及到sub2为空的情况,比如h==0,此时要对sub2是否为空进行一次判断{sub2->_parent = parent;}sub1->_left = parent;parent->_parent = sub1;if (_root == parent){_root = sub1;sub1->_parent = nullptr;}else{if (PPparent->_right == parent){PPparent->_right = sub1;}else if (PPparent->_left == parent){PPparent->_left = sub1;}sub1->_parent = PPparent;}parent->_bf = sub1->_bf = 0;
}

我们大致的调控方法如下:
在这里插入图片描述
3.新节点插入较高左子树的右侧—左右:先左单旋再右单旋
这种属于情况比较复杂的情况,大致的情况图示如下:
在这里插入图片描述
我们处理方式为:先对30进行左单旋,然后再对90进行右单旋,旋转完成后再考虑平衡因子的更新。
代码如下:

	void RotateLR(Node* parent)//左右双旋转{Node* sub1 = parent->_left;Node* sub2 = sub1->_right;int bf = sub2->_bf;RotateL(parent->_left);RotateR(parent);if (bf == 0){parent->_bf = sub1->_bf = sub2->_bf = 0;}else if (bf == 1){parent->_bf = 0;sub2->_bf = 0;sub1->_bf = -1;}else if (bf == -1){sub2->_bf = 0;parent->_bf = 1;sub1->_bf = 0;}else{assert(false);}}

这种方法需要注意的就是,我们需要先存储sub2,即图中数值为60的节点的平衡因子数,即为代码中的bf,因此我们从图中可以得知,sub2的左右分别给了parent和sub1,因此我们就可以通过知道sub2左右的节点情况,对最后的parent sub1 sub2三个节点的平衡因子进行调整。

4.新节点插入较高右子树的左侧—右左:先右单旋再左单旋
在这里插入图片描述
代码如下:

void RotateRL(Node* parent)//右左双旋转
{Node* sub1 = parent->_right;Node* sub2 = sub1->_left;int bf = sub2->_bf;RotateR(parent->_right);RotateL(parent);if (bf == 0){//即插入的点就是分解后的sub2,此时对应的平衡树两边层数相同parent->_bf = sub1->_bf = sub2->_bf = 0;}else if (bf == 1){//sub2右子树新增sub2->_bf = 0;parent->_bf = -1;sub1->_bf = 0;}else if (bf == -1){//sub2左子树新增sub2->_bf = 0;parent->_bf = 0;sub1->_bf = 1;}else{assert(false);//即平衡因子出现错误的点,直接报错检查出来}
}

需要注意的点和第三种情况类似,在这里我不过多赘述了,直接看代码即可。
以上便是我们旋转的四种情况,根据上面的四种情况,我们对应相应的方法即可,对应的代码为:

//旋转
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;//调整一次后,对上面的根节点无影响了,恢复到插入前的高度了,不用再继续更新了,直接退出即可

AVL树的验证:

我们的AVL树构建好之后,接下来针对AVL树,我们需要进行一次关于AVL树的验证:
对于AVL树来说,其高度的平衡因子是我们最为关键的检验数据,因此我们的验证可以通过验证高度的方式来完成。
我们验证的方面主要有两个:
1.验证其为二叉搜索树
如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
2. 验证其为平衡树
每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
节点的平衡因子是否计算正确

我们的代码如下:

void _InOrder(Node* root)//中序遍历
{if (root == nullptr){return;}_InOrder(root->_left);cout << root->_kv.first << " ";_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 ? 1 + leftHeight : 1 + rightHeight;
}bool _IsBalance(Node* root)//判断是否为AVL树的函数,利用求二叉树的高度来判断
{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(leftheight - rightheight) < 2 && _IsBalance(root->_left) && _IsBalance(root->_right);
}

AVL树的性能分析:

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

总结:

以上便是关于AVL树的全部内容了,后续我会更新AVL树的删除节点的代码,希望在看完本文后,可以让你学会使用AVL树进行数据存储。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/527603.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【物联网】-智能社会的分类

万物感知 感知物理世界&#xff0c;变成数字信号 &#xff08;温度、空间、触觉、嗅觉、听觉、视觉&#xff09; 万物互联 将数据变成online&#xff0c;使智能化 &#xff08;宽联接、广联接、多联接和深联接&#xff09; 万物智能 基于大数据和人工智能的应用 &#…

多模太与交叉注意力应用

要解决的问题 对同一特征点1从不同角度去拍&#xff0c;在我们拿到这些不同视觉的特征后&#xff0c;就可以知道如何从第一个位置到第二个位置&#xff0c;再到第三个位置 对于传统算法 下面很多点检测都是错 loftr当今解决办法 整体流程 具体步骤 卷积提取特征&#xff0c;…

双体系Java学习之循环结构

循环结构 package struct;public class WhileDemo01 {public static void main(String[] args) {//输出1-100int i 0;while (i<100){i;System.out.println(i);}} }package struct;public class WhileDemo02 {public static void main(String[] args) {//死循环while (true)…

OpenCV filter2D函数详解

OpenCV filter2D函数简介 OpenCV filter2D将图像与内核进行卷积&#xff0c;将任意线性滤波器应用于图像。支持就地操作。当孔径部分位于图像之外时&#xff0c;该函数根据指定的边界模式插值异常像素值。 该函数实际上计算相关性&#xff0c;而不是卷积&#xff1a; filter…

C++20四大特性之Ranges

C20 Ranges 1.基础概念2.使用 在之前的文章已经写过另外三大特性&#xff0c;直通点&#xff1a; C那些事之C20协程开篇 盘点C20模块那些事 C20&#xff1a;从0到1学懂concept 那么&#xff0c;本篇将开始学习另外一个特性ranges。 ranges是C20的主要特性之一&#xff0c;其中&…

WebRTC简介及实战应用 — 从0到1实现实时音视频聊天等功能

一、WebRTC简介 WebRTC 是由一家名为 Gobal IP Solutions,简称 GIPS 的瑞典公司开发的。Google 在 2011 年收购了 GIPS,并将其源代码开源。然后又与 IETF 和 W3C 的相关标准机构合作,以确保行业达成共识。其中: Web Real-Time Communications (WEBRTC) W3C 组织:定义浏览…

Nginx的页面优化,安全优化,日志分割,配置防盗链,图片缓存,解决部分问题

一、隐藏版本号 他人可以使用调试代理工具&#xff08;fiddler工具&#xff09;、浏览器插件等&#xff0c;通过识别网站的响应头信息来获取你的 Nginx 版本号。为了增强服务器的安全和隐私&#xff0c;可以进行隐藏版本号&#xff0c;也可以在 nginx.h 文件中修改服务名称和版…

力扣同类题:重排链表

很明显做过一次 class Solution { public:void reorderList(ListNode* head) {if(!head||!head->next)return;ListNode *fasthead,*lowhead;ListNode *prenullptr,*curnullptr,*nextnullptr;while(fast->next!nullptr){fastfast->next;if(fast->next)fastfast->…

Yolov8-pose关键点检测:特征融合涨点篇 | 广义高效层聚合网络(GELAN) | YOLOv9

💡💡💡本文独家改进:即结合用梯度路径规划(CSPNet)和(ELAN)设计了一种广义的高效层聚合网络(GELAN),高效结合YOLOv8-pose,实现涨点。 将GELAN添加在backbone和head处,提供多个yaml改进方法 Yolov8-Pose关键点检测专栏介绍:https://blog.csdn.net/m0_6377421…

开源组件安全风险及应对

在软件开发的过程中&#xff0c;为了提升开发效率、软件质量和稳定性&#xff0c;并降低开发成本&#xff0c;使用开源组件是开发人员的不二选择&#xff08;实际上&#xff0c;所有软件开发技术的演进都是为了能够更短时间、更低成本地构建软件&#xff09;。这里的开源组件指…

2023年终总结——跌跌撞撞不断修正

目录 一、回顾1.一月&#xff0c;鼓足信心的开始2.二月&#xff0c;焦躁不安3.三月&#xff0c;路还是要一步一步的走4.四月&#xff0c;平平淡淡的前行5.五月&#xff0c;轰轰烈烈的前行6.六月&#xff0c;看事情更底层透彻了7.七月&#xff0c;设计模式升华月8.八月&#xff…

前端WebRTC局域网1V1视频通话

基本概念 WebRTC&#xff08;Web Real-Time Communications&#xff09; 网络实时通讯&#xff0c;它允许网络应用或者站点&#xff0c;在不借助中间媒介的情况下&#xff0c;建立点对点&#xff08;Peer-to-Peer&#xff09;的连接&#xff0c;实现视频流和音频流或者其他任…