[c++高阶]AVL树的深度剖析模拟实现

1.前言

如果你不知道什么是二叉搜索树,那么请你一定要阅读以下文章。

[c++高阶]二叉搜索树深度剖析-CSDN博客

二叉搜索树如果在已经有序的情况下进行插入的话,那么他的时间复杂度是O(N),然后有时候的时间复杂度又是O(logN),因此在实际使用中很少人会使用二叉搜索树。

为了解决上述问题,有2位俄罗斯的天才提出了AVL树的结构,也就是说高度平衡二叉搜索树。

这样二叉搜索树由原来不稳定变成了稳定了。

后面学习AVL树可能有点难,请各位小伙伴耐心阅读。

本章重点:

本章着重讲解AVL树的概念,AVL树性质以及定义、AVL树插入的详解等。

2.AVL树的性质

       当我们向二叉搜索树中插入新节点时,如果能用某种方法时刻保证树中每个节点的左右子树高度之差不超过1,就可以降低整棵树的高度,保证每条分支的平衡。

AVL树的性质如下:

  • AVL树可以是空树
  • 一颗AVL树的左右子树都是AVL树
  • 一颗AVL树的左右子树高度差不超过1

例如:

 3.AVL树的节点的定义

 AVL树一棵高度平衡的二叉树,那么如何通过代码的方式告知他自己是否平衡呢?

我们可以定义一个平衡因子,每一个节点都有一个平衡因子,然后通过判断该节点的平衡因子来判断这棵树或者这棵子树是否平衡。同时,如果左子树比右子树高一层,那么根节点的平衡因子就是-1;如果右子树比左子树高一层,那么根节点的平衡因子就是1,如果左右子树一样高,那么根节点的平衡因子就是0。

并且在定义之前答成一个约定:往左子树插入节点,那么根的平衡因子就--;往右子树插入节点,那么根的平衡因子就++。

因为我们后续在不平衡的时候要进行调整,而调整的过程中,要频繁的知道根节点的平衡因子是多少,所以在AVL树中需要使用三叉链,即在AVL树中,不仅有左指针,右指针,还有一个指向父节点的父指针。

定义代码如下:

template <class K,class V>
struct AVLTreeNode
{AVLTreeNode<K, V>* _left;AVLTreeNode<K, V>* _right;AVLTreeNode<K, V>* _parent;pair<K, V>  _kv;int _bf;//平衡因子,后续用来判断AVL树是否平衡AVLTreeNode(const pair<K,V>& kv):_left(nullptr),_right(nullptr),_parent(nullptr),_kv(kv),_bf(0){}
};

可能有的同学对于pair会有一些疑惑,不知道这是什么。简单解释一下:

pair可以将两个数据组成一组元素,因此对于key/value模型这种需要用到两个数据为一组的元素时就可以使用,内部的成员变量为first和second,其主要使用方法为:

pair<T1, T2> p1(v1, v2); //输入两个数据创建pair类型变量
make_pair(v1, v2);       //输入两个数据通过函数创建pair类型变量
p1.first                 //访问p1的第一个数据
p1.second                //访问p1的第二个数据

4.AVL树的插入深度剖析

先给出AVL树的基本结构:

template<class K,class V>
struct AVLTree
{typedef AVLTreeNode<K, V> Node;
private:Node* _root;//定义一个根节点
};

向AVL树中插入节点与向二叉搜索树中插入节点的过程基本相同,唯一的区别就是AVL树在插入节点后可能存在失衡的情况,需要调整。

AVL树的插入的三个步骤:

1.先找到自己对应的位置插入

2.更新平衡因子

3.对于平衡因子过大或者过小的要进行调整

AVL树的插入主要就是对以上三步进行理解,只要能够理解到位,就能够写出相应的代码。

第一步在讲解二叉搜索树的时候,已经说过了再这里就不过的赘述。

现在着重讲解更新平衡因子,以及平衡因子过大或者过小究竟是多大或者多小。

更新平衡因子规则如下:

1.往父节点的左边插入,父节点--

   往父节点的右边插入,父节点++

2.更新完后,看父节点的平衡因子是多少

   如果是1或者-1,那就表明之前是平衡的,现在新加了一个

   可能导致不平衡,所以要顺着这个分支一直遍历到根节点,

   判断是否有不平衡的现象出现。

 

3.如果某一个节点的值时2或者-2出现时,就表示这棵树不平衡了,那么就需要进行调整。

