深入学习c++之---AVL树

VL树简介

AVL树是一种自平衡二叉搜索树,通过平衡因子(Balance Factor, BF)​旋转操作,确保树始终保持平衡,避免退化成链表,从而保证查找、插入、删除的时间复杂度稳定在 ​O(log n)​

核心特点
  1. 平衡条件​:每个节点的左右子树高度差不超过1(BF ∈ {-1, 0, 1})。
  2. 调整方法​:通过四种旋转​(右单旋、左单旋、左右双旋、右左双旋)修复失衡。
  3. 优势​:比普通BST更稳定,适合频繁动态更新的场景。

目录

VL树简介​

​核心特点​

一, 搜索二叉树的固有问题:

二,平衡因子:

1`基本定义:

2`补充说明:

三,结构的设计:

        1`扩充的成员变量: 

         2`小坑: 

四,元素的插入:

         1`插入元素 :

         2`平衡因子的维护:

        3`旋转:

        1`右单旋:

        2`左单旋:

        3`左右双旋:

        4`右左双旋:

五`元素的查找(按值):

 六`中序遍历顺序输出:

七`类的结构和成员函数代码汇总:


一, 搜索二叉树的固有问题:

  •         理想情况下 , 搜索二叉树的搜索效率已经足够高 , 仅仅log(N)的时间复杂度 .
  •         但是 , 如果是按照近似有序序列来插入 , 会导致树形结构退化为普通的链式结构 (没错,和普通单链表一样了!!!).
  •         于是 , 本节索要探讨的AVL树的结构就应运而生了(AVL是按照两个前苏联科学家的名字来命名的,没有特殊含义).

二,平衡因子:

        想要避免搜索二叉树的退化 , 就需要在恰当的时候通过旋转来调整(之后的内容) , 而为了能够更加优雅地理清旋转的逻辑和简化相关代码编写 , 平衡因子是我们可以提前准备的小工具 .

1`基本定义:

  • 节点初始的平衡因子为0
  • 在节点左边插入新节点 , 平衡因子 +1
  • 在节点右边插入新节点,平衡因子 -1
  • 形象的来说 , 一个节点的平衡因子 = 右子树高度 - 左子树高度 .

2`补充说明:

  •         平衡因子可以让我们以量化的方式来判断一个节点左右子树的高度差 , 以此来决定是否需要旋转.
  •          对于AVL树来说 , 任何一个节点的平衡因子的绝对值必须小于等于1 .
  •          之所以没有要求节点的平衡因子严格等于0 , 是因为在部分情况下不现实 , 比如整棵树只有两个节点时 , 那要么左子树高度低要么右子树高度低 .
  •          下面列举几个例子来具象化展示: 
平衡因子为0的根节点

平衡因子的绝对值为1的根节点, 此处为-1


平衡因子的绝对值大于等于2的节点 , 此处为-2
平衡因子做不到为0的情况

三,结构的设计:

     总的类框架没变 , 还是分为一个主类来包含一个根节点的成员变量以及各种成员函数 , 和一个节点类 . 只是节点类的成员变量稍有扩充   

        1`扩充的成员变量: 

AVL树
和搜索二叉树相同的成员变量值key / 左孩子left / 右孩子right
新增的成员变量一 : 平衡因子BF (单词 balance factor的缩写)
新增的成员变量二:父节点parent (方便检索平衡因子)
//节点类
template<class K , class V>
struct AvlNode
{//构造AvlNode(K key , V val){_k.first = key;_k.second = val;_parent = nullptr;_left = nullptr;_right = nullptr;}//成员变量pair<K, V> _k;AvlNode* _parent;  //新增AvlNode* _left;AvlNode* _right;int _bf = 0;    //新增
};

         2`小坑: 

        对于一颗初始为空的AVL树 , 成员变量(_root)应该默认初始化为nullptr , 否则难以应对向空树中插入元素时的情况.

//AVL树的类声明
template<class K>
class AVLTree
{
public:using Node = AVLNode<K>;
private:Node* _root == nullptr;   //c++11的语法,用于初始化列表的缺省值
};

