【数据结构】AVL树相关知识详细梳理

1. AVL树的概念

        AVL的全称是Adelson-Velsky-Landis,其名称来源于其发明者Adelson、Velsky和Landis,

平衡二叉树搜索树

        它的出现是由于二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。这个解决办法就是AVL树。


        AVL树是具有以下性质的二叉搜索树:

        1. 它的左右子树都是AVL树。
        2. 左右子树高度之差(简称平衡因子)的绝对值不超过1。

 

        AVL树是高度平衡的二叉搜索树如果它有n个结点,其高度可保持在log_2N,搜索的时间复杂度O(log_2N)。并且克服了普通二叉搜索树可能退化,导致搜索效率大大降低的缺点。

2. AVL树原理
        

2.1 节点结构

        AVL树是通过对节点进行调整来控制树的高度以达到两端平衡,那么它是如何调整节点的呢?

        当插入节点打破了AVL树的规则----左右子树高度之差的绝对值超过1后,就会对节点进行旋转来降低子树的高度,来达到左右子树的相对平衡,避免树结构退化。

        为了方便对节点进行调整和检测,我们引入平衡因子的概念,即在每个树节点中增加一个int类型的变量来记录左右子树的高度差(这里是右子树高度 减 左子树高度),这样一来,通过分析平衡因子的大小,我们就可以判断节点是否需要旋转处理。

        显然,平衡因子的更新很多时候是牵一发而动全身的,例如:

        因此,通过平衡因子维护树结构的平衡既带来了便利,又带来了麻烦,我们往往需要从插入节点开始向上不断更新平衡因子,为了解决二叉树在向上遍历时的麻烦,我这里将节点设置为三叉,即一个节点同时拥有 父节点,左子树节点,右子树节点的指针。 

 

2.2 更新平衡因子

        在对节点进行调整前,首先要维护平衡因子,每次插入或旋转节点后,都要对相关平衡因子进行更新,旋转操作也是当发现平衡因子值异常(绝对值大于1)时才执行的。

        首先,二叉搜索树的插入节点一定是叶节点,插入节点为父节点的左子树的时候,父节点的平衡因子 -1,插入节点为父节点的右子树时,父节点的平衡因子 +1。

        然后,为了向上不断更新平衡因子,我们需要总结平衡因子更新的规律:

        1. 更新后的平衡因子为 1 或 -1(说明插入节点前,父节点的平衡因子为0),说明子树的高度变高,需要继续向上更新平衡因子。

        2. 更新后的平衡因子为 0(说明插入节点前,父节点的平衡因子为-1或1,插入节点在较矮的树那边),说明子树的高度不变,不需要再向上更新平衡因子了。

        3. 更新后的平衡因子为2 或 -2(说明插入节点前,父节点的平衡因子为-1或1,且插入节点在较高的子树那边),此时破坏了平衡规则,需要进行旋转调整。

        情况1:

       

        情况2: 

        情况3: 

 

2.3 旋转 (插入)

        二叉平衡搜索树通过旋转操作来改变节点之间的连接关系以降低数的高度,保持平衡,同时不破坏搜索树的规则。那么是如何旋转的呢?

        首先,旋转分为四种:

        1. 右单旋。

        2. 左单旋。

        3. 右左双旋。

        4. 左右双旋。

        下面通过概括图来分别描述这几种旋转对应的情况,以及如何完成旋转:

        右单旋:

        从图中可以看出,当根节点平衡因子为-2(此时左子树必定存在),且其左子树平衡因子为-1时,要进行右单旋,调整完毕后,subL和pRoot的平衡因子皆更新为0,此时子树的高度等于插入前的高度,不需要再向上更新。

        左单旋:

        类似于右单旋,当根节点平衡因子为2(此时右子树必定存在),且其左子树平衡因子为1时,要进行左单旋,调整完毕后,subL和pRoot的平衡因子皆更新为0,此时子树的高度等于插入前的高度,不需要再向上更新。 

        右左双旋:

        左右双旋:

        和右左双旋是镜像的操作,这里不再详细说明。 

        看到这里,想必你以及懂得了什么叫做旋转,也就是把下面的节点“转”到上面来,把上面的“转”下去,以到达平衡左右子树,缩小左右子树高度差的效果。

        2.4 删除

                因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,最差情况下一直要调整到根节点的位置,原理比插入更加繁琐,但也是基于旋转的原理之上的,我们只理解插入时的情况就够用了,感兴趣可以自行了解AVL树删除的实现原理。
 

