C++【AVL树】

目录

1.AVL树(高度平衡搜索二叉树)定义

1.1平衡因子(Balance Factor简写成bf)

1.2avl树的定义

2.AVL树插入的基本操作

 2.1逻辑抽象图

2.2插入流程

2.3左单旋

2.4右单旋

2.5右左双旋

2.6左右双旋 

3.AVL树的检验技巧 

   3.1、检验依据

3.2、检验方法

3.3、AVL树的性能



1.AVL树(高度平衡搜索二叉树)定义

平衡二叉树 全称叫做 平衡二叉搜索(排序)树,简称 AVL树。英文:Balanced Binary Tree (BBT),注:二叉查找树(BST)

由 前苏联 的两位数学家:G.M.Adelson-Velskii 和 E.M.Landis 共同提出,首次出现在 1962 发布的论文 《An algorithm for the organization of information》 中。

AVL树在二叉搜索树的的基础上增加了两个特性:

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

1.1平衡因子(Balance Factor简写成bf)

定义:节点的右子树减去左子树的高度,在 AVL树中,所有节点的平衡因子都必须满足: -1<=bf<=1;

1.2avl树的定义

AVL 树在原 二叉搜索树 的基础上添加了 平衡因子 bf 以及用于快速向上调整的 父亲指针 parent,所以 AVL 树是一个三叉链结构:

 上图我们描述了一棵树的构建每个节点应该具备的属性,我们要想构建一颗avl树,我们就要以上图为基准,先创建节点,代码如下:

template<class K,class V>
struct AVLTreeNode
{AVLTreeNode(const pair<K, V> kv):_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _bf(0){}AVLTreeNode* _left;AVLTreeNode* _right; AVLTreeNode* _parent;std::pair<K, V>_kv;int bf; //平衡因子};

然后就是组织的过程,我们只要创建根节点,然后增删改查就OK了,我们要注意平衡因子的约束,比如我们要按顺序插入1 2 3 4 5 6有了平衡因子的约束,插入题如下所示:

由上图可知,同样的结点,由于插入方式不同导致树的高度也有所不同。特别是在带插入结点个数很多且正序的情况下,会导致二叉树的高度是O(N),而AVL树就不会出现这种情况,树的高度始终是O(lgN).高度越小,对树的一些基本操作的时间复杂度就会越小。这也就是我们引入AVL树的原因。

2.AVL树插入的基本操作

 插入节点的基本操作跟搜索二叉树差不多,但是需要收到平衡因子的约束,下面是插入过程平衡因子的注意事项;

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

 2.1逻辑抽象图

AVL 树的 旋转操作 比较复杂,需要考虑多种形状、多种情况,为了方便理解,将 部分节点 视为一个整体(抽象化),主要看高度 h进行旋转操作,可以得出下面这个抽象图

通过逻辑图可以方便我们对avl树插入的情况做清晰的分析。

2.2插入流程

  1. 判断根是否为空,如果为空,则进行第一次插入,成功后返回 true
  2. 找到合适的位置进行插入,如果待插入的值比当前节点值大,则往 右 路走,如果比当前节点值小,则往 左 路走
  3. 判断父节点与新节点的大小关系,根据情况判断链接至 左边 还是 右边
  4. 更新平衡因子,然后判断是否需要进行 旋转 调整高度

代码框架如下,具体旋转情况具体分析实现 

//插入节点
bool Insert(const std::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->_right;}else if (cur->_kv.first > kv.first){parent = cur;cur = cur->_left;}elsereturn false;}//创建新节点,链接cur = new Node(kv);if (parent->_kv.first < kv.first){parent->_right = cur;cur->_parent = parent;}else{parent->_left = cur;cur->_parent = parent;}//根据平衡因子判断是否需要旋转while (parent){//更新平衡因子if (parent->_right == cur)parent->_bf++;elseparent->_bf--;//判断是否需要调整//……}return true;
}

 