4.如果父节点由原来的1或者-1变成0的话,那么就表示这棵树由原来的不平衡变成了平衡,那么就不需要向上更新了。

例:

对于不平衡的那么就需要用旋转来进行处理了,由于旋转比较复杂,所以单独介绍旋转。

5.AVL树的旋转剖析

在介绍旋转前,先把前面的代码给出来。

template <class K,class V>
class AVLTree
{
public:typedef AVLTreeNode<K, V> Node;AVLTree()//构造函数{}bool Insert(const pair<K, V>& kv){//1.根节点为空的情况if (_root == nullptr){_root = new Node(kv);return true;}//2.不为空的情况,那么就需要先找到Node* prev = nullptr;Node* cur = _root;while (cur){if (cur->_kv.first > kv.first){prev = cur;cur = cur->_left;}else if (cur->_kv.first < kv.first){prev = cur;cur = cur->_right;}else return false;}//3.到这里就找到了cur = new Node(kv);if (prev->_kv.first > kv.first)prev->_left = cur;elseprev->_right = cur;cur->_parent = prev;//4.插入完了之后,就要更新根的平衡因子//然后根据平衡因子的大小来判断是否需要旋转while (prev){//插入左边,高度减-;插入右边,高度加一if (cur == prev->_left)prev->_bf--;elseprev->_bf++;//到这里就需要判断平衡因子是多少了if (prev->_bf == 0)break;else if (prev->_bf == 1 || prev->_bf == -1){//如果是1或者-1,那就表示高度变了,那么就需要向上更新prev = prev->_parent;cur = cur->_parent;}else if (prev->_bf == 2 || prev->_bf == -2){//到这里就需要进行旋转了}}return true;}~AVLTree(){_Destroy(_root);}
private:void _Destroy(Node* root){if (root == nullptr) return;_Destroy(root->_left);_Destroy(root->_right);delete root;root = nullptr;}
private:Node* _root = nullptr;
};

旋转一共有四种情况,不管哪种情况最终的目的

都是为了将不平衡的树变成平衡的树。

情况一:左单旋

简单的例子:

抽象的例子:

左单旋规则:

1.把父节点向左旋转,变成右孩子的左孩子

2.把右孩子的左孩子变成父节点的右孩子

代码如下:

void RotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;Node* pparent = parent->_parent;//开始链接parent->_right = subRL;if (subRL)subRL->_parent = parent;subR->_left = parent;parent->_parent = subR;if (parent == _root){_root = subR;subR->_parent = nullptr;}else{if (pparent->_left == parent)pparent->_left = subR;elsepparent->_right = subR;subR->_parent = pparent;}//链接完了,更新平衡因子parent->_bf = subR->_bf = 0;
}

情况二:右单旋

简单例子:

抽象的例子:

右单旋的规则:

1.父节点变成左孩子的右孩子,左孩子升级成为左孩子

2.左孩子的右孩子变成父节点的左孩子。

代码如下:

void RotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;Node* pparent = parent->_parent;//开始链接parent->_left = subLR;if (subLR)subLR->_parent = parent;parent->_parent = subL;subL->_right = parent;if (parent == _root){_root = subL;subL->_parent = nullptr;}else{if (pparent->_left == parent)pparent->_left = subL;else if (pparent->_right == parent)pparent->_right = subL;subL->_parent = pparent;}parent->_bf = subL->_bf = 0;
}

上述两种情况都是在二叉树的最边一侧进行添加孩子,而对于在内侧添加则没有体现。接下来两种情况就是着重介绍在内侧添加的情况。

上述两种情况代码如下:

void RotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;Node* pparent = parent->_parent;//开始链接parent->_right = subRL;if (subRL)subRL->_parent = parent;subR->_left = parent;parent->_parent = subR;if (parent == _root){_root = subR;subR->_parent = nullptr;}else{if (pparent->_left == parent)pparent->_left = subR;elsepparent->_right = subR;subR->_parent = pparent;}//链接完了,更新平衡因子parent->_bf = subR->_bf = 0;
}void RotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;Node* pparent = parent->_parent;//开始链接parent->_left = subLR;if (subLR)subLR->_parent = parent;parent->_parent = subL;subL->_right = parent;if (parent == _root){_root = subL;subL->_parent = nullptr;}else{if (pparent->_left == parent)pparent->_left = subL;else if (pparent->_right == parent)pparent->_right = subL;subL->_parent = pparent;}parent->_bf = subL->_bf = 0;
}

 上述代码看上去很复杂,其实是因为我们刚开始定义AVL树节点的时候,定义成了三叉链。所以上述都是在修改左右节点链接的对象和父节点链接的对象。

