一篇搞定AVL树+旋转【附图详解旋转思想】

🎉个人名片:

🐼作者简介:一名乐于分享在学习道路上收获的大二在校生
🙈个人主页🎉:GOTXX
🐼个人WeChat:ILXOXVJE
🐼本文由GOTXX原创,首发CSDN🎉🎉🎉
🐵系列专栏:零基础学习C语言----- 数据结构的学习之路----C++的学习之路
🐓每日一句:如果没有特别幸运,那就请特别努力!🎉🎉🎉
————————————————

一.AVL 树

1.1 AVL树的概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。(上章提过)
所以就有人发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

AVL树的条件:
一棵AVL树可以是空树,是具有以下性质的二叉搜索树:
1.它的左右子树都是AVL树
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)。

1.2 AVL树节点的定义

                                //树节点的定义
template<class K,class V>
struct AVLNode
{ AVLNode<K, V>* _left;        //存储左节点AVLNode<K, V>* _right;       //存储右节点AVLNode<K, V>* _parent;      //存储父亲pair<K,V> _kv;               //存储数据int _bl;                     //平衡因子AVLNode(pair<K, V>& kv)      //构造函数:_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _bl(0)     {}
};

1.3 AVL树的插入

AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:

  1. 按照二叉搜索树的方式插入新节点
  2. 调整节点的平衡因子

平衡因子的处理方法:

Cur插入后,Parent的平衡因子一定需要调整,在插入之前,Parent 的平衡因子分为三种情况:-1,0, 1, 分以下两种情况:

  1. 如果Cur插入到Parent的左侧,只需给Parent的平衡因子-1即可
  2. 如果Cur插入到Parent的右侧,只需给Parent的平衡因子+1即可

此时:Parent的平衡因子可能有三种情况:0,正负1, 正负2

  1. 如果Parent的平衡因子为0,说明插入之前Parent的平衡因子为正负1,插入后被调整成0,此时满足 AVL树的性质,插入成功。
  2. 如果Parent的平衡因子为正负1,说明插入前Parent的平衡因子一定为0,插入后被更新成正负1,此时以Parent为根的树的高度增加,需要继续向上更新。
  3. 如果Parent的平衡因子为正负2,则Parent的平衡因子违反平衡树的性质,需要对其进行旋转处理。

【旋转代码在下面讲解后】
实现代码:

bool insert(pair<K,V>& kv)
{if (_root == nullptr){_root = new Node(kv);}//找插入位置Node* parent = nullptr;   //记录插入位置的父亲,方便插入后链接Node* cur = _root;while (cur){if (cur->_kv > kv){parent = cur;cur = cur->_left;}else if (cur->_kv < kv){parent = cur;cur = cur->_right;}else{return false;}}//到这里就说明找到插入位置了,下面就开始插入了//判断插在父亲的左边还是右边if (cur == parent->left)             {cur = new Node(kv);      parent->_left = cur;cur->_parent = parent;cur->_parent->_bl--;       //如果是在左边插入,则--平衡因子}else if (cur == parent->right){cur = new Node(kv);parent->_right = cur;cur->_parent = parent;cur->_parent->_bl++;      //如果是在右边插入,则++平衡因子}//调节上面节点的平衡值while (parent){//情况1:插入节点后父亲的平衡因子改变if (parent->_bl == 1 || parent->_bl == -1){Node* grandfather = parent->_parent;if (parent = grandfather->_left){grandfather->_bl--;parent = grandfather;}else{grandfather->_bl++;parent = grandfather;}}//情况二:插入节点后父亲的平衡因子不改变else if (parent->_bl == 0){break;}//情况三:父亲的平衡因子已经不满足AVL树的条件//需要旋转处理else if (){/下面的旋转代码后面讲解后贴出//左单旋if (){}//右单旋else if (){}//左右双旋else if (){}//右左双旋else if (){}}}
}

1.4AVL树的旋转

如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:
单旋分为:左单旋与右单旋
双旋分为:左右双旋与右左双旋

单旋思想