3. AVL树结构模拟实现

        总体结构:

template<class T>//AVLTree节点
struct AVLTreeNode
{AVLTreeNode(const T& data = T()): _pLeft(nullptr), _pRight(nullptr), _pParent(nullptr), _data(data), _bf(0){}//使用三叉结构方便后续更新平衡因子AVLTreeNode <T>* _pLeft;//左节点指针AVLTreeNode <T>* _pRight;//右节点指针AVLTreeNode <T>* _pParent;//父节点指针T _data;int _bf; //节点的平衡因子
};//AVL: 二叉搜索树 + 平衡因子的限制
template<class T>
class AVLTree
{typedef AVLTreeNode<T> Node;
public:AVLTree(): _pRoot(nullptr){}//在AVL树中插入值为data的节点bool Insert(const T& data);//AVL树的验证bool IsAVLTree(){return _IsAVLTree(_pRoot);}//AVL树的遍历//void Inorder()//{//	return _Inorder(_pRoot);//}private://AVL树的遍历//void _Inorder(Node* pRoot);//根据AVL树的概念验证pRoot是否为有效的AVL树bool _IsAVLTree(Node* pRoot);//获取树高度size_t _Height(Node* pRoot);//右单旋void RotateR(Node* pParent);//左单旋void RotateL(Node* pParent);//右左双旋void RotateRL(Node* pParent);//左右双旋void RotateLR(Node* pParent);Node* _pRoot;//根节点
};

 获取树高:

//获取树高度
template<class T>
size_t AVLTree<T>::_Height(Node* pRoot)
{if (pRoot == nullptr)return 0;size_t leftsize = _Height(pRoot->_pLeft)+1;size_t rightsize = _Height(pRoot->_pRight)+1;return leftsize >= rightsize ? leftsize : rightsize;
}

插入节点:

template<class T>
bool AVLTree<T>::Insert(const T& data)
{//树为空,直接插入if (_pRoot == nullptr){_pRoot = new Node(data);return true;}//根据比较规则找到插入位置Node* pcur = _pRoot;Node* parent = nullptr;while (pcur){if (data == pcur->_data)return false;parent = pcur;if (data > pcur->_data)pcur = pcur->_pRight;elsepcur = pcur->_pLeft;}//直接插入并更新平衡因子if (data > parent->_data){pcur = new Node(data);parent->_pRight = pcur;pcur->_pParent = parent;parent->_bf += 1;}else{pcur = new Node(data);parent->_pLeft = pcur;pcur->_pParent = parent;parent->_bf -= 1;}while (parent){// 如果插入位置子树的根平衡因子为0,则子树高度不变,不需要向上更新if (parent->_bf == 0){//插入完成,返回真return true;}// 违反avl树规则,需要旋转if (parent->_bf == 2){//右子树平衡因子为1,需要左单旋if (parent->_pRight->_bf == 1){RotateL(parent);//旋转后子树高度不变,不需要继续向上更新return true;}//右子树平衡因子为-1,需要右左双旋else if (parent->_pRight->_bf == -1){RotateRL(parent);//双旋后子树高度不变,不需要再向上更新return true;}}// 违反AVL树规则,需要旋转if (parent->_bf == -2){//左子树平衡因子为-1,需要右单旋if (parent->_pLeft->_bf == -1){RotateR(parent);//旋转后子树高度不变,不需要继续向上更新return true;}//左子树平衡因子为1,需要左右双旋else if (parent->_pLeft->_bf == 1){RotateLR(parent);//双旋后子树高度不变,不需要再向上更新return true;}}// 如果插入位置子树的根平衡因子为1/-1,则子树高度增加,需要向上更新if (parent->_bf == 1 || parent->_bf == -1){//如果parent不为根节点if (parent->_pParent){if (parent->_data > parent->_pParent->_data)parent->_pParent->_bf += 1;elseparent->_pParent->_bf -= 1;}}//向上更新parent = parent->_pParent;}
}

        实现插入代码时,重点要理清插入以及旋转的逻辑,利用节点的三叉结构,循环向上更新平衡因子并进行旋转,这也是最难的部分。 