情况三:右左单旋

右左单旋简单理解就是先进行右旋转,然后再进行左旋转。

简单例子:

注意在左旋和右旋的时候,都是通过给出父节点来进行旋转的。

而此处的右旋,是以20节点为父节点进行旋转的,所以在传值的时候要千万小心。

抽象的例子:

注意:这里在b和c处插入,用的旋转都是一样的,只是对最后平衡因子的具体数值有影响。

代码如下:

void RotateRL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;RotateR(subR);RotateL(parent);//这里也是更新平衡因子if (bf == -1){parent->_bf = 0;subR->_bf = 1;subRL->_bf = 0;}else if (bf == 1){parent->_bf = -1;subR->_bf = 0;subRL->_bf = 0;}else{parent->_bf = 0;subR->_bf = 0;subRL->_bf = 0;}
}

 这里面还加了更新平衡因子的代码。具体解释如下图:

1.旋转玩之后平衡因子全为0的情况:

 2.旋转之后父节点变成了-1,而其他节点变成了0的情况

见图抽象的例子那一张

3.旋转之后父节点的右孩子变成了1,而其他节点变成了0,主要就是看新增节点是放在了b,还是放在了c下面。

直接给出最后旋转成功的图:

唯一的区别就是插入的位置不同,导致最后的平衡因子的不同。

情况四、左右单旋

与情况三类似,此处只是先进行左旋转,然后再进行右旋转。

简单例子:

抽象例子:

PS:不管是左右单旋还是右左单旋在第一个旋转的时候都是用父节点的孩子来作为传递值进行旋转。

代码如下:

	void RotateLR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;RotateL(subL);RotateR(parent);//最后就是更新平衡因子了if (bf == -1){parent->_bf = 1;subL->_bf = 0;subLR->_bf = 0;}else if (bf == 1){parent->_bf = 0;subL->_bf = -1;subLR->_bf = 0;}else{parent->_bf = 0;subL->_bf = 0;subLR->_bf = 0;}}

左右单旋的平衡因子的更新和上面左右单旋是一样的,就不做过多的解释了。有兴趣的小伙伴可以根据下图自己去推导推导

 6.AVL树的验证

6.1验证有序

最重要的插入节点部分完成了,不过在验证是否符合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 a[] = { 16,3,7,11,9,26,18,14,15 };

输出:3,7,9,11,14,15,16,18,26

有序的话就代表其符合二叉搜索树的性质

6.2验证是否平衡

验证是否平衡也很好判断,只需要判断左右子树的高度的差不超过1即可

在这里千万要注意,不能直接使用平衡因子来判断是否平衡,因为平衡因子也是可以进行修改的,只需要在最后面退出的时候,修改某一个值的平衡因子,然后一棵是平衡的二叉树就有可能因为平衡因子的修改,被误判成了不是平衡的。

通过递归来判断是否是一个平衡的二叉树:

代码如下:

bool IsBalance()
{if (_IsAVLTree(_root)) return true;else return false;
}
bool _IsAVLTree(Node* root)
{if (root == nullptr) return true;int left = IsHeight(root->_left);int right = IsHeight(root->_right);if (right - left != root->_bf){cout << root->_kv.first << "平衡因子异常" << endl;return false;}return abs(left - right) < 1 && _IsAVLTree(root->_left) && _IsAVLTree(root->_right);
}
int IsHeight(Node* root)
{if (root == nullptr) return 0;int lefth = IsHeight(root->_left);int righth = IsHeight(root->_right);return lefth > righth ? lefth + 1 : righth + 1;
}

测试用例的话,大家可以自己随机生成一些值,然后进行测试。或者用我上述使用的过的例子。

7.AVL的删除

有了插入的旋转之后,AVL的删除就不应该是这里的主角了,并且在学了旋转之后,会发现删除还是比较简单的。只有三种情况:左为空,右为空,左右不为空(找左子树的最右值或者右子树的最左值来进行替换)。删除之后再判断是否需要进行旋转即可。

感兴趣的可以阅读这篇文章,这里面详细介绍了二叉搜索树的删除

[c++高阶]二叉搜索树深度剖析-CSDN博客

8.总结与拓展

AVL树是一棵高度严格平衡的二叉树,这样的二叉树对于查找来说效率就是O(logN),但是 我们需要进行多次的旋转来保持它的平衡,这样在一定程度上就会影响效率,尤其是在删除的时候,删除了某一个值,可能一直要旋转,旋转到根的位置。这样就极大地影响它的效率,因此有没有更好的办法呢?有,红黑树。后续文章中会详细介绍红黑树。