四,元素的插入:

        AVL树就是在搜索二叉树的基础上通过旋转来维持平衡 , 而插入元素的函数里也包含了平衡因子的维护和旋转的逻辑.

         1`插入元素 :

        AVL树插入元素的逻辑和二叉搜索树类似 , 只不过每个节点多出来的BF(平衡因子)和parent(父节点)要求略微的调整.

  1. 函数接受一个插入的值 key.
  2. 定义一个指向根节点的局部指针变量cur , 和一个备用的局部指针变量parent来保存cur每次更新之前的值(放标最后链接)
  3. 每次通过判断cur节点的key和待插入的key的大小来决定往左子树还是右子树更新.
  4. 如果cur走到空,则找到插入位置,借助以保存的parent变量来连接新节点 ; 如果cur走到值和key相同的节点,说明已存在该值,不做处理直接结束.

特殊情况 : 如果起初的根节点为空(nullptr) , 则直接让新节点作根,结束函数体.

//insert函数插入数据的部分
//-----------------------------------------------------------
//根为空的空树情况
if (nullptr == _root)
{_root = new Node(key);return true;
}Node* parent = nullptr;
Node* cur = _root;
while (cur)
{if (key < cur->_key) //待插入的节点值小于当前节点,则往左子树{parent = cur;cur = cur->_left}else if (key > cur->_key) //待插入的节点值大于当前节点,则往右子树{parent = cur;cur = cur->_right;}else{return false;}
}
Node* newNode = new Node(key);
//判断新节点应该连接到父节点的哪边(通过值判断!!!)
if (key < parent->_key)
{parent->_left = newNode;
}
else
{parent->_right = newNode;
}
newNode->_parent = parent;

         2`平衡因子的维护:

        前面已经基本介绍过了节点平衡因子的定义 , 用代码维护起来也比较简单.

  1. 使用两个局部指针变量 , 一个新插入节点的指针cur , 另一个是他的父节点指针parent
  2. 新节点cur的平衡因子是天然的0 , 所以从parent的平衡因子开始判断.
  3. 若新节点cur是parent的左节点 , parent的平衡因子BS减一 ;                                         若新节点cur是parent的右节点 , parent的平衡因子BS加一;
  4. 接下来直接判断更新后的当前parent的平衡因子 , 有三种情况:                                                  第一种 : parent更新后平衡因子为0 , 说明平衡了 , 不用旋转 , 结束插入的过程.              第二种 : parent更新后的平衡因子为 1或-1 , 说明还是平衡 , 但是需要将cur和parent全部往上更新一层 , 进行第 3 点的操作.                                                                            第三种 : parent更新后的平衡因子为2或-2 , 说明当前节点的左右子树不平衡 , 需要旋转(下文的内容,此处省略).
