【C++高阶】掌握AVL树:构建与维护平衡二叉搜索树的艺术

📝个人主页🌹:Eternity._
⏩收录专栏⏪:C++ “ 登神长阶 ”
🤡往期回顾🤡:STL-> map与set
🌹🌹期待您的关注 🌹🌹

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

❀AVL树

  • 📒1. AVL树的概念
  • 📙2. AVL树节点的定义
  • 📜3. AVL树的插入
  • 📚4. AVL树的旋转
    • 🌈右单旋
    • 🌞左单旋
    • 🌙左右双旋
    • ⭐右左双旋
  • 📝5. AVL树的验证
  • 📘6. AVL树的缺陷
  • 📖7. 总结


前言: 在数据结构的浩瀚海洋中,AVL树(Adelson-Velsky和Landis发明的树)以其独特的平衡机制和高效的搜索性能,成为了一颗璀璨的明星。它不仅解决了二叉搜索树在数据插入和删除时可能产生的失衡问题,更通过旋转操作,使得树的高度始终保持在一个相对较低的水平,从而保证了搜索的高效性

AVL树的学习并非一蹴而就。它需要我们深入理解其背后的数学原理和算法思想,掌握其插入、和旋转等操作的具体实现,并在实践中不断摸索和优化。只有经过这样的过程,我们才能真正掌握AVL树的精髓,并在实际项目中灵活运用

本篇我们将详细介绍AVL树的基本概念、性质、插入操作的具体实现、旋转操作的原理和技巧等内容!

让我们一起踏上学习 AVL树 的旅程,探索它带来的无尽可能!


📒1. AVL树的概念

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

因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:

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

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

  • 它的左右子树都是AVL树
  • 左右子树高度之差(简称平衡因子)的绝对值不超过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)


📙2. AVL树节点的定义

AVL树节点的定义通常包含以下几个关键部分:

基本元素:

  • _left:指向节点的左子节点的指针
  • _right:指向节点的右子节点的指针
  • _parent:指向节点的父节点的指针
  • _kv:一个结构体或配对(pair),包含节点的键值(key)和值(value)。这取决于AVL树的具体用途,可能只包含键或包含键值对。

平衡因子(_bf):

  • 一个整数,表示节点左子树和右子树的高度差。AVL树的性质要求任何节点的平衡因子的绝对值不超过1(-1, 0, 1)

构造函数:

  • 初始化一个新节点时,通常需要一个构造函数,它接受一个键值对(或仅键),并设置节点的左子节点、右子节点、父节点和平衡因子(初始化为0)

节点定义示例(C++):

template<class K,class V>
struct AVLTreeNode
{AVLTreeNode<K, V>* _left; // 该节点的左孩子AVLTreeNode<K, V>* _right; // 该节点的右孩子AVLTreeNode<K, V>* _parent; // 该节点的父亲pair<K, V> _kv;  // pairint _bf; // balance factor 该节点的平衡因子AVLTreeNode(const pair<K, V>& kv):_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _bf(0){}
};

📜3. AVL树的插入

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

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

在我们进行插入操作之前,我们先定义一个AVL树的类

AVL树定义示例(C++):

template<class K,class V>
class AVLTree
{typedef AVLTreeNode<K, V> Node;
public:// 其他未实现的成员函数
private:Node* _root = nullptr;
};

cur插入后,parent的平衡因子一定需要调整

在插入之前,parent的平衡因子分为三种情况:-1,0, 1, 分以下两种情况:

  • 如果cur插入到parent的左侧,只需给parent的平衡因子-1即可
  • 如果cur插入到parent的右侧,只需给parent的平衡因子+1即可

插入后,parent的平衡因子可能有三种情况:0,正负1, 正负2

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

AVL树的插入操作类似于我们之前二叉搜索树的插入,只不过AVL树的插入操作涉及到旋转操作,我们先演示一下它的全部代码

AVL树插入示例(C++):