AVL树的插入代码还是很难写的,有很多的情况要进行考虑,希望各位小伙伴能够多理解几遍然后再动手去写,一定要边写变画图。

行文至此,AVL树就讲解完毕了,要是有不对的地方,还请大家批评指正。

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

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

相关文章

我在命令行下剪辑视频

是的&#xff0c;你不需要格式工厂&#xff0c;你也不需要会声会影&#xff0c;更不需要爱剪辑这些莫名其妙的流氓软件&#xff0c;命令行下视频处理&#xff0c;包括剪辑&#xff0c;转码&#xff0c;提取&#xff0c;合成&#xff0c;缩放&#xff0c;字幕&#xff0c;特效等…

Tita:什么是 360 评估?

360 评估是一个专业的反馈机会&#xff0c;使一组同事和经理能够提供有关同事绩效的反馈。与仅由其经理评估员工工作绩效的典型员工绩效评估不同&#xff0c;360 评估会考虑来自同事和报告员工的反馈&#xff0c;甚至包括客户和与员工互动的其他人。 Tita&#xff1a;什么是 3…

jenkins ssh 免密报错Host key verification failed.

jenkins 发布项目&#xff0c;ssh连接远程服务器时报错&#xff1a;Host key verification failed. 解决&#xff1a; 原因是生成的sshkey不是用的jenkins用户&#xff0c;所以切换用户到&#xff1a;jenkins重新生成sshkey su jenkins ssh-keygen -t rsa ssh-copy-id -i ~/…

【Linux第七课--基础IO】内存级文件、重定向、缓冲区、文件系统、动态库静态库

目录 引入内存级文件重新使用C文件接口 -- 对比重定向写文件读文件文件流 认识文件操作的系统接口open参数 -- flagflag的内容宏的传参方式 open关闭文件写文件读文件结论 引入文件描述符fd、对文件的理解理解一切皆文件方法集文件fd的分配规则 重定向代码的重定向输入重定向输…

创意设计的起点:十大网页设计模板网站

对于网页设计领域的专业人士和爱好者而言&#xff0c;从零开始构建一个网页可能会耗费大量的时间和劳力。幸运的是&#xff0c;我们可以通过使用现成的网页模板来提升工作效率并节省宝贵的时间。一个好的模板不仅能提高设计效率&#xff0c;还能激发出卓越的创意灵感。因此&…

鸿蒙Harmony-矩形绘制组件Rect使用详解

目录 一&#xff0c;定义 二&#xff0c;绘制自定义图形 三&#xff0c;作为其他控件背景使用 一&#xff0c;定义 Rect是鸿蒙提供的矩形绘制组件&#xff0c;利用该组件可以绘制矩形背景&#xff0c;矩形图案等 官方提供的参数和属性&#xff1a; 参数&#xff1a; 参数名…

netty之bootstrap源码分析

写在前面 本文看下bootstrap类。 1&#xff1a;正文 1.1&#xff1a;干啥的&#xff1f; 在进行netty编程的时候都是先创建一个bootstrap&#xff0c;然后设置很多的东西&#xff0c;如下代码&#xff08;服务端启动代码&#xff09;&#xff1a; ServerBootstrap b new …

c# WinForm弹出窗体时不获取焦点方法

WinForm开发的软件有时候需要在屏幕右下角弹窗进行一些提示&#xff0c;通常使用new MyForm().Show()即可实现此需求。 但是当MyForm显示出来时&#xff0c;会抢走原本窗体上的光标&#xff0c;导致原本在软件上比如打字或者其他操作被中断&#xff0c;非常不人性化&#xff0…

方差和标准差哪些事儿

1.方差 在概率论与数理统计中&#xff0c;方差用来度量随机变量和其数学期望&#xff08;即均值&#xff09;之间的偏离程度。方差是各个数据与平均数之差的平方和的平均数,即: s(1/n)[(x1-x_)^2 (x2-x_)^2 …(xn-x_)^2] 其中&#xff0c;x_表示样本的平均数&#xff0c;n表示…

Hudi Upsert原理

1. 前言 如果要深入了解Apache Hudi技术的应用或是性能调优&#xff0c;那么明白源码中的原理对我们会有很大的帮助。Upsert是Apache Hudi的核心功能之一&#xff0c;主要完成增量数据在HDFS/对象存储上的修改&#xff0c;并可以支持事务。而在Hive中修改数据需要重新分区或重…