2.3左单旋

左单旋的适用场景如下:在根的右子树中出现 平衡因子 为 1 的情况下,仍然往右侧插入节点,插入后会导致 右子树 中某个节点 平衡因子 值为 2 ,此时就需要使用 左单旋 降低高度。

在Z的右枝插入节点会导致z的平衡因子++,R的平衡因子会增加成2,则不满足avl树的要求,我们就要对R节点左旋,具体代码如下:

//左单旋
void RotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;//先将 subR 的左孩子移交给父亲parent->_right = subRL;if (subRL != nullptr)subRL->_parent = parent;Node* pparent = parent->_parent;//易错点:忘记更改原父亲的链接关系subR->_left = parent;parent->_parent = subR;//再将父亲移交给 subR,subR 成为新父亲if (parent == _root){//如果原父亲为根,那么此时需要更新 根subR->_parent = nullptr;_root = subR;}else{//单纯改变链接关系if (pparent->_right == parent)pparent->_right = subR;elsepparent->_left = subR;subR->_parent = pparent;}//更新平衡因子parent->_bf = subR->_bf = 0;
}

旋转其实就是改变链接过程,更改的链接示意图如下

助记:搜索二叉树的右数>左树 则     1.parent->right = cur->left 2.cur->left = parent

注意: subRL 可能是 nullptr,在改变其链接关系时,需要判断一下,避免空指针解引用行为;parent 可能是 根节点,subR 在链接后,需要更新 根节点;左单旋后,parentsubR 的平衡因子都可以更新为 0,此时是很平衡的

2.4右单旋

右单旋的适用场景如下:在根的左子树中出现 平衡因子 为 1 的情况下,仍然往左侧插入节点,插入后会导致 左子树 中某个节点 平衡因子 值为 2 ,此时就需要使用 右单旋 降低高度

右单旋 的场景与 左单旋 如出一辙,不过方向不同而已

右单选的代码,复制左单旋改变链接指向就行

//右单旋
void RotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;//先将 subL 的右孩子移交给父亲parent->_left = subLR;if (subLR != nullptr)subLR->_parent = parent;Node* pparent = parent->_parent;subL->_right = parent;parent->_parent = subL;//再将父亲移交给 subL,subL 成为新父亲if (parent == _root){//如果原父亲为根,那么此时需要更新 根subL->_parent = nullptr;_root = subL;}else{//单纯改变链接关系if (pparent->_right == parent)pparent->_right = subL;elsepparent->_left = subL;subL->_parent = pparent;}//更新平衡因子parent->_bf = subL->_bf = 0;
}

助记:搜索二叉树的右数>左树 则     1.parent->left = cur->right 2.cur-> right = parent

注意: subLR 可能是 nullptr,在改变其链接关系时,需要判断一下,避免空指针解引用行为;parent 可能是 根节点,subL 在链接后,需要更新 根节点;右单旋后,parentsubLR 的平衡因子都可以更新为 0,此时是很平衡的

2.5右左双旋

当值插入 右子树的右侧 时,可能引发 左单旋,当值插入 左子树的左侧 时,则可能引发 右单旋

如果插入的是 右子树的左侧 或 左子树的右侧 时,则可能引发 双旋

比如 插入右子树的左侧 时,单单凭借 左单旋 无法解决问题,需要 先进行 右单旋,再进行 左单旋 才能 降低高度,这一过程就成为 双旋(右左双旋)

//右左双旋
void RotateRL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;int BF = subRL->_bf;//先右单旋RotateR(subR);//再左单旋RotateL(parent);//根据不同的情况更新平衡因子if(BF == 0){parent->_bf = subR->_bf = 0;}else if (BF == 1){parent->_bf = -1;subR->_bf = 0;subRL->_bf = 0;}else if (BF == -1){parent->_bf = 0;subR->_bf = 1;subRL->_bf = 0;}else{//非法情况std::cerr << "此处的平衡因子出现异常!" << std::endl;assert(false);	//直接断言报错}
}