右单旋:

//右单旋
template<class T>
void AVLTree<T>::RotateR(Node* pParent)
{Node* subL = pParent->_pLeft;Node* subLR = subL->_pRight;subL->_pParent = pParent->_pParent;//如果原pParent不为根节点if (pParent->_pParent){if (subL->_data > subL->_pParent->_data)subL->_pParent->_pRight = subL;elsesubL->_pParent->_pLeft = subL;}subL->_pRight = pParent;pParent->_pParent = subL;pParent->_pLeft = subLR;//如果subLR不为空if (subLR)subLR->_pParent = pParent;//更新平衡因子pParent->_bf = 0;subL->_bf = 0;//若subL为根节点,更新根节点if (subL->_pParent == nullptr)_pRoot = subL;
}

左单旋:

//左单旋
template<class T>
void AVLTree<T>::RotateL(Node* pParent)
{Node* subR = pParent->_pRight;Node* subRL = subR->_pLeft;subR->_pParent = pParent->_pParent;//如果原pParent不为根节点if (pParent->_pParent){if (subR->_data > subR->_pParent->_data)subR->_pParent->_pRight = subR;elsesubR->_pParent->_pLeft = subR;}subR->_pLeft = pParent;pParent->_pParent = subR;pParent->_pRight = subRL;//如果subRL不为空if (subRL)subRL->_pParent = pParent;//更新平衡因子pParent->_bf = 0;subR->_bf = 0;//若subL为根节点,更新根节点if (subR->_pParent == nullptr)_pRoot = subR;
}

右左双旋:

//右左双旋
template<class T>
void AVLTree<T>::RotateRL(Node* pParent)
{Node* subR = pParent->_pRight;Node* subRL = subR->_pLeft;//记录subRL原先的平衡因子int subRLbf = subRL->_bf;//先对subR右旋RotateR(subR);//再对pParent左旋RotateL(pParent);//更新平衡因子subRL->_bf = 0;//如果subRL原先的平衡因子为-1if (subRLbf == -1){pParent->_bf = 0;subR->_bf = 1;}//如果subRL原先的平衡因子为1else if (subRLbf == 1){pParent->_bf = -1;subR->_bf = 0;}//如果subRL原先的平衡因子为0else{pParent->_bf = 0;subR->_bf = 0;}//双旋后子树高度不变,不需要再向上更新
}

 左右双旋:

//左右双旋
template<class T>
void AVLTree<T>::RotateLR(Node* pParent)
{Node* subL = pParent->_pLeft;Node* subLR = subL->_pRight;//记录subLR原先的平衡因子int subLRbf = subLR->_bf;//先对subL左旋RotateL(subL);//再对pParent右旋RotateR(pParent);//更新平衡因子subLR->_bf = 0;//如果subRL原先的平衡因子为-1if (subLRbf == -1){pParent->_bf = 1;subL->_bf = 0;}//如果subRL原先的平衡因子为1else if (subLRbf == 1){pParent->_bf = 0;subL->_bf = -1;}//如果subRL原先的平衡因子为0else{pParent->_bf = 0;subL->_bf = 0;}
}

验证用代码: 

 最后可以搭配两个验证AVL树的代码:

//AVL树的遍历(检测结果是否有序)
/*template<class T>
void AVLTree<T>::_Inorder(Node* pRoot)
{if (pRoot == nullptr)return;if (pRoot->_pLeft)_Inorder(pRoot->_pLeft);cout << pRoot->_data << ' ';if (pRoot->_pRight)_Inorder(pRoot->_pRight);
}*///根据AVL树的概念验证pRoot是否为有效的AVL树
template<class T>
bool AVLTree<T>::_IsAVLTree(Node* pRoot)
{//空树为AVL树,返回trueif(pRoot == nullptr)return true;//计算左右子树高度差int diff = _Height(pRoot->_pRight) - _Height(pRoot->_pLeft);if (diff != pRoot->_bf || (diff > 1 || diff < -1))//左右子树高度绝对值大于1,返回falsereturn false;else//继续向下检查左右子树是否是AVL树return true && _IsAVLTree(pRoot->_pRight) && _IsAVLTree(pRoot->_pLeft);
}