bool Insert(const pair<K, V>& kv)
{// 当根节点为空时直接插入if (_root == nullptr){_root = new Node(kv);return true;}Node* parent = nullptr;Node* cur = _root;// 寻找插入位置while (cur){if (cur->_kv.first > kv.first){parent = cur;cur = cur->_left;}else if (cur->_kv.first < kv.first){parent = cur;cur = cur->_right;}else{return false;}}// 链接新节点cur = new Node(kv);if (parent->_kv.first > kv.first){parent->_left = cur;cur->_parent = parent;}else{parent->_right = cur;cur->_parent = parent;}// 更改平衡因子while (parent){if (cur == parent->_left){parent->_bf--;}else{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){RotateLR(parent);}else if (parent->_bf == 2 && cur->_bf == -1){RotateRL(parent);}break;}else{assert(false);}}return true;
}

📚4. AVL树的旋转

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


🌈右单旋

新节点插入较高左子树的左侧—左左:

此处旋转是将30的右子树变成60的左子树,然后让60成为30的右子树

在旋转中有几点要注意:

  • 30这个节点的右孩子可能不存在
  • 60这个节点可能是根节点,也可能是子树

如果是根节点,旋转完成后,要更新根节点
如果是子树,可能是某个节点的左子树,也可能是右子树

AVL树右单旋示例(C++):

void RotateR(Node* parent)
{// 定义parent的左孩子 和 parent的左孩子的右孩子Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;if (subLR){subLR->_parent = parent;}// 当旋转点是子树时,保留父亲节点Node* Parentparent = parent->_parent;subL->_right = parent;parent->_parent = subL;// 当旋转点是根时,跟新根节点if (_root == parent){_root = subL;subL->_parent = nullptr;}// 当旋转点是子树时,更新链接else{if (parent == Parentparent->_left){Parentparent->_left = subL;}else{Parentparent->_right = subL;}subL->_parent = Parentparent;}// 更新平衡因子为0parent->_bf = subL->_bf = 0;
}

🌞左单旋

新节点插入较高右子树的右侧—右右:
在这里插入图片描述
左单旋与单旋类似,所以我们直接来看代码

AVL树左单旋示例(C++):

void RotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;subR->_left = parent;Node* Parentparent = parent->_parent;parent->_parent = subR;if (subRL){subRL->_parent = parent;}// 判断parent是不是根节点if (_root == parent){_root = subR;subR->_parent = nullptr;}else{if (parent == Parentparent->_left){Parentparent->_left = subR;}else{Parentparent->_right = subR;}subR->_parent = Parentparent;}parent->_bf = subR->_bf = 0;
}

🌙左右双旋

新节点插入较高左子树的右侧—左右:
在这里插入图片描述
这里是将双旋变成单旋后再旋转,先对30进行左单旋,然后再对90进行右单旋,旋转完成后再考虑平衡因子的更新

这里单旋可以复用上面讲的

AVL树左右双旋示例(C++):

void RotateLR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf; // 根据右子树的右孩子的平衡因子,判断旋转后的平衡因子情况// 复用单旋RotateL(parent->_left);RotateR(parent);// subLR就是新插入节点if (bf == 0){parent->_bf = subL->_bf = subLR->_bf = 0;}else if (bf == 1){subLR->_bf = 0;parent->_bf = 0;subL->_bf = -1;}else if (bf == -1){subLR->_bf = 0;parent->_bf = 1;subL->_bf = 0;}else{assert(false);}
}

⭐右左双旋

新节点插入较高右子树的左侧—右左:
在这里插入图片描述
右左双旋和左右双旋类似,我们直接看代码

AVL树右左双旋示例(C++):

void RotateRL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;// 复用单旋RotateR(parent->_right);RotateL(parent);if (bf == 0){parent->_bf = subR->_bf = subRL->_bf = 0;}else if (bf == 1){subRL->_bf = 0;parent->_bf = -1;subR->_bf = 0;}else if (bf == -1){subRL->_bf = 0;parent->_bf = 0;subR->_bf = 1;}else{assert(false);}
}