//维护平衡因子
cur = newNode;
while (parent)
{if (parent->_left == cur){parent->_bf--;}else{parent->_bf++;}if (parent->_bf == 0){return true;}else if (abs(parent->_bf) == 1){cur = parent;parent = parent->_parent;}else if (abs(parent->_bf) == 2){//旋转的类型和逻辑 , 此处略}else{assert(false); //防御性编程}
}

        3`旋转:

        旋转是最有意思的部分 , 通过节点不平衡的情况来判断旋转的类型 .

        旋转的类型有左单旋/右单旋/右左双旋/左右双旋.

  1. 单旋转 : 存粹的左子树高或者右子树高 , 可以大致理解为新插入的节点是左子树的最左边或右子树的最右边 .
  2. 双旋转:不存粹的左子树高或者右子树高 , 可以大致理解为新插入的节点是左子树的叶子结点的右侧 或 右子树的叶子结点的左侧.

不纯粹的高 :

         如果是看图 , 会发现一个节点的第一个节点插入到了左边(右边) , 下一个节点插入在了刚刚插入的节点的右边(右边) ;

        如果是看平衡因子 , 令最后插入的节点为cur , 则cur->Parent的平衡因子和cur->Parent->Parent的平衡因子异号.

        1`右单旋:

// 右单旋
void RotateR(Node* root) 
{Node* subL = root->_left;Node* subLR = subL->_right;//将左子树的左子树节点链接给旋转节点的左边root->_left = subLR;if (subLR != nullptr) //防止左子树的左子树的跟节点为空{subLR->_parent = root;}//提前保存节点旋转之前的父节点Node* oldParent = root->_parent;//将旋转节点和其左子节点颠倒父子关系subL->_right = root;root->_parent = subL;if (root == _root) //待旋转节点为整棵树的跟的情况{_root = subL;subL->_parent = nullptr;}else{//让subL和root的父节点建立链接if (oldParent->_right == root){oldParent->_right = subL;}else{oldParent->_left = subL;}subL->_parent = oldParent;}//更新平衡因子root->_bf = 0;subL->_bf = 0;
}

        2`左单旋:

//左单旋
void RotateL(Node* root)
{Node* subR = root->_right;Node* subRL = subR->_left;root->_right = subRL;if (subRL != nullptr){subRL->_parent = root;}Node* oldParent = root->_parent;subR->_left = root;root->_parent = subR;if (root == _root){_root = subR ;subR->_parent = nullptr;}else{if (oldParent->_left == root){oldParent->_left = subR;}else{oldParent->_right = subR;}subR->_parent = oldParent;}root->_bf = 0;subR->_bf = 0;
}

        3`左右双旋:

 

//左右双旋
void RotateLR(Node* root)
{Node* subL = root->_left;Node* subLR = subL->_right;int bf_subLR = subLR->_bf;RotateL(subL);RotateR(root);if (bf_subLR == 0){root->_bf = 0;subL->_bf = 0;subLR->_bf = 0;}else if (bf_subLR == 1){root->_bf = 0;subL->_bf = -1;subLR->_bf = 0;}else if (bf_subLR == -1){root->_bf = 1;subL->_bf = 0;subLR->_bf = 0;}else{assert(false);}
}

        4`右左双旋:

//右左双旋
void RotateRL(Node* root)
{Node* subR = root->_right;Node* subRL = subR->_left;int bf_subRL = subRL->_bf;RotateR(subR);RotateL(root);if (bf_subRL == 0){root->_bf = 0;subR->_bf = 0;subRL->_bf = 0;}else if(bf_subRL == 1){root->_bf = -1;subR->_bf = 0;subRL->_bf = 0;}else if (bf_subRL == -1){root->_bf = 1;subR->_bf = 0;subRL->_bf = 0;}else{assert(false);}
}

五`元素的查找(按值):

        元素的查找平平无奇 , 和搜索二叉树一个样 ,  都是拿着待查找的值key从根节点开始比对 , key更小则找左子树,更大则找右子树 .

//按值查找节点
Node* Find(const K& key)
{Node* cur = _root;while (cur){if (key < cur->_key){cur = cur->_left;}else if (key > cur->_key){cur = cur->_right;}else{return cur;}}return false;
}

 六`中序遍历顺序输出:

        一般来说 , AVL树的涉及使得  左子树的值<根节点的值<右子树的值 . 所以通过中序遍历(左子树->根节点->左子树)正好就可以得到一个顺序序列.

        需要注意的是:树形结构的递归操作需要将根节点作为参数传递,但由于根节点通常作为类的私有成员变量,无法在外部直接访问。因此,在C++中常见的做法是提供一个公有成员函数来传递根节点,从而确保内部的递归函数能够正常运行。

//设置为共有成员函数,方便外部调用
public:
void MidTrave()
{_MidTrave(_root);cout << endl;
}//设置成私有成员函数,仅仅让类类内部访问
private:
void _MidTrave(Node* root)
{if (nullptr == root){return;}_MidTrave(root->_left);cout << root->_key << " ";_MidTrave(root->_right);
}

七`类的结构和成员函数代码汇总:

....//头文件的包含此处省略
----------------------------
//节点类的声明
template<class K>
struct AVLNode
{AVLNode(K key):_key(key), _parent(nullptr), _left(nullptr), _right(nullptr), _bf(0){}K _key;AVLNode<K>* _parent;AVLNode<K>* _left;AVLNode<K>* _right;int _bf;
};//AVL树的类声明
template<class K>
class AVLTree
{
public:using Node = AVLNode<K>;//插入bool Insert(const K& key){//根为空的空树情况if (nullptr == _root){_root = new Node(key);return true;}Node* parent = nullptr;Node* cur = _root;while (cur){if (key < cur->_key) //待插入的节点值小于当前节点,则往左子树{parent = cur;cur = cur->_left;}else if (key > cur->_key) //待插入的节点值大于当前节点,则往右子树{parent = cur;cur = cur->_right;}else{return false;}}Node* newNode = new Node(key);//判断新节点应该连接到父节点的哪边(通过值判断!!!)if (key < parent->_key){parent->_left = newNode;}else{parent->_right = newNode;}newNode->_parent = parent;//----------------------------------------------------------------//维护平衡因子cur = newNode;while (parent){if (parent->_left == cur){parent->_bf--;}else{parent->_bf++;}if (parent->_bf == 0){return true;}else if (abs(parent->_bf) == 1){cur = parent;parent = parent->_parent;}else if (abs(parent->_bf) == 2){if (parent->_bf == -2 && cur->_bf <= 0) //右单旋{RotateR(parent);}else if (parent->_bf == 2 && cur->_bf >= 0) //左单旋{RotateL(parent);}else if (parent->_bf == -2 && cur->_bf >= 0) //左右双旋{RotateLR(parent);}else if (parent->_bf == 2 && cur->_bf <= 0) //右左双旋{RotateRL(parent);}else{assert(false);}break;}else{assert(false);}}return true;}// 右单旋void RotateR(Node* root) {Node* subL = root->_left;Node* subLR = subL->_right;//将左子树的左子树节点链接给旋转节点的左边root->_left = subLR;if (subLR != nullptr) //防止左子树的左子树的跟节点为空{subLR->_parent = root;}//提前保存节点旋转之前的父节点Node* oldParent = root->_parent;//将旋转节点和其左子节点颠倒父子关系subL->_right = root;root->_parent = subL;if (root == _root) //待旋转节点为整棵树的跟的情况{_root = subL;subL->_parent = nullptr;}else{//让subL和root的父节点建立链接if (oldParent->_right == root){oldParent->_right = subL;}else{oldParent->_left = subL;}subL->_parent = oldParent;}//更新平衡因子root->_bf = 0;subL->_bf = 0;}//左单旋void RotateL(Node* root){Node* subR = root->_right;Node* subRL = subR->_left;root->_right = subRL;if (subRL != nullptr){subRL->_parent = root;}Node* oldParent = root->_parent;subR->_left = root;root->_parent = subR;if (root == _root){_root = subR ;subR->_parent = nullptr;}else{if (oldParent->_left == root){oldParent->_left = subR;}else{oldParent->_right = subR;}subR->_parent = oldParent;}root->_bf = 0;subR->_bf = 0;}//左右双旋void RotateLR(Node* root){Node* subL = root->_left;Node* subLR = subL->_right;int bf_subLR = subLR->_bf;RotateL(subL);RotateR(root);if (bf_subLR == 0){root->_bf = 0;subL->_bf = 0;subLR->_bf = 0;}else if (bf_subLR == 1){root->_bf = 0;subL->_bf = -1;subLR->_bf = 0;}else if (bf_subLR == -1){root->_bf = 1;subL->_bf = 0;subLR->_bf = 0;}else{assert(false);}}//右左双旋void RotateRL(Node* root){Node* subR = root->_right;Node* subRL = subR->_left;int bf_subRL = subRL->_bf;RotateR(subR);RotateL(root);if (bf_subRL == 0){root->_bf = 0;subR->_bf = 0;subRL->_bf = 0;}else if(bf_subRL == 1){root->_bf = -1;subR->_bf = 0;subRL->_bf = 0;}else if (bf_subRL == -1){root->_bf = 1;subR->_bf = 0;subRL->_bf = 0;}else{assert(false);}}//按值查找节点Node* Find(const K& key){Node* cur = _root;while (cur){if (key < cur->_key){cur = cur->_left;}else if (key > cur->_key){cur = cur->_right;}else{return cur;}}return nullptr;}void MidTrave(){_MidTrave(_root);cout << endl;}
private:void _MidTrave(Node* root){if (nullptr == root){return;}_MidTrave(root->_left);cout << root->_key << " ";_MidTrave(root->_right);}Node* _root = nullptr;
};

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

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

相关文章

【PTA数据结构 | C语言版】输出 1 ~ n

本专栏持续输出数据结构题目集&#xff0c;欢迎订阅。 文章目录题目代码题目 给定正整数 n&#xff0c;输出 1 ~ n&#xff0c;每个数字占一行。 本题旨在测试不同的算法在各种数据情况下的表现。各组测试数据特点如下&#xff1a; 数据 0&#xff1a;测试基本正确性&#x…

如何禁止用户复制页面内容?

某些特定的业务场景下&#xff0c;我们可能会有禁止用户复制页面内容的需求。比如&#xff1a; 付费内容保护&#xff1a;在线小说、付费课程等&#xff0c;希望防止内容被轻易拷贝和传播。试卷或答题系统&#xff1a;防止考生将题目复制出去寻求场外帮助。敏感信息展示&#x…

React + PDF.js 预览 PDF 文件:从基础实现到高级优化的完整指南

关键点 PDF.js&#xff1a;Mozilla 开发的开源 JavaScript 库&#xff0c;用于在浏览器中渲染 PDF 文件。React 集成&#xff1a;结合 React 组件化特性&#xff0c;实现高效、交互式的 PDF 预览功能。功能实现&#xff1a;支持 PDF 文件加载、页面导航、缩放、搜索、书签和注…

新能源汽车BMS电感产品应用及选型推荐

在新能源电动汽车中&#xff0c;BMS&#xff08;电池管理系统&#xff09;如同一个守护者&#xff0c;默默守护电池的安全与性能。它精准监控电压、电流、温度&#xff0c;防止过充过放&#xff0c;并通过智能均衡技术提升续航能力。电感在BMS系统的电源转换、滤波和隔离通信等…

【机器学习笔记 Ⅱ】12随机森林

随机森林&#xff08;Random Forest&#xff09;详解 随机森林是一种基于集成学习&#xff08;Ensemble Learning&#xff09;的高性能分类/回归算法&#xff0c;通过构建多棵决策树并综合其预测结果&#xff0c;显著提升模型的准确性和鲁棒性。其核心思想是“集体智慧优于个体…

问题 1:MyBatis-plus-3.5.9 的分页功能修复

问题 1&#xff1a;MyBatis-plus-3.5.9 的分页功能修复 使用 Sw‏agger 接口文档‎依次对上述接口进行测 试&#xff0c;发现 listU⁡serVOByPage 接口有一些问题&#xff01; 分页好像没有生效&#xff0c;还是查出了全部数据&#xff1a; 由于我们用的是 MyBatis Plus 来操…

Qt 如何提供在线帮助

Qt 如何提供在线帮助一、概述二、工具提示、状态提示和"Whats This?"帮助1、工具提示(Tool Tips)添加工具提示到控件富文本工具提示全局工具提示设置延迟显示控制自定义工具提示窗口禁用工具提示工具提示与状态栏联动特点&#xff1a;2、状态提示(Status Tips)3、&q…

Typecho站点关闭插件开发全指南:从原理到实现

文章目录 开发Typecho站点关闭插件:从原理到实现一、背景与需求分析二、插件设计思路2.1 技术选型2.2 功能模块设计三、插件开发实现3.1 插件基础结构3.2 插件主文件实现3.3 核心功能实现3.4 后台管理界面3.5 关闭页面模板四、插件配置完善4.1 配置表单实现4.2 定时任务处理五…

详细解析 .NET 依赖注入的三种生命周期模式

文章目录一、Transient&#xff08;瞬时生命周期&#xff09;原理使用方式核心特性适用场景优势劣势二、Scoped&#xff08;作用域生命周期&#xff09;原理使用方式核心特性适用场景优势劣势三、Singleton&#xff08;单例生命周期&#xff09;原理使用方式核心特性适用场景优…

软件工程经济与伦理

前言 各位帅哥美女&#xff0c;能看到这篇博客的都有口福了&#xff0c;学习这门课程就像遨游在大份的海洋&#xff0c;一不小心就吃上一口。能看到这篇博客说明我们是有缘人可以点赞收藏一下&#xff0c;这篇博客可以在你无比饥饿的时候给你送上一坨&#xff01;&#xff08;香…

AI 体验走查 - 火山引擎存储的 AI UX 探索之路

01 概述 火山引擎存储技术团队驱动 AI 自主完成用户体验走查 / 可用性测试的执行与评价&#xff0c;帮助业务改善交互体验。 立项“故事走查”的背景诉求和 AI 机遇 如何搭建“AI 评价”能力&#xff0c;精准识别交互问题 让交互体验故事走查变为技术产品&#xff0c;讲解系…

【世纪龙科技】汽车零部件检验虚拟实训室-助力汽车职教实训

在汽车产业加速向电动化、智能化转型的背景下&#xff0c;职业院校汽车专业教学面临新的挑战&#xff1a;传统实训受限于设备数量不足、操作风险高、标准化程度低等问题&#xff0c;导致学生实践机会有限&#xff0c;技能掌握不扎实。如何让学生在有限资源下高效掌握零部件检验…

MySQL常用操作 查看表描述以及表结构、连接数及缓存和性能指标

查看表描述以及表结构查看数据库名SHOW DATABASES; SELECT DATABASE(); SELECT DATABASE() AS current_database;查看数据库中表的列表SHOW TABLES; SELECT TABLE_NAME, TABLE_COMMENT FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA your_database_name; SELECT TABLE_NA…

音视频学习(三十六):websocket协议总结

概述项目描述标准RFC 6455使用端口默认 80&#xff08;ws&#xff09;&#xff0c;443&#xff08;wss&#xff09;基于协议TCP特性全双工、低开销、持久连接、可穿透代理特点 全双工通信&#xff1a; WebSocket 允许客户端和服务器之间建立一个持久的连接&#xff0c;并且数据…

docker版本nacos的搭建

1.下载镜像2.拷贝出容器中对应的配置文件&#xff0c;logs&#xff0c;data&#xff0c;conf3.编写yaml配置文件version: 3.8 services:nacos-server:image: nacos/nacos-server:v2.4.0container_name: nacos-serverrestart: unless-stoppedports:- "8848:8848" # …

【机器学习深度学习】 如何解决“宏平均偏低 / 小类识别差”的问题?

目录 &#x1f9e9; 场景 一、先问清楚&#xff1a;小类差&#xff0c;到底差在哪&#xff1f; 二、对症下药&#xff1a;六大优化策略&#xff08;分类任务专用&#xff09; ✅ 1. 处理类别不平衡&#xff08;最常见&#xff09; ✅ 2. 优化数据质量 ✅ 3. 更强的模型结…

数据结构 --- 栈

栈 --- stack前言一、栈结构二、相关方法1.初始化2.入栈3.出栈4.判空5.获取栈顶元素6.获取栈大小7.销毁前言 栈是一个特殊的线性表&#xff0c;遵循一个先进后出的特性&#xff0c;即操作数据&#xff08;入栈&#xff0c;出栈&#xff09;只能从栈顶操作&#xff0c;栈底是一…

【uniapp】---- 在 HBuilderX 中使用 tailwindcss

1. 前言 接手了一个uniapp的微信小程序项目,因为在上一个 taro 的项目中使用的 tailwindcss,感觉比较方便,又不想动项目中原来的代码,因此就配置 tailwindcss,在新创建的子包中使用。 2. 分析 vue2 版本的 uni-app 内置的 webpack 版本为 4 , postcss 版本为 7, 所以还是…

Spring Boot + Easy Excel 自定义复杂样式导入导出

tips&#xff1a;能用模板就用模板&#xff0c;当模板不适用的情况下&#xff0c;再选择自定义生成 Excel。官网&#xff1a;https://easyexcel.opensource.alibaba.com安装<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</arti…

Spark从入门到实战:安装与使用全攻略

目录一、Spark 简介1.1 Spark 的概念1.2 Spark 的优势1.3 Spark 的应用场景二、安装前准备2.1 硬件要求2.2 软件要求2.3 下载 Spark三、Spark 安装步骤3.1 解压安装包3.2 配置环境变量3.3 配置 spark-env.sh3.4 配置 slaves 文件&#xff08;分布式模式&#xff09;3.5 启动 Sp…