了解SQLExpress数据库

SQLExpress&#xff08;Microsoft SQL Server Express&#xff09;是由微软公司开发的一款免费且轻量级的数据库管理系统。以下是关于SQLExpress的详细解释&#xff1a; 一、定义与特点 定义&#xff1a; SQLExpress是Microsoft SQL Server的一个缩减版或基础版&#xff0c;旨在…

空天地遥感数据识别与计算

在科技飞速发展的时代&#xff0c;遥感数据的精准分析已经成为推动各行业智能决策的关键工具。从无人机监测农田到卫星数据支持气候研究&#xff0c;空天地遥感数据正以前所未有的方式为科研和商业带来深刻变革。然而&#xff0c;对于许多专业人士而言&#xff0c;如何高效地处…

JavaEE-多线程初阶(2)

目录 1.创建线程的五种写法 1.1 继承Thread类 1.2 实现Runnable接口 1.3 使用匿名内部类 1.4 使用Runnable&#xff0c;匿名内部类 1.5 引入lambda表达式 2.Thread类及常见方法 2.1 认识Thread 2.2 Thread的常见构造方法 2.3 Thread的几个常见属性 关于后台线程 关…

【网络安全】揭示 Web 缓存污染与欺骗漏洞

未经许可,不得转载。 文章目录 前言污染与欺骗Web 缓存污染 DoS1、HTTP 头部超大 (HHO)2、HTTP 元字符 (HMC)3、HTTP 方法覆盖攻击 (HMO)4、未键入端口5、重定向 DoS6、未键入头部7、Host 头部大小写规范化8、路径规范化9、无效头部 CP-DoS10、HTTP 请求拆分Web 缓存污染与有害…

重工业数字化转型创新实践:某国家特大型钢铁企业如何快速落地基于实时数仓的数据分析平台

使用 TapData&#xff0c;化繁为简&#xff0c;摆脱手动搭建、维护数据管道的诸多烦扰&#xff0c;轻量替代 OGG, Kettle 等同步工具&#xff0c;以及基于 Kafka 的 ETL 解决方案&#xff0c;「CDC 流处理 数据集成」组合拳&#xff0c;加速仓内数据流转&#xff0c;帮助企业…

Golang | Leetcode Golang题解之第522题最长特殊序列II

题目&#xff1a; 题解&#xff1a; func isSubseq(s, t string) bool {ptS : 0for ptT : range t {if s[ptS] t[ptT] {if ptS; ptS len(s) {return true}}}return false }func findLUSlength(strs []string) int {ans : -1 next:for i, s : range strs {for j, t : range s…

(C#面向初学者的 .NET 的生成 AI) 第 1 部分-简介

第 1 部分简介就是由Luis Quintanilla讲述本系列教程要学习哪些部分&#xff0c;基本都是介绍&#xff0c;内容不是很多。但可以先了解一下.net 生成式AI需要学习接触哪些东西。 在这个关于机器学习和AI的初学者系列中&#xff0c;Luis Quintanilla向.net开发人员介绍了基础知识…

【密码学】全同态加密基于多项式环计算的图解

全同态加密方案提供了一种惊人的能力 —— 能够在不知道数据具体内容的情况下对数据进行计算。这使得你可以在保持潜在敏感源数据私密的同时&#xff0c;得出问题的答案。 这篇文章的整体结构包括多项式环相关的数学介绍&#xff0c;基于多项式环的加密和解密是如何工作的&…

Spring Boot中解决BeanDefinitionStoreException问题的实战分享

目录 前言1. 问题背景2. 问题分析2.1 异常分析2.2 常见的错误原因2.3 排查过程 3. 解决方案3.1 清理缓存和重建项目3.1.1 清理IDEA缓存3.1.2 使用Maven清理并重建项目 3.2 升级Maven版本3.2.1 下载最新Maven版本3.2.2 IDEA配置新的Maven版本3.2.3 清理缓存并重新构建 3.3 验证问…

Java避坑案例 - 线程池未复用引发的故障复盘及源码分析

文章目录 问题现象问题定位问题代码根因分析现象剖析newCachedThreadPool 源码SynchronousQueue特点构造方法主要方法应用场景Code Demo运行结果 问题修复 问题现象 时不时有报警提示线程数过多&#xff0c;超过2000 个&#xff0c;收到报警后查看监控发现&#xff0c;瞬时线程…