C++: AVL树(实现旋转操作)

前言

map/set容器有个共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查 找元素相当于在顺序表中搜索元素,效率低下。因此,有了AVL树。

(一)AVL树介绍

两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis

在1962年发明了一种解决上述问题的方法:

当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

(1)AVL树的概念 

一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

        -它的左右子树都是AVL树

        -左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)

  

        平衡因子=右子树的高度-左子树的高度。 

        平衡因子是一个存在于二叉树中的变量

(实现AVL树不一定需要引入平衡因子,可以计算右子树高度和左子树高度差进行平衡比较)

1.新增结点在左,parent平衡因子减减
2、新增在右,parent平衡因子加加
3、更新后parent平衡因子==0,说明parent所在的子树的高度不变,不会再影响祖先,不用再继续沿着到root的路径往上更新
4、更新后parent平衡因子=1 or -1,说明parent所在的子树的高度变化,会再影响祖先,需要继续沿着到root的路径往上更新
5、更新后parent平衡因子==2or-2,说明parent所在的子树的高度变化且不平衡,对parent所在子树进行旋转,让他平衡

 如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 log(n),搜索时间复杂度O(logN)。

 

(二)AVL树的实现

        这里我通过引入平衡因子来进行AVL树的旋转操作

(1)AVL树节点的定义

数据存放在pair中:

         pair是一种STL中的类,该类将一对不同类型的值(T1和T2)耦合在一起。单个值可以通过它的公共成员first和second来访问。

key和value:

        key是存取时进行比较大小的标准,val是key对应的值。他们是一组键值对pair{key,val},key和val可以是相同的类型,也可以是不同的。

template<class K, class V>
struct AVLTreeNode
{AVLTreeNode(const pair<K, V>& kv = pair<K,V>()): _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;    //数据存在与pair中int _bf;   // 节点的平衡因子 balance factor
};

(2)AVL树的旋转 

AVL树的旋转总结来说一共右四种情况,分别是:左单旋,右单旋,左右双旋,右左双旋。

只有当平衡因子 = 2 或者 -2 时,才需要进行旋转平衡)

        (i)左单旋(新节点插入较高右子树的右侧)

        

       当插入节点在c的节点上时,进行左旋。(不能在a或者b中,这样不是左单旋)

parent的平衡因子为2,cur的平衡因子为1。

如何进行左单旋的旋转操作?

        i.将parent的right指向curleft,curleft的父亲改为parent(需要判断curleft是否为空)。

        ii. 将parent的parent记录下来(Pparent),因为parent上面可能还有节点。

        iii. cur的left指向parent,然后将parent的parent指向cur。

        iiii. 判断Pparent是否为空,若为空则将cur的parent指向空。

                若Pparent不为空,则需要判断原先的parent是Pparent的左孩子还是右孩子,将Pparent的左孩子(或者右孩子)指向cur。

        iiiii. 最后要修改cur和 parent的 平衡因子 为 0;

        