总结:

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

  • parent的平衡因子为2,说明parent的右子树高,设parent的右子树的根为subR
    当subR的平衡因子为1时,执行左单旋
    当subR的平衡因子为-1时,执行右左双旋
  • parent的平衡因子为-2,说明parent的左子树高,设parent的左子树的根为subL
    当subL的平衡因子为-1是,执行右单旋
    当subL的平衡因子为1时,执行左右双旋

旋转完成后,原parent为根的子树个高度降低,已经平衡,不需要再向上更新


📝5. AVL树的验证

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

验证其为二叉搜索树

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

代码演示示例(C++):

// 中序遍历
void InOrder()
{_InOrder(_root);cout << endl;
}void _InOrder(Node* root)
{if (root == nullptr){return;}_InOrder(root->_left);cout << root->_kv.first << " ";_InOrder(root->_right);
}

验证其为平衡树

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

代码演示示例(C++):

bool IsBalance()
{return _IsBalance(_root);
}int _Height(Node* root)
{if (root == nullptr){return;}int rightHeight = _Height(root->_right);int leftHeight = _Height(root->_left);return rightHeight > leftHeight ? rightHeight + 1 : leftHeight + 1;
}bool _IsBalance(Node* root)
{if (root == nullptr){return true;}int rightHeight = _Height(root->_right);int leftHeight = _Height(root->_left);if (rightHeight - leftHeight != root->_bf){cout << root->_kv.first << "平衡因子有误" << endl;return false;}return abs(rightHeight - leftHeight) < 2&& _IsBalance(root->_left)&& _IsBalance(root->_right);
}

验证用例

int main()
{// 这里会进行双旋int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };AVLTree<int, int> t;for (auto e : a){t.Insert(make_pair(e, e));}t.InOrder();cout << t.IsBalance() << endl;return 0;
}

在这里插入图片描述


📘6. AVL树的缺陷

缺陷原因
插入操作复杂为了保持树的平衡,每次插入或删除节点时,AVL树可能需要进行多次旋转操作。具体来说,插入一个节点可能需要单旋转或双旋转来重新平衡树结构,而删除节点后可能需要从被删除节点到根节点这条路径上所有节点的平衡,旋转的量级最坏情况下为O(logN)。这增加了操作的复杂性并可能影响性能。
维护成本高由于AVL树要求每个节点的左右子树高度差不超过1,因此需要频繁地检查和调整树的结构。这种严格的平衡要求导致了相对较高的维护成本,特别是在频繁进行插入和删除操作的情况下。
空间开销较大虽然AVL树在查找效率上具有优势,但由于其需要频繁地进行旋转操作以维持平衡,这可能导致额外的空间开销。尤其是在处理大量数据时,这种开销可能会更加明显。
不适用于所有场景AVL树适用于查找操作远多于插入和删除操作的场景。如果在一个应用中插入和删除操作也非常频繁,那么AVL树可能不是最优选择,因为每次插入和删除都需要进行平衡调整,这会影响性能。

📖7. 总结

在深入探讨AVL树的旅程即将结束时,我们不禁为这种精妙的数据结构所折服。AVL树不仅以其高度的平衡性保证了高效的搜索、插入操作,而且它所蕴含的平衡维护机制也体现了计算机科学中的智慧与美

学习AVL树的过程,不仅是一次对数据结构知识的积累,更是一次对问题分析和解决能力的锻炼。我们学会了如何在插入和删除操作中通过旋转操作来保持树的平衡,这种动态调整的思想在软件开发中同样具有广泛的应用

AVL树的学习之旅虽然告一段落,但我们对数据结构和算法的探索永无止境。在未来的学习和工作中,我们将会遇到更多复杂的问题和挑战,但只要我们保持对知识的渴望和对问题的深入思考,就一定能够找到解决问题的钥匙

让我们以AVL树为起点,继续在数据结构和算法的海洋中遨游,不断挖掘计算机科学的奥秘,为未来的技术创新和进步贡献自己的力量。愿每一位学习者都能在求知的道路上不断前行,收获满满的智慧与快乐