右左双旋 的抽象图 旋转 流程如下(动图)

注:双旋 部分的动图省略了部分细节,着重展现 高度降低 的现象

 

右左双旋 逻辑:

确定 parent、subR、subRL
subRL 的右子树托付给 subR,左子树托付给 parent
subRL 向上提,整体高度下降
需要特别注意平衡因子的调整
双旋 的 平衡因子 调整需要分类讨论:
情况一:新节点插入至右子树左侧后,subRL 平衡因子变为 0,此时树变得更加平衡了,因此 parent、subR、subRL 三者的平衡因子都为 0

情况二:新节点插入至右子树的左侧后,subRL 平衡因子变为 -1,证明 新节点插入至 subRL 的左边,并且右边没有东西,旋转后,将新节点托付给 parent 后,parent 变得平衡了,但 subR 因没有分到节点,因此导致其左侧失衡,平衡因子变为 1subRL 平衡,为 0(这其实就是动图展示的情况)

情况三:新节点插入至右子树的左侧后,subRL 平衡因子变为 1,证明 新节点插入至 subRL 的右边,并且左边没有东西,旋转后,parent 没有分到节点,subR 分到了,subRL 为平衡,因此 parent 的平衡因子为 -1,subR subRL 的平衡因子都是 0

经过这样分析后,就能得到代码中的判断逻辑

注意: 先要右单旋,才左单旋;平衡因子的更新需要分类讨论

 

2.6左右双旋 

当节点插入至 左子树的右侧 时,会触发 左右双旋,需要 先进行 左单旋,再进行 右单旋 才能降低高度

//左右双旋
void RotateLR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;int BF = subLR->_bf;//先左单旋RotateL(subL);//再右单旋RotateR(parent);//根据不同的情况更新平衡因子if (BF == 0){parent->_bf = subL->_bf = 0;}else if (BF == 1){parent->_bf = 0;subL->_bf = -1;subLR->_bf = 0;}else if (BF == -1){parent->_bf = 1;subL->_bf = 0;subLR->_bf = 0;}else{//非法情况std::cerr << "此处的平衡因子出现异常!" << std::endl;assert(false);	//直接断言报错}
}

左右双旋 的 旋转 流程如下图所示(动图)

 

 

左右双旋 逻辑:

确定 parent、subL、subLR
将 subLR 的右子树托付给 parent,左子树托付给 subL
subLR 向上提,整体高度下降
需要特别注意平衡因子的调整
调整逻辑与 右左双旋 差不多

情况一:新节点插入至左子树右侧后,subLR 平衡因子变为 0,此时树变得更加平衡了,因此 parent、subL、subLR 三者的平衡因子都为 0

情况二:新节点插入至左子树的右侧后,subLR 平衡因子变为 -1,证明 新节点插入至 subLR 的左边,并且右边没有东西,旋转后,将新节点托付给 subL 后,subL 变得平衡了,但 parent 因没有分到节点,因此导致其右侧失衡,平衡因子变为 1,subLR 平衡,为 0

情况三:新节点插入至左子树的右侧后,subLR 平衡因子变为 1,证明 新节点插入至 subLR 右边,并且左边没有东西,旋转后,subL 没有分到节点,parent 分到了,subLR 为平衡,因此 subL 的平衡因子为 -1,parent 和 subLR 的平衡因子都是 0(动图中演示的就是情况三)

总的来说,双旋 需要慎重考虑 平衡因子 的调整

 小结:旋转情况:

  • 插入至 右右 时,左单旋
  • 插入至 左左 时,右单旋
  • 插入至 右左 时,右左双旋
  • 插入至 左右 时,左右双旋

3.AVL树的检验技巧 

   3.1、检验依据

 如何检验自己的 AVL 树是否合法? 答案是通过平衡因子检查 