还可以依次插入以下节点同时画图验证正确性:

        1. {16, 3, 7, 11, 9, 26, 18, 14, 15}
        2. {4, 2, 6, 1, 3, 5, 15, 7, 16, 14}

4. 总结 

理解:

        总的来说,AVLTree实现的关键在于理解旋转原理,尤其是双旋中的不同情况。

性能:

        AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log_2N

        但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但如果一个结构还需要经常修改,就不太适合。

 

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

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

相关文章

【数据结构】栈和队列(Stack Queue)

引言 在对顺序表&#xff0c;链表有了充分的理解之后&#xff0c;现在让我们学习栈和队列&#xff01;&#xff01;&#xff01; 【链表】 &#x1f448;链表 【顺序表】&#x1f448;顺序表 目录 &#x1f4af;栈 1.栈的概念及结构 2.栈的实现 ⭐初始化栈 ⭐入栈 ⭐…

Vue引入js脚本问题记录(附解决办法)

目录 一、需求 二、import引入问题记录 三、解决方式 一、需求 我想在我的Vue项目中引入jquery.js和bootstrap.js这种脚本文件&#xff0c;但发现不能单纯的import引入&#xff0c;问题如下。 二、import引入问题记录 我直接这么引入&#xff0c;发现控制台报错TypeError: …

POI操作EXCEL增加下拉框

文章目录 POI操作EXCEL增加下拉框 POI操作EXCEL增加下拉框 有时候通过excel将数据批量导入到系统&#xff0c;而业务操作人员对于一些列不想手动输入&#xff0c;而是采用下拉框的方式来进行选择 采用隐藏sheet页的方式来进行操作 String sheetName "supplier_hidden_s…

MedPrompt:基于提示工程的医学诊断准确率优化方法

Medprompt&#xff1a;基于提示工程的医学诊断准确率优化方法 秒懂大纲解法拆解MedPrompt 提示词全流程分析总结创意视角 论文&#xff1a;Can Generalist Foundation Models Outcompete Special-Purpose Tuning? Case Study in Medicine 秒懂大纲 ├── 1 研究背景【描述背…

vue.js——“微商城”后台管理系统

1. 需求背景&#xff1a; 先创建运行环境&#xff0c;“微商城”后台管理系统是一种后台管理系统平台,旨在提供一个便捷、安全和高效的管 理和操作各类数据的平台。系统将涵盖用户登录、商品管理、分类管理、新增分类和个人中 心等功能&#xff0c;以满足用户高效数据管理的各…

一窥AI大模型奥秘:技术前沿与产业应用双轮驱动

在科技日新月异的今天&#xff0c;人工智能&#xff08;AI&#xff09;正以前所未有的速度重塑着我们的生活与工作方式。其中&#xff0c;AI大模型作为技术的最前沿&#xff0c;不仅引领着技术体系的革新&#xff0c;更是产业实践与未来趋势的关键所在。 近期&#xff0c;有幸…

P335_0334韩顺平Java_零钱通介绍

目录 P335_0334韩顺平Java_零钱通介绍代码过程编程OOP&#xff08;Object-Oriented Project&#xff09; 参考资料 P335_0334韩顺平Java_零钱通介绍 先完成显示菜单&#xff0c;并可以选择。完成零钱通明细。完成收益入账。消费。退出。 PS&#xff1a;判断时尽量用不正确的条…

甘蔗茎节检测系统源码分享

甘蔗茎节检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Vis…

【小程序】微信小程序课程 -2 快速上手

目录 1、快速上手基本概念 1.1 小程序常用组件 1.2 tabbar配置 1.3 尺寸单位 1.4 样式 1.4.1 全局样式 app.wxss 1.4.2 局部样式 xx.wxss 2、首页案例 2.1 button组件使用 2.2 swiper swiper-item 2.3 tips效果 2.4 引入矢量图 2.5 flex&#xff08;布局&#…