在这里插入图片描述

希望本文能够为你提供有益的参考和启示,让我们一起在编程的道路上不断前行!
谢谢大家支持本篇到这里就结束了,祝大家天天开心!

在这里插入图片描述

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

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

相关文章

JavaSE (Java基础):运算符

3 运算符 3.1 二元运算符 为什么下面这段代码中最后的语句中b元素要加&#xff08;double&#xff09;呢&#xff1f; 因为要计算10/40的话&#xff0c;他们都是int类型的在计算机中会取整&#xff0c;而计算机取整一般都是直接舍去小数点后面的数字&#xff0c;那么就会返回0&…

VScode基本使用

VScode下载安装&#xff1a; Visual Studio Code - Code Editing. Redefined MinGW的下载安装&#xff1a; MinGW-w64 - for 32 and 64 bit Windows - Browse Files at SourceForge.net x86是64位处理器架构&#xff0c;i686是32为处理器架构。 POSIX和Win32是两种不同的操…

vscode pip : 无法将“pip”项识别为 cmdlet、函数、脚本文件或可运行程序的名称

在vscode中控制台运行python文件出现&#xff1a;无法将"pip”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。 使用vscode开发python&#xff0c;需要安装python开发扩展&#xff1a; 本文已经安装&#xff0c;我们需要找的是python安装所在目录&#xff0c;本文…

【云原生】Kubernetes----证书过期处理办法

目录 引言 一、证书过期的问题与影响 二、解决方案 &#xff08;一&#xff09;查看证书剩余时间 &#xff08;二&#xff09;备份重要数据 &#xff08;三&#xff09;更新证书 &#xff08;四&#xff09;重启相关组件的pod 引言 随着云计算技术的飞速发展&#xff0…

消息认证码解析

1. 什么是消息认证码 消息认证码(Message Authentication Code)是一种确认完整性并进行认证的技术&#xff0c;取三个单词的首字母&#xff0c;简称为MAC。 消息认证码的输入包括任意长度的消息和一个发送者与接收者之间共享的密钥&#xff0c;它可以输出固定长度的数据&#x…

[分布式网络通讯框架]----ZooKeeper下载以及Linux环境下安装与单机模式部署(附带每一步截图)

首先进入apache官网 点击中间的see all Projects->Project List菜单项进入页面 找到zookeeper&#xff0c;进入 在Zookeeper主页的顶部点击菜单Project->Releases&#xff0c;进入Zookeeper发布版本信息页面&#xff0c;如下图&#xff1a; 找到需要下载的版本 …

【vite】define 全局常量定义

&#x1f9ed; define 说明 类型&#xff1a; Record<string, any> 定义全局常量替换方式。其中每项在开发环境下会被定义在全局&#xff0c;而在构建时被静态替换。 Vite 使用 esbuild define 来进行替换&#xff0c;因此值的表达式必须是一个包含 JSON 可序列化值&a…

【面试干货】Java中的四种引用类型:强引用、软引用、弱引用和虚引用

【面试干货】Java中的四种引用类型&#xff1a;强引用、软引用、弱引用和虚引用 1、强引用&#xff08;Strong Reference&#xff09;2、软引用&#xff08;Soft Reference&#xff09;3、弱引用&#xff08;Weak Reference&#xff09;4、虚引用&#xff08;Phantom Reference…

外部网络如何访问内网?

在现代信息化时代&#xff0c;随着企业规模的扩大和业务范围的扩展&#xff0c;越来越多的企业需要实现外部网络访问内网的需求。外部网络访问内网指的是在外部网络环境下&#xff0c;通过互联网等公共网络途径&#xff0c;实现对企业内部网络的访问和操作。这种需求的出现&…

【LinuxC语言】深入理解IP地址与端口号