如图
在这里插入图片描述
如上图,上面的左右单旋,要么是在parent的左子树高,并且左子树中左边高(左左高)
要么是parent的右子树高,并且右子树中右边高(右右高),这种及只需要旋转一边就可以解决不平衡的问题,哪边高,就往另一边旋转即可。

1.4.1左单旋

使用场景:
新节点插入较高右子树的右侧—右右:左单旋。

如图:长方形代表高度为h的子树。

在这里插入图片描述
具体例子:
在这里插入图片描述

解析:

含义解析:

pNodeR = parent->_right
pNodeRL = pNodeR->_left

思想解析:
如上图,右单旋是让pNodeRL节点成为parent的右孩子,然后parent自己变为pNodeR的左孩子,pNodeR变成这个子树的根。

平衡因子的调节:
单旋后pNodeR与parent的平衡因子都变为0;

注意:
在旋转过程中,有以下几种情况需要考虑:

  1. 50节点的左孩子可能存在,也可能不存在。
  2. 25可能是根节点,也可能是子树
    如果是根节点,旋转完成后,要更新根节点。
    如果是子树,可能是某个节点的左子树,也可能是右子树。

实现代码

//左单旋
void rotateL(Node* parent)
{Node* pparent = parent->_parent;  //记录所旋转根节点的父亲Node* pNodeR = parent->_right;   Node* pNodeRL = pNodeR->_left;if (pNodeRL)                //如果该旋转节点的右节点的左孩子存在parent->_right = pNodeRL;pNodeR->_left = parent;  //新的父节点的链接if (parent == _root)       //若parent是根节点{_root = pNodeR;pparent = nullptr;     }else                     //parent不是根节点{if (pparent->_left == parent){pparent->_left = pNodeR;}else{pparent->_right = pNodeR;}}pNodeR->_bl = 0;parent->_bl = 0;
}
1.4.2右单旋

使用场景:
新节点插入较高左子树的左侧—左左:右单旋

如图
在这里插入图片描述
具体例子:
在这里插入图片描述

解析:

含义解析

pNodeL = parent->_Left
pNodeLR = pNodeL->_right

思想解析

如上图,右单旋是让pNodeLR节点变为parent的左孩子,然后parent自己变为pNodeL的右孩子,pNodeL变成这个子树的根。

平衡因子的调节
单旋后pNodeL与parent的平衡因子都变为0;

在旋转过程中,有以下几种情况需要考虑:

  1. 5节点的右孩子可能存在,也可能不存在。
  2. 8可能是根节点,也可能是子树
    如果是根节点,旋转完成后,要更新根节点。
    如果是子树,可能是某个节点的左子树,也可能是右子树。

代码实现

/右单旋void rotateR(Node* parent){Node* pparent = parent->_parent;Node* pNodeL = parent->_left;Node* pNodeLR = pNodeL->_right;if (pNodeLR)parent->_left = pNodeLR;pNodeL->_right = parent;if (parent == _root){_root = pNodeL;pparent = nullptr;}else{if (pparent->_left == parent){pparent->_left = pNodeL;}else{pparent->_right = pNodeL;}}pNodeL->_bl = 0;parent->_bl = 0;}
双旋思想

如图
在这里插入图片描述
如上图,如果parent的左子树高,并且左子树中的右子树高(左右高),或则是parent的右子树高,并且右子树的左子树高(右左高),则旋转一次不能解决问题,所以就有了双旋的思想。

1.4.3左右单双旋

使用场景:

新节点插入较高左子树的右侧—左右:先左单旋再右单旋

如图
在这里插入图片描述
具体例子:
在这里插入图片描述
解析:
因为是parent的左子树中的右子树高,所以只需要先将parent的左子树进行左旋,将parent的左子树变为左边高,则旋转后parent整个树就变为了左左高,再用上面单旋的思想,parent以旋转点进行右旋即可;

else if (parent->_bl == -2 && parent->_left->_bl == 1)
{int bl = parent->_left->_right->_bl;Node* pNodeL = parent->_left;Node* pNodeLR = pNodeL->_right;rotateL(pNodeL);     //左旋转rotateR(parent);    //右旋转if (-1 == bl)      //分情况调节平衡因子{pNodeLR->_bl = 0;pNodeL->_bl = 0;parent->_bl = 1;}else if (1 == bl){pNodeLR->_bl = 0;parent->_bl = 0;pNodeL->_bl = -1;}else{pNodeLR->_bl = 0;parent->_bl = 0;pNodeL->_bl = 0;}
}
1.4.4右左单旋