代码如下:

	void RotateL(Node* parent){Node* cur = parent->_right;Node* curleft = cur->_left;parent->_right = curleft;if (curleft){curleft->_parent = parent;}Node* Pparent = parent->_parent;cur->_left = parent;parent->_parent = cur;if (Pparent == nullptr){cur->_parent = nullptr;}else //父亲的父亲不是空{if (Pparent->_kv.first < cur->_kv.first){Pparent->_right = cur;}else{Pparent->_left = cur;}cur->_parent = Pparent;}cur->_bf = parent->_bf = 0;}

        (ii)右单旋 (新节点插入较高左子树的左侧)

        

        当插入节点在a的节点上时,进行右旋。(不能在b或者c中,这样不是左单旋)

parent的平衡因子为-2,cur的平衡因子为-1。

如何进行右单旋的旋转操作?        

        i.将parent的left指向curright,curright的父亲改为parent(需要判断curright是否为空)。

        ii. 将parent的parent记录下来(Pparent),因为parent上面可能还有节点。

        iii. cur的right指向parent,然后将parent的parent指向cur。

        iiii. 判断Pparent是否为空,若为空则将cur的parent指向空。

                若Pparent不为空,则需要判断原先的parent是Pparent的左孩子还是右孩子,将Pparent的左孩子(或者右孩子)指向cur。

        iiiii. 最后要修改cur和 parent的 平衡因子 为 0;

 代码如下:

void RotateR(Node* parent)
{Node* cur = parent->_left;Node* curright = cur->_right;parent->_left = curright;if (curright){curright->_parent = parent;}Node* Pparent = parent->_parent;cur->_right = parent;parent->_parent = cur;if (Pparent == nullptr){cur->_parent = nullptr;}else{if (Pparent->_kv.first < cur->_kv.first){Pparent->_right = cur;}else{Pparent->_left = cur;}cur->_parent = Pparent;}cur->_bf = parent->_bf = 0;
}

        (iii) 右左双旋(新节点插入较高右子树的左侧)

        

        当插入节点在b或c的节点上时,进行右左旋。

        parent的平衡因子为2,cur的平衡因子为-1。

如何进行右左双旋的旋转操作? 

        i.对cur所在树进行右旋操作。

        ii. 再对parent所在树进行左旋操作。

    需要注意的是我的左右旋操作都会将平衡因子改为 0, 所以需要将行旋转后需要修改平衡因子:

        (1)若插入的地方为c时,即curleft的平衡因子为 1 时,由于右旋操作,已经将c连接到了cur的左孩子上,(此时cur的左右孩子高度相同)所以此时cur平衡因子为0,而b 经过左旋被连接到了parent的右孩子上(此时parent的左孩子高度比右孩子高1),此时parent的平衡因子为-1。

        (2)若插入的地方为b时,同理可知,此时parent的平衡因子为 0,cur的平衡因子为1。

 代码如下:

	void RotateRL(Node* parent){Node* cur = parent->_right;Node* curleft = cur->_left;int bf = curleft->_bf;  //记录bf的值RotateR(parent->_right);RotateL(parent);if (bf == 0){parent->_bf = 0;cur->_bf = 0;curleft->_bf = 0;}else if (bf == 1){parent->_bf = -1;cur->_bf = 0;curleft = 0;}else if (bf == -1){parent->_bf = 0;cur->_bf = 1;curleft->_bf = 0;}}

 

        (iiii) 左右双旋(新节点插入较高左子树的右侧
 


        当插入节点在b或c的节点上时,进行左右旋。

        parent的平衡因子为-2,cur的平衡因子为1。

如何进行左右双旋的旋转操作? 

        i.对cur所在树进行左旋操作。

        ii. 再对parent所在树进行右旋操作。

    需要注意的是我的左右旋操作都会将平衡因子改为 0, 所以需要将行旋转后需要修改平衡因子:

        (1)若插入的地方为c时,即curright的平衡因子为 1 时,由于左旋操作b连接到了cur的右孩子上,(此时cur的左孩子高度比右孩子高1)所以此时cur平衡因子为-1,而c 经过右旋被连接到了parent的左孩子上(此时parent的左右孩子高度相同),此时parent的平衡因子为0。

        (2)若插入的地方为b时,同理可知,此时parent的平衡因子为 1,cur的平衡因子为0。

代码如下:

        

	void RotateLR(Node* parent){Node* cur = parent->_left;Node* curright = cur->_right;int bf = curright->_bf;RotateL(parent->_left);RotateR(parent);if (bf == 0){parent->_bf = 0;cur->_bf = 0;curright = 0;}else if (bf == 1){parent->_bf = 0;cur->_bf = -1;curright = 0;}else if (bf == -1){parent->_bf = 1;cur->_bf = 0;curright->_bf = 0;}}

(3) AVL树的插入

AVL树的插入过程分为两个步骤:

                (1)与普通二叉树相同的插入操作;

                (2)调整节点的平衡因子,根据平衡因子,判断是否要进行旋转操作。

代码如下:

	bool Insert(const pair<K, V>& kv){//如果_root 等于空if (_root == nullptr){_root = new Node(kv);}Node* parent = nullptr;Node* cur = _root;//找到插入值,应该插入在哪个位置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;}}cur = new Node(kv);//将插入结点的值进行连接if (parent->_kv.first > kv.first){parent->_left = cur;}else if (parent->_kv.first < kv.first){parent->_right = cur;}cur->_parent = parent;//平衡因子,满足AVL树的性质while (parent)  //当到空时,平衡退出(根结点的父亲是空){if (cur == parent->_right){parent->_bf++;}else if (cur == parent->_left){parent->_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;}

(三) AVL树的验证

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

        1. 验证其为二叉搜索树

    如果中序遍历可得到一个有序的序列,就说明为二叉搜索树

        2. 验证其为平衡树

        每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)

        节点的平衡因子是否计算正确

// AVL树的验证
bool IsAVLTree()
{return _IsAVLTree(_root);
}size_t _Height(Node* root)
{if (root == nullptr){return 0;}int LeftHeight = _Height(root->_left);int RightHeight = _Height(root->_right);return LeftHeight > RightHeight ? LeftHeight + 1 : RightHeight + 1;
}// 根据AVL树的概念验证Root是否为有效的AVL树
bool _IsAVLTree(Node* root)
{if (root == nullptr)return true;int LeftHeight = _Height(root->_left);int RightHeight = _Height(root->_right);if (RightHeight - LeftHeight != root->_bf){cout << "平衡因子异常:" << root->_kv.first << "->" << root->_bf << endl;return false;}return abs(LeftHeight - RightHeight) <= 1 && _IsAVLTree(root->_left) && _IsAVLTree(root->_right);
}

(四)AVL树总结 

1. 基本特性

AVL树是一种严格平衡的二叉搜索树,通过维护每个节点的平衡因子(左子树高度与右子树高度之差)确保树的高度平衡。其平衡条件要求所有节点的平衡因子绝对值不超过1。

2. 时间复杂度
  • 插入、删除、查找操作:时间复杂度均为O(log⁡n)。
    由于AVL树始终保持高度平衡,树的高度严格满足h≤1.44log⁡2(n+1)h≤1.44log2​(n+1),这使得其查询效率接近最优。
  • 平衡操作:插入或删除节点后可能需要通过旋转操作恢复平衡,单次旋转的时间复杂度为O(1),但最坏情况下可能需要从插入点回溯到根节点调整平衡因子,因此平衡操作的总体时间复杂度仍为O(log⁡n)。
3. 性能优缺点
  • 优点
    • 严格平衡保证查询效率极高,适合读多写少的场景(如数据库索引)。
    • 时间复杂度稳定,无极端退化情况。
  • 缺点
    • 插入和删除时需频繁调整平衡因子和旋转,写操作开销较大
    • 与红黑树相比,平衡操作更复杂,可能影响实际性能

 

 

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

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

相关文章

OpenCV中距离公式

一、各类距离公式总结 常见距离公式 欧氏距离&#xff1a; 曼哈顿距离&#xff08;L1&#xff09;‌&#xff1a; 切比雪夫距离&#xff08;Chessboard&#xff09;‌&#xff1a; 1、点与点距离(欧氏距离) ‌二维空间‌ 设两点坐标为 P1(x1,y1)、P2(x2,y2)&#xff0c;其距离…

六十天前端强化训练之第二十四天之Vue 模板语法与 v-for 指令大师级详解

欢迎来到编程星辰海的博客讲解 看完可以给一个免费的三连吗&#xff0c;谢谢大佬&#xff01; 目录 一、模板语法与指令知识精讲 1.1 模板语法三大核心 1.2 常见指令全家福 1.3 v-for 深度解析 二、商品列表示例完整实现 2.1 完整可运行代码 2.2 代码解析 2.3 运行效果…

XSS跨站脚本攻击漏洞(Cross Site Scripting)

前提概要 本文章主要用于分享XSS跨站脚本攻击漏洞基础学习&#xff0c;以下是对XSS跨站脚本攻击漏洞的一些个人解析&#xff0c;请大家结合参考其他文章中的相关信息进行归纳和补充。 XSS跨站脚本攻击漏洞描述 跨站脚本攻击&#xff08;XSS&#xff09;漏洞是一种常见且危害较…

用ArcGIS做一张符合环评要求的植被类型图

植被类型图是环境影响评价&#xff08;环评&#xff09;中的重要图件&#xff0c;需满足数据准确性、制图规范性和信息完整性等要求。本教程将基于ArcMap平台&#xff0c;从数据准备到成果输出&#xff0c;详细讲解如何制作符合环评技术规范的植被类型图。 ArcGIS遥感解译土地…

详解string类+迭代器

迭代器 概念&#xff1a;在 C 中&#xff0c;迭代器是访问容器&#xff08;如数组、列表、向量、字符串等&#xff09;元素的一种方式。迭代器提供了一种统一的接口&#xff0c;使得你可以使用相同的代码来遍历不同类型的容器。迭代器本质上是一个指针或者指针的封装&#xff0…

Sqoop安装部署

Apache Sqoop 简介 Sqoop&#xff08;SQL-to-Hadoop&#xff09;是 Apache 开源项目&#xff0c;主要用于&#xff1a; 将关系型数据库中的数据导入 Hadoop 分布式文件系统&#xff08;HDFS&#xff09;或相关组件&#xff08;如 Hive、HBase&#xff09;。 将 Hadoop 处理后…

软件工程之软件验证计划Software Verification Plan

个人主页&#xff1a;云纳星辰怀自在 座右铭&#xff1a;“所谓坚持&#xff0c;就是觉得还有希望&#xff01;” 本文为基于ISO26262软件验证计划模板&#xff0c;仅供参考。 软件验证计划&#xff0c;包括&#xff1a; 1. 软件需求验证计划 2. 软件架构设计验证计划 3. 软件单…

Windows系统本地部署OpenManus对接Ollama调用本地AI大模型

文章目录 前言1. 环境准备1.1 安装Python1.2. 安装conda 2. 本地部署OpenManus2.1 创建一个新conda环境2.2 克隆存储库2.3 安装依赖环境 3. 安装Ollama4. 安装QwQ 32B模型5. 修改OpenManus配置文件6. 运行OpenManus7.通过网页使用OpenManus8. 安装内网穿透8.1 配置随机公网地址…

计算机网络总结

一、IP地址及子网掩码、MAC 二、DNS、ARP 三、DHCP、UDP、TCP 四、NAT、NAPT、端口、网关 五、路由器与交换机 六、OSI模型 一、IP地址及子网掩码、MAC 1.1 IP地址的作用 用来全局网络通信&#xff08;门牌号&#xff09;用来区分相同网络之间的主机 1.2 子网掩码的作用 …

MySQL0基础学习记录-下载与安装

下载 下载地址&#xff1a; &#xff08;Windows&#xff09;https://dev.mysql.com/downloads/file/?id536787 安装 直接点next&#xff0c;出现&#xff1a; 点execute 然后一直next到这页&#xff1a; next 然后需要给root设置一个密码&#xff1a; 在next。。很多页…

React基础语法速览

一、项目创建 npm create vite 这里选择react即可&#xff0c;如图&#xff1a; 二、基本文件说明 react函数式编程时&#xff0c;用的是JSX语法进行开发的&#xff0c;这里注意&#xff0c;return时只能有一个根标签&#xff1b; 三、React核心语法 1.插值功能 插值可以使用…

IT工具 | node.js 进程管理工具 PM2 大升级!支持 Bun.js

P(rocess)M(anager)2 是一个 node.js 下的进程管理器&#xff0c;内置负载均衡&#xff0c;支持应用自动重启&#xff0c;常用于生产环境运行 node.js 应用&#xff0c;非常好用&#x1f44d; &#x1f33c;概述 2025-03-15日&#xff0c;PM2发布最新版本v6.0.5&#xff0c;这…

teaming技术

一.介绍 在CentOS 6与RHEL 6系统中&#xff0c;双网卡绑定采用的是bonding技术。到了CentOS 7&#xff0c;不仅能继续沿用bonding&#xff0c;还新增了teaming技术。在此推荐使用teaming&#xff0c;因其在查看与监控方面更为便捷 。 二.原理 这里介绍两种最常见的双网卡绑定…

SpringSecurity配置(自定义认证过滤器)

文末有本篇文章的项目源码文件可供下载学习 在这个案例中,我们已经实现了自定义登录URI的操作,登录成功之后,我们再次访问后端中的API的时候要在请求头中携带token,此时的token是jwt字符串,我们需要将该jwt字符串进行解析,查看解析后的User对象是否处于登录状态.登录状态下,将…

【机器学习-模型评估】

“评估”已建立的模型 在进行回归和分类时&#xff0c;为了进行预测&#xff0c;定义了预测函数fθ(x) 然后根据训练数据求出了预测函数的参数θ(即对目标函数进行微分&#xff0c;然后求出参数更新表达式的操作) 之前求出参数更新表达式之后就结束了。但是&#xff0c;其实我…

区块链开发技术公司:引领数字经济的创新力量

在数字化浪潮席卷全球的今天&#xff0c;区块链技术作为新兴技术的代表&#xff0c;正以其独特的去中心化、不可篡改和透明性等特点&#xff0c;深刻改变着各行各业的发展格局。区块链开发技术公司&#xff0c;作为这一领域的先锋和推动者&#xff0c;正不断研发创新&#xff0…

油候插件、idea、VsCode插件推荐(自用)

开发软件&#xff1a; 之前的文章&#xff1a; 开发必装最实用工具软件与网站 推荐一下我使用的开发工具 目前在用的 油候插件 AC-baidu-重定向优化百度搜狗谷歌必应搜索_favicon_双列 让查询变成多列&#xff0c;而且可以流式翻页 Github 增强 - 高速下载 github下载 TimerHo…

Linux中find 命令的高级用法 组合条件 与、或、非(-a、-o、!) 以及通过 -regex 和 -iregex 选项使用正则表达式

find 命令详解 find 是 Unix 和类 Unix 操作系统&#xff08;如 Linux 和 macOS&#xff09;中一个非常强大的命令行工具&#xff0c;用于在文件系统中搜索文件和目录。find 命令可以根据多种条件&#xff08;如文件名、类型、大小、修改时间等&#xff09;进行搜索&#xff0c…

基于Python的垃圾短信分类

垃圾短信分类 1 垃圾短信分类问题介绍 1.1 垃圾短信 随着移动互联科技的高速发展&#xff0c;信息技术在不断改变着我们的生活&#xff0c;让我们的生活更方便&#xff0c;其中移动通信技术己经在我们生活起到至关重要的作用&#xff0c;与我们每个人人息息相关。短信作为移…

go语言中空结构体

空结构体(struct{}) 普通理解 在结构体中&#xff0c;可以包裹一系列与对象相关的属性&#xff0c;但若该对象没有属性呢&#xff1f;那它就是一个空结构体。 空结构体&#xff0c;和正常的结构体一样&#xff0c;可以接收方法函数。 type Lamp struct{}func (l Lamp) On()…