文章目录 前言端口号IP地址IP地址的分类主机地址与网络地址多播是什么子网掩码特殊的地址与私有的地址总结前言 在计算机网络中,IP 地址和端口号是两个非常重要的概念。IP 地址用于标识网络上的设备,而端口号则用于在同一设备上区分不同的服务或应用。在 Linux C 语言编程中…

obsidian中用check list 打造待办清单

背景 在快节奏的现代生活中&#xff0c;有效管理个人时间和任务成为提升生活与工作效率的关键。 Obsidian&#xff0c;作为一款强大的知识管理和笔记应用&#xff0c;通过其丰富的插件生态&#xff0c;为我们提供了高度自定义的任务管理解决方案。本文旨在详细介绍如何在Obsid…

基于C# .NET 的数字图像处理系统开发

嗨&#xff0c;我是射手座的程序媛&#xff0c;期待和大家更多的交流与学习&#xff0c;欢迎添加3512724768。 《面向对象程序设计》-基于C# .NET 的数字图像处理系统开发 &#xff08;一&#xff09;实习的目的 根据《面向对象程序设计》理论课授课内容&#xff0c;是使学生…

Java项目:基于SSM框架实现的助学贷款管理系统【ssm+B/S架构+源码+数据库+毕业论文】

一、项目简介 本项目是一套基于SSM框架实现的助学贷款管理系统 包含&#xff1a;项目源码、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过严格调试&#xff0c;eclipse或者idea 确保可以运行&#xff01; 该系统功能完善、界面美观、操作简单、功能…

python学习笔记-09

面向对象编程-中 面向对象三大特征&#xff1a;封装、继承、多态。 封装&#xff1a;把内容封装起来便于后面的使用。对于封装来讲&#xff0c;就是使用__init__方法将内容封装道对象中&#xff0c;然后通过对象直接或者self获取被封装的内容。 继承&#xff1a;子继承父的属…

Uniapp在屏幕尺寸低于960出现样式错乱(开箱即用)

我司项目突然要做平板兼容,我在调试的时候发现当屏幕尺寸低于960px发现样式但凡是以rpx单位的全部失效&#xff0c;如果是以px为单位那么影响就比较小&#xff0c;当时解决方案是写了不少媒体查询和把单位rpx改成px&#xff0c;后面查阅文档发现大错特错宽屏适配只需一行代码即…

04--MySQL8.0_JDBC

第一章 JDBC概述 之前我们学习了JavaSE,编写了Java程序,数据保存在变量、数组、集合等中,无法持久化,后来学习了IO流可以将数据写入文件,但不方便管理数据以及维护数据的关系; 后来我们学习了数据库管理软件MySQL,可以方便的管理数据1。 那么如何将它俩结合起来呢?即…

《AI旋律:创意产业的重塑与共生》

AI乐章&#xff1a;技术革命下的创意产业新生态 在数字化浪潮的推动下&#xff0c;音乐创作领域迎来了前所未有的变革——AI音乐大模型的横空出世&#xff0c;犹如一颗石子投入平静的湖面&#xff0c;激起了层层涟漪。这些模型以令人难以置信的速度和多样性&#xff0c;将音乐…

【树形dp 换根法 BFS】2581. 统计可能的树根数目

本文涉及知识点 CBFS算法 动态规划汇总 图论知识汇总 树形dp 换根法 BFS LeetCode 2581. 统计可能的树根数目 Alice 有一棵 n 个节点的树&#xff0c;节点编号为 0 到 n - 1 。树用一个长度为 n - 1 的二维整数数组 edges 表示&#xff0c;其中 edges[i] [ai, bi] &#xf…

《书生·浦语大模型实战营》第5课 学习笔记:LMDeploy 量化部署 LLM 实践

文章大纲 0.背景知识与简介计算机组成原理&#xff1a;变量的存储参数量与推理的关系 1.LMDeploy环境部署1.1 创建开发机1.2 创建conda环境InternStudio开发机创建conda环境&#xff08;推荐&#xff09;本地环境创建conda环境 1.3 安装LMDeploy 2.LMDeploy模型对话(chat)2.1 H…