平衡因子 反映的是 左右子树高度之差,计算出 左右子树高度之差 与当前节点的 平衡因子 进行比对,如果发现不同,则说明 AVL 树 非法或者如果当前节点的 平衡因子 取值范围不在 [-1, 1] 内

3.2、检验方法

统计 二叉树子树高度 很简单,只需要在 检验合法性函数 中调用即可

 

//验证是否为 AVL 树
bool IsAVLTree()
{return _IsAVLTree(_root);
}//获取高度
size_t getHeight()
{return _getHeight(_root);
}bool _IsAVLTree(Node* root)
{if (root == nullptr)return true;//计算左右子树的高度size_t leftTreeH = _getHeight(root->_left);size_t rightTreeH = _getHeight(root->_right);//计算差值int diff = rightTreeH - leftTreeH;if (diff != root->_bf || root->_bf < -1 || root->_bf > 1){std::cerr << "当前节点出现了问题: " << root->_kv.first<< " | " << root->_bf << std::endl;return false;}return _IsAVLTree(root->_left) && _IsAVLTree(root->_right);
}size_t _getHeight(Node* root)
{if (root == nullptr)return 0;size_t leftH = _getHeight(root->_left);size_t rightH = _getHeight(root->_right);return 1 + std::max(leftH, rightH);
}

通过一段简单的代码,随机插入 10000 个节点,判断 是否合法 及当 AVL 树的 高度

void AVLTreeTest2()
{srand((size_t)time(NULL));AVLTree<int, int> av;for (int i = 0; i < 10000; i++){int val = rand() % 10000 + i;av.Insert(val, val);}cout << "检查AVL树: " << av.IsAVLTree() << endl << "高度为:" << av.getHeight() << endl;
}

3.3、AVL树的性能

AVL 树是一棵 绝对平衡 的二叉树,对高度的控制极为苛刻,稍微有点退化的趋势,都要被旋转调整,这样做的好处是 严格控制了查询的时间,查询速度极快,约为 logN

但是过度苛刻也会带来一定的负面影响,比如涉及一些 结构修改 的操作时,性能非常低下,更差的是在 删除 时,因为从任意位置破坏了 二叉搜索树 及 AVL 树的属性,有可能会引发连锁旋转反应,导致一直 旋转 至 根 的位置(旋转比较浪费时间)

AVL 树性能很优秀,如果在存储大量不需要修改的静态数据时,用 AVL 树是极好的,但在大多数场景中,用不到这么极限的性能,此时就需要一种 和 AVL 树差不多,但又没有那么严格 的 平衡二叉搜索树 了

 

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

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

相关文章

LeetCode、901. 股票价格跨度【中等,单调栈】

文章目录 前言LeetCode、901. 股票价格跨度【中等&#xff0c;单调栈】题目链接及分类思路思路1&#xff1a;暴力思路2&#xff1a;单调栈写法优化&#xff1a;单调栈简化写法(数组替代栈集合) 资料获取 前言 博主介绍&#xff1a;✌目前全网粉丝2W&#xff0c;csdn博客专家、…

力扣精选算法100道——【模板】前缀和 (二维)

目录 &#x1f388;题目解析 &#x1f388;算法原理 &#x1f388;实现代码 二维前缀和【模板】 &#x1f388;题目解析 上一题我们讲述了一维的前缀和求法。 第一行三个参数&#xff0c;n是行数3&#xff0c;m是列数4&#xff0c;q3代表查询次数 接下来就是n行m列的矩阵…

《Java 简易速速上手小册》第5章:Java 开发工具和框架(2024 最新版)

文章目录 5.1 Maven 和 Gradle - 构建你的堡垒5.1.1 基础知识5.1.2 重点案例&#xff1a;使用 Maven 构建一个简单的 Java 应用5.1.3 拓展案例 1&#xff1a;使用 Gradle 构建一个 Spring Boot 应用5.1.4 拓展案例 2&#xff1a;使用 Maven 管理多模块项目 5.2 Spring 框架 - 你…