使用场景:
新节点插入较高右子树的左侧—右左:先右单旋再左单旋

如图
在这里插入图片描述
具体例子:
在这里插入图片描述

解析:

因为是parent的右子树中的左子树高,所以只需要先将parent的右子树进行右旋,将parent的右子树变为右边高,则旋转后parent整个树就变为了右右高,再用上面单旋的思想,parent以旋转点进行左旋即可;

代码实现

else if (parent->_bl == 2 && parent->_right->_bl == -1)
{Node* pNodeR = parent->_right;Node* pNodeRL = pNodeR->_left;int bl = pNodeRL->_bl;rotateR(pNodeR);   //先右旋rotateL(parent);   //再左旋pNodeRL->_bl = 0;    //分情况调节平衡因子if (1 == bl){parent->_bl = -1;pNodeR->_bl = 0;}else if (-1 == bl){parent->_bl = 0;pNodeR->_bl = 1;}else{parent->_bl = 0;pNodeR = 0;}
}
双旋后平衡因子的调节

我们从结果来看,忽略过程,从图中可以得到
在这里插入图片描述
解析:
实际上就是将60的左孩子给了30的右孩子,把60的有孩子给了90的左孩子。
所以可以得出:
平衡因子的改变与60的平衡因子有关(与它的左右孩子有关)。
情况分为3种:60的平衡因子为(1,0,-1)

下图解为:右左双旋
当为1时:
在这里插入图片描述
结论:

pNodeRL的平衡因子为0
parent–>-1
pNodeR–>0

当为0时:
在这里插入图片描述
结论:

pNodeRL的平衡因子为0
parent–>0
pNodeR–>0

当为-1时:
在这里插入图片描述
结论:

pNodeRL的平衡因子为0
parent–>0
pNodeR–>1

左右双旋的图解
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

旋转总结:

假如以Parent为根的子树不平衡,即Parent的平衡因子为2或者-2
分以下情况考虑:

  1. Parent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pNodeR 当pNodeR的平衡因子为1时,执行左单旋 当pNodeR的平衡因子为-1时,执行右左双旋
  2. Parent的平衡因子为-2,说明Parent的左子树高,设Parent的左子树的根为pNodeL 当pNodeL的平衡因子为-1是,执行右单旋 当pNodeL的平衡因子为1时,执行左右双旋
    旋转完成后,原Parent为根的子树个高度降低,已经平衡,不需要再向上更新。

1.5 AVL树的验证

AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:

  1. 验证其为二叉搜索树
    如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
  2. 验证其为平衡树
    每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
    节点的平衡因子是否计算正确。

代码实现
方法一:

	int Hight(Node* root)    //计算该节点的高度{if (root == nullptr){return 0;}int Hightleft = Hight(root->_left);int Hightright = Hight(root->right);return Hightleft > Hightright ? Hightleft + 1 : Hightright + 1;  //返回左右子树高的那一个}bool _isbalance(Node* root){if(root==nullptr){return true;}int hightleft = Hight(root->_left);    //计算左右子树的高度int hightright = Hight(root->_right);if (abs(hightright - hightleft) >= 2)   //判断高度差{return flase;}if (hightright - hightleft !=root->_bl)    //判断计算结果是否与该节点的平衡因子相等{cout << root->_kv->first<<':' << "异常" << endl;return false;}return isbalance(root->_left) && isblance(root->_right);  //递归}

方法一有大量的重复计算(每一个节点都需要重新计算高度)
方法二更优

方法二:

	bool _isbalance(Node* root,int& height)   //height记录高度{if (root == nullptr){height = 0;return true;}if (!isbalance(root->_left,height) || !isblance(root->_right,height)){return false;}int heightleft = 0;int heightright = 0;if (abs(heightright - heightleft) >= 2)    //如果高度差超过1,则不平衡,返回false{return false;}if (heightright - heightleft != root->_bl)   //检查该节点的平衡因子是否正确{cout << root->_kv->first << ':' << "异常" << endl;return false;}height = heightleft > heightright ? heightleft + 1 : heightright + 1;   //计算height的值return true;}bool isbalance(){return _isbalance(_root);}