LeetCode: 1971. 寻找图中是否存在路径

寻找图中是否存在路径 原题 有一个具有 n 个顶点的 双向 图&#xff0c;其中每个顶点标记从 0 到 n - 1&#xff08;包含 0 和 n - 1&#xff09;。图中的边用一个二维整数数组 edges 表示&#xff0c;其中 edges[i] [ui, vi] 表示顶点 ui 和顶点 vi 之间的双向边。 每个顶点…

『功能项目』按钮的打开关闭功能【73】

本章项目成果展示 我们打开上一篇72QFrameWork制作背包界面UGUI的项目&#xff0c; 本章要做的事情是制作打开背包与修改器的打开关闭按钮 首先打开UGUICanvas复制button按钮 重命名为ReviseBtn 修改脚本&#xff1a;UIManager.cs 将修改器UI在UGUICanvas预制体中设置为隐藏 运…

Python--操作列表

1.for循环 1.1 for循环的基本语法 for variable in iterable: # 执行循环体 # 这里可以是任何有效的Python代码块这里的variable是一个变量名&#xff0c;用于在每次循环迭代时临时存储iterable中的下一个元素。 iterable是一个可迭代对象&#xff0c;比如列表&#xff08;…

YOLOv8+注意力机制+PyQt5玉米病害检测系统完整资源集合

资源包含可视化的玉米病害检测系统&#xff0c;基于最新的YOLOv8注意力机制训练的玉米病害检测模型&#xff0c;和基于PyQt5制作的可视玉米病害系统&#xff0c;包含登陆页面和检测页面&#xff0c;该系统可自动检测和识别图片或视频当中出现的七类玉米病害&#xff1a;矮花叶病…

Maya没有Arnold材质球

MAYA 没有Arnold材质球_哔哩哔哩_bilibili

软件测试基础篇

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 “尽早的介入测试&#xff0c;遇到问题的解决成本就越低” 随着软件测试技术的发展&#xff0c;测试工作由原来单一的寻找缺陷逐渐发展成为预防缺陷&#xff0c;…

2024.9.26 作业 +思维导图

一、作业 1、什么是虚函数&#xff1f;什么是纯虚函数 虚函数&#xff1a;函数前加关键字virtual&#xff0c;就定义为虚函数&#xff0c;虚函数能够被子类中相同函数名的函数重写 纯虚函数&#xff1a;把虚函数的函数体去掉然后加0&#xff1b;就能定义出一个纯虚函数。 2、基…

前端——实现时钟 附带小例子

创建日期对象 toLocaleDateString() 获取日期 console.log(date.toLocaleDateString()) toLocaleTimeString() 获取时间 console.log(date.toLocaleTimeString()) toLocaleString() 获取日期和时间 console.log(date.toLocaleString()) date.getDay() 获取星期几 周日为…

软件无线电3-微相E316和HackRF实现FM调制解调

前面介绍了基于Matlab、矢量信号器和HackRF One实现射频下的FM调制解调&#xff0c;今天分享的内容是用微相E316替代矢量信号器完成发射工作。注意本文仅用于科研和学习&#xff0c;私自搭建电台属于违法行为。 1.概述 微相E316和HackRF One实现FM调制解调测试框图如1所示&am…

【后端开发】JavaEE初阶—线程的理解和编程实现

前言&#xff1a; &#x1f31f;&#x1f31f;本期讲解多线程的知识哟~~~&#xff0c;希望能帮到屏幕前的你。 &#x1f308;上期博客在这里&#xff1a;【后端开发】JavaEE初阶——计算机是如何工作的&#xff1f;&#xff1f;&#xff1f;-CSDN博客 &#x1f308;感兴趣的小伙…

哈希表(一)

一、基础知识 哈希表的优点&#xff1a; 查找key的时间效率是O&#xff08;1&#xff09; 什么时候要用到哈希表&#xff1a; 查询元素的出现问题&#xff08;是否出现过&#xff0c;是否在集合里&#xff0c;出现次数等&#xff09; 哈希表的三种数据结构&#xff1a; 数组…