【数据分享】2000~2018年青藏高原0.05°逐日积雪深度数据集

各位同学们好&#xff0c;今天和大伙儿分享的是2000~2018年青藏高原0.05逐日积雪深度数据集。如果大家有下载处理数据等方面的问题&#xff0c;可以添加我的微信交流~ 闫大江, 马宁, 张寅生. (2021). 青藏高原0.05逐日积雪深度数据集&#xff08;2000-2018&#xff09;. 国家青…

Java并发基础:PriorityBlockingQueue全面解析!

内容概要 PriorityBlockingQueue类能高效处理优先级任务&#xff0c;确保高优先级任务优先执行&#xff0c;它内部基于优先级堆实现&#xff0c;保证了元素的有序性&#xff0c;同时&#xff0c;作为BlockingQueue接口的实现&#xff0c;它提供了线程安全的队列操作&#xff0…

[缓存] 1. 缓存共性问题

1. 缓存的作用 为什么需要缓存呢&#xff1f;缓存主要解决两个问题&#xff0c;一个是提高应用程序的性能&#xff0c;降低请求响应的延时&#xff1b;一个是提高应用程序的并发性。 2. 缓存的分类 本地缓存&#xff0c;分布式缓存 3. 缓存数据分类 3.1 数据缓存 程序数据直接…

Linux_环境变量_命令行参数

一.环境变量 在Linux中自己写的程序必须要带路径才能运行&#xff0c;相对路径或是绝对路径&#xff0c;但是像ls pwd这样的程序&#xff0c;不带路径也能运行。当你想要运行一个程序时&#xff1a; 如果带有路径的话&#xff0c;则直接将对应路径的程序加载进内存&#xff0…

七天入门大模型 :提示词工程 Prompt Engineering,最全的总结来了!

文章目录 技术交流群用通俗易懂方式讲解系列引 言LLM 的超参配置Prompt Engineering指令主要内容少样本学习更加明确的提示善用分隔符思维链提示对输出格式的明确要求 最佳实践案例1. Agent场景&#xff1a;使用prompt实现agent create2. Agent场景&#xff1a;使用system mess…

腾讯云4核8G服务器3年600元?

腾讯云4核8G服务器3年600元&#xff1f;目前的价格是轻量应用服务器4核8G12M带宽一年446元、646元15个月&#xff0c;云服务器CVM标准型S5实例4核8G配置价格15个月1437.3元&#xff0c;5年6490.44元&#xff0c;标准型SA2服务器1444.8元一年&#xff0c;在txy.wiki可以查询详细…

thinkphp5.0提示不支持redis,not support: redis

安装PHP扩展 例如宝塔&#xff0c;其他环境请用命令行&#xff0c;安装 redis配置完成以后&#xff0c;修改php.ini把redis扩展打开即可&#xff0c;重启环境

英伟达市值超越谷歌!老黄隔空回应Altman的巨资筹款计划:没必要,真的没必要!

凭借算力上的霸主地位&#xff0c;英伟达正稳步成为科技领域的下一个巨头&#xff0c;在不久的15个月前&#xff0c;英伟达的市值还不足3000亿美元。然而&#xff0c;截至昨日&#xff0c;英伟达股价飙升使其市值达到了1.83万亿美元&#xff0c;超越了Alphabet&#xff08;谷歌…

解锁未来:探秘Zxing二维码技术的神奇世界

解锁未来&#xff1a;探秘Zxing二维码技术的神奇世界 1. 引言 在当今数字化和智能化的社会中&#xff0c;二维码技术已经成为人们生活中不可或缺的一部分。从商品购物、支付结算到健康码、门票核销&#xff0c;二维码无处不在&#xff0c;极大地方便了人们的生活和工作。而Zx…