1.6 AVL树的性能分析

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

本章完~

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

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

相关文章

【Effective Web】页面优化

页面优化 页面渲染流程 JavaScript 》 Style 》 Layout 》 Paint 》 Composite 首先js做了一些逻辑&#xff0c;触发了样式变化&#xff0c;style计算好这些变化后&#xff0c;把影响的dom元素进行重新布局&#xff08;layout&#xff09;,再画到画布中&#xff08;Paint&am…

半导体工艺技术

完整内容点击&#xff1a;【半导体工艺技术】

将jupyter notebook文件导出为pdf(简单有效)

1.打开jupyter notebook笔记&#xff1a; 2.点击file->print Preview 3.在新打开的页面右键打印 4.另存为PDF 5.保存即可 6.pdf效果 &#xff08;可能有少部分图片显示不了&#xff09; 网上也有其他方法&#xff0c;比如将其转换为.tex再转为PDF等&#xff0c;但个人觉…

ubuntu 中安装docker

1 资源地址 进入ubuntu官网下载Ubuntu23.04的版本的镜像 2 安装ubuntu 这里选择再Vmware上安装Ubuntu23.04.6 创建一个虚拟机&#xff0c;下一步下一步 注意虚拟机配置网络桥接&#xff0c;CD/DVD选择本地的镜像地址 开启此虚拟机&#xff0c;下一步下一步等待镜像安装。 3…

数据可视化-ECharts Html项目实战(8)

在之前的文章中&#xff0c;我们学习了如何设置散点图涟漪效果与仪表盘动态指针效果。想了解的朋友可以查看这篇文章。同时&#xff0c;希望我的文章能帮助到你&#xff0c;如果觉得我的文章写的不错&#xff0c;请留下你宝贵的点赞&#xff0c;谢谢 今天的文章&#xff0c;会…

【c++】类和对象(六)深入了解隐式类型转换

&#x1f525;个人主页&#xff1a;Quitecoder &#x1f525;专栏&#xff1a;c笔记仓 朋友们大家好&#xff0c;本篇文章我们来到初始化列表&#xff0c;隐式类型转换以及explicit的内容 目录 1.初始化列表1.1构造函数体赋值1.2初始化列表1.2.1隐式类型转换与复制初始化 1.3e…

python基础——文件操作【文件编码、文件的打开与关闭操作、文件读写操作】

&#x1f4dd;前言&#xff1a; 这篇文章主要讲解一下python中对于文件的基础操作&#xff1a; 1&#xff0c;文件编码 2&#xff0c;文件的打开与关闭操作 3&#xff0c;文件读写操作 &#x1f3ac;个人简介&#xff1a;努力学习ing &#x1f4cb;个人专栏&#xff1a;C语言入…

04 | Swoole 源码分析之 epoll 多路复用模块

首发原文链接&#xff1a;Swoole 源码分析之 epoll 多路复用模块 大家好&#xff0c;我是码农先森。 引言 在传统的IO模型中&#xff0c;每个IO操作都需要创建一个单独的线程或进程来处理&#xff0c;这样的操作会导致系统资源的大量消耗和管理开销。 而IO多路复用技术通过…

OceanBase OBCA 数据库认证专员考证视频

培训概述 OceanBase 认证是 OceanBase 官方推出的唯一人才能力认证体系&#xff0c;代表了阿里巴巴及蚂蚁集团官方对考生关于 OceanBase 技术能力的认可&#xff0c;旨在帮助考生更好地学习 OceanBase 数据库产品&#xff0c;早日融入 OceanBase 技术生态体系&#xff0c;通过由…

MYSQL——索引概念索引结构

索引 索引是帮助数据库高效获取数据的排好序的数据结构。 有无索引时&#xff0c;查询的区别 主要区别在于查询速度和系统资源的消耗。 查询速度&#xff1a; 在没有索引的情况下&#xff0c;数据库需要对表中的所有记录进行扫描&#xff0c;以找到符合查询条件的记录&#…

Doris实践——票务平台的实时数仓建设

目录 前言 一、引入 Doris原因 二、基于Doris搭建数据平台 2.1 构建实时数仓 2.2 Flink CDC全库同步 三、基于Doris进行OLAP报表开发 四、未来规划 原文大佬介绍的这篇票务平台的实时数仓建设有借鉴意义&#xff0c;现摘抄下来用作沉淀学习。如有侵权&#xff0c;请告知…

web前端面试题----->VUE

Vue的数据双向绑定是通过Vue的响应式系统实现的。具体原理&#xff1a; 1. Vue会在初始化时对数据对象进行遍历&#xff0c;使用Object.defineProperty方法将每个属性转化为getter、setter。这样在访问或修改数据时&#xff0c;Vue能够监听到数据的变化。 2. 当数据发生变化时…

Java学习31-Java 多线程Thread 线程的创建

多线程的概念&#xff1a; 用户想要一边听歌&#xff0c;一边QQ聊天&#xff0c;一边游戏。要求能并发执行。 program程序&#xff1a; 有特殊功能的一组代码process进程&#xff1a; 正在执行中的program&#xff0c;或者程序program的一次执行过程thread线程&#xff1a;程…

CSS实现元素边框渐变动画

前言&#xff1a; 边框流动动画是一种非常常见的效果&#xff0c;能够让网页看起来更加生动有趣。通过使用 CSS3&#xff0c;我们可以轻松地实现这种动画效果。本文将介绍如何使用 CSS3 实现边框流动效果&#xff0c;下面一起来看看吧。 示例图&#xff1a;边框是动画持续变化的…

前端简历:大学还没毕业,就写2年工作经验,上的啥大学呢?

我看过很多前端简历&#xff0c;不知道是不是被所谓简历专家和面试专家给拐带的&#xff0c;还没毕业就敢写2年工作经验&#xff0c;如果属实&#xff0c;大学上了个啥&#xff1f; 简历每天都能收到几十上百封&#xff0c;对于初级人才招聘&#xff0c;基本上看以下几点。 1、…

CIM搭建实现发送消息的效果

目录 背景过程1、下载代码2、进行配置3、直接启动项目4、打开管理界面5、启动web客户端实例项目6、发送消息 项目使用总结 背景 公司项目有许多需要发送即时消息的场景&#xff0c;之前一直采用的是传统的websocket连接&#xff0c;它会存在掉线严重&#xff0c;不可重连&…

深入理解数据结构(3):栈和队列详解

文章主题&#xff1a;顺序表和链表详解&#x1f331;所属专栏&#xff1a;深入理解数据结构&#x1f4d8;作者简介&#xff1a;更新有关深入理解数据结构知识的博主一枚&#xff0c;记录分享自己对数据结构的深入解读。&#x1f604;个人主页&#xff1a;[₽]的个人主页&#x…

瑞吉外卖实战学习--7、员工信息分页查询

员工信息分页查询 前言1、配置公共分页组件2、创建接口并查看接收的参数3、使用分页构造器并添加查询条件和排序条件4、测试结果 前言 1、配置公共分页组件 package com.example.ruiji_demo.config;import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;…

sharemore少数派提示词或许需要工程,但提问一定需要智慧

Matrix 首页推荐 Matrix 是少数派的写作社区&#xff0c;我们主张分享真实的产品体验&#xff0c;有实用价值的经验与思考。我们会不定期挑选 Matrix 最优质的文章&#xff0c;展示来自用户的最真实的体验和观点。 文章代表作者个人观点&#xff0c;少数派仅对标题和排版略作…

代码随想录算法训练营第二十五天| 216.组合总和III,17.电话号码的字母组合

题目与题解 216.组合总和III 题目链接&#xff1a;216.组合总和III 代码随想录题解&#xff1a;216.组合总和III 视频讲解&#xff1a;和组合问题有啥区别&#xff1f;回溯算法如何剪枝&#xff1f;| LeetCode&#xff1a;216.组合总和III_哔哩哔哩_bilibili 解题思路&#xf…