【C++干货铺】会旋转的二叉树——AVLTree

=========================================================================

个人主页点击直达:小白不是程序媛

C++系列专栏:C++干货铺

代码仓库:Gitee

=========================================================================

目录

前言

AVL树

AVL树的概念

AVL树结点的定义

AVL树的插入

寻找插入结点的位置

修改平衡因子

 AVL树的旋转

右单旋

左单旋

先右旋再左旋 

先左旋再右旋

 AVL树的验证

AVL树的删除(了解)

AVL树的性能


前言

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


AVL树

AVL树的概念

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

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

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

AVL树结点的定义

template <class k, class v>
struct AVLTreeNode
{AVLTreeNode<k, v>* _left;//指向右孩子AVLTreeNode<k, v>* _right;//指向左孩子AVLTreeNode<k, v>* _parent;//键值对pair<k, v> _kv;//平衡因子int _bf;//构造函数AVLTreeNode(const pair<k, v>& kv):_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _bf(0){}
};

AVL树的插入

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

1. 按照二叉搜索树的方式插入新节点
2. 调整节点的平衡因子                                                                                                              3. 将插入后不符合的子树进行旋转调整

寻找插入结点的位置

AVL树是基于搜索二叉树的,搜索二叉树的原则是左小右大。根据这个规则可以找到插入结点的位置。

注:搜索二叉树是没有两个相同的结点的。

//根节点是否为空,如果为空开辟新的结点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;}else{//AVL树是基于搜索二叉树的不会存在相等的情况return 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){//当新增结点为左子树时相当于左子树高度加1//减数不变,被减数增加,结果减小if (cur == parent->_left){parent->_bf--;}else{//当新增结点为右子树时相当于右子树高度加1//减数增加,被减数不变,结果增大parent->_bf++;}//添加后左右子树高度相等//对父节点往上的结点的平衡因子都没有影响if (parent->_bf == 0){break;}//添加节点后对树的高度发生变化//继续向上修改平衡//对新增结点的父节点的父节点的平衡因子进行修改else if (parent->_bf == 1 || parent->_bf == -1){cur = parent;parent = parent->_parent;}}

 AVL树的旋转

结点的插入必然会影响父代及以上结点的平衡因子,从插入结点的父节点往上修改平衡因子,一定会出现平衡因子异常(超过1/0/-1),这时候就要进行旋转操作了。根据插入节点的位置不同,AVL树的旋转分为四种

            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);}

右单旋

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

抽象图 

 

	//右单璇void RotateR(Node* 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 (parent == _root){_root = subL;subL->_parent = nullptr;}else{//更新父亲节点为旋转结点if (parent == parentParent->_left){parentParent->_left = subL;}else {parentParent->_right = subL;}//更新旋转结点的父亲结点,此时选装结点彻底为父亲结点subL->_parent = parentParent;}//更新平衡因子subL->_bf = parent->_bf = 0;}

左单旋

新节点插入较高右子树的右侧

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 == nullptr;}if (parent == _root){_root = subR;subR->_parent = nullptr;}else{if (parent == parentParent->_left){parentParent->_left = subR;}else if (parent == parentParent->_right){parentParent->_right = subR;}subR->_parent = parentParent;}parent->_bf = subR->_bf = 0;}

先右旋再左旋 

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

抽象图 

 

	void RotateRL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;RotateR(parent->_right);RotateL(parent);if (bf == 0){// subRL自己就是新增parent->_bf = subR->_bf = subRL->_bf = 0;}else if (bf == -1){// subRL的左子树新增parent->_bf = 0;subRL->_bf = 0;subR->_bf = 1;}else if (bf == 1){// subRL的右子树新增parent->_bf = -1;subRL->_bf = 0;subR->_bf = 0;}else{assert(false);}}

先左旋再右旋

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

 

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

AVL树的验证

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

1. 验证其为二叉搜索树

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

	void _InOrder(Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_kv.first << " ";_InOrder(root->_right);}


2. 验证其为平衡树

  • 每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
  • 节点的平衡因子是否计算正确
	int _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;}bool _IsBalance(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 << "平衡因子异常" << endl;return false;}return abs(rightHeight - leftHeight) < 2&& _IsBalance(root->_left)&& _IsBalance(root->_right);}

AVL树的删除(了解)

 因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不
错与删除不同的时,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。
具体实现大家可参考《算法导论》或《数据结构-用面向对象方法与C++描述》殷人昆版。


AVL树的性能

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


今天的对数据结构中基于二叉搜索树的AVL树的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,个人主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是我前进的动力!

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

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

相关文章

Kubernetes (十三) 存储——持久卷-动静态分配

一. 简介 二. NFS持久化存储步骤&#xff08;静态分配&#xff09; 1. 集群外…

数字信号处理教程学习笔记1-第2章时域中的离散信号和系统

信号处理的任务示意方框图 模拟信号和数字信号分别是啥样的,有啥区别

freetos day1

作业&#xff1a;使用计数型信号量设计&#xff1a;生产者和消费者模型 总结今天学习的API函数&#xff0c;写出函数参数和返回值的意思&#xff0c;并且说明函数功能 1 2 3

如何结合主从复制,不停服情况下解决分库分表

首先我们要知道主从复制和分库分表两个概念&#xff0c;在此基础上可以将问题分为几个阶段来执行&#xff0c;参考了公众号 双写读老 双写双读 写新读新

WEB 3D技术 three.js 3D贺卡(1) 搭建基本项目环境

好 今天 我也是在网上学的 带着大家一起来做个3D贺卡 首先 我们要创建一个vue3的项目、 先创建一个文件夹 装我们的项目 终端执行 vue create 项目名称 例如 我的名字想叫 greetingCards 就是 vue create greetingcards因为这个名录 里面是全部都小写的 然后 下面选择 vue3 …

代码随想录算法训练营Day21| 93.复原IP地址、78.子集、90.子集||

LeetCode 93 复原 IP 地址 本题思路&#xff1a;最重要的是想到一个收集结果的条件&#xff0c;也就是终止条件。 当 . 的个数达到三个时候&#xff0c;并且&#xff0c;判断最后剩余的是否符合要求&#xff0c;如果符合&#xff0c;说明整个ip地址可以&#xff0c;就加入到结…

【数据结构】C语言实现共享栈

共享栈的C语言实现 导言一、共享栈1.1 共享栈的初始化1.2 共享栈的判空1.3 共享栈的入栈1.3.1 空指针1.3.2 满栈1.3.3 入栈空间错误1.3.4 正常入栈1.3.5 小结 1.4 共享栈的查找1.5 共享栈的出栈1.6 共享栈的销毁 二、共享栈的实现演示结语 导言 大家好&#xff0c;很高兴又和大…

腾讯云主机价格表和优惠活动汇总(2024年更新)

腾讯云服务器租用价格表&#xff1a;轻量应用服务器2核2G3M价格62元一年、2核2G4M价格118元一年&#xff0c;540元三年、2核4G5M带宽218元一年&#xff0c;2核4G5M带宽756元三年、轻量4核8G12M服务器446元一年、646元15个月&#xff0c;云服务器CVM S5实例2核2G配置280.8元一年…

检索增强生成RAG

文章目录 RAG解释混合检索重排序Rerank为什么需要RerankHNSW带来的随机性问题 当前大模型处理长输入的水平依然不够大模型如何处理长输入&#xff1f;重要信息位置为什么会影响大模型的效果LangChain的解决方案-检索后重新排序文档 召回模式N选1召回模式多路召回模式 摘要 在RA…

记录一次git merge后发现有些文件不对的问题,排查过程

分支进行merge&#xff08;A merge到B&#xff09;之后&#xff0c;发现string.xml中有些字段的值没有merge过来&#xff0c;一开始还以为自己是自己merge错误&#xff0c;检查了一遍自己的merge操作没有问题。 那为啥没有merge过来呢&#xff1f;有一种可能是&#xff0c;merg…

排序——计数排序

文章目录 概念思路绝对映射&#xff1a;相对映射 代码实现特性结果演示 概念 计数排序是一个非基于比较的排序算法&#xff0c;该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时&#xff0c;它的复杂度为Ο(nk)&#xff08;其中k是整数的范围…

内网穿透[让你在家里也能榨干学校的服务器]Yep!

内网穿透 问题&#xff1a;什么是内网穿透&#xff0c;内网穿透的作用是什么&#xff1f; 前提&#xff01;&#xff01;&#xff01;&#xff01;你得拥有超级管理员的权限&#xff0c;比如root&#xff0c;不然后面的一切免提&#xff01; 应用场景如下&#xff1a;比如你…

中科院国产多语言大模型-YAYI2开源!家族AI应用场景全覆盖!

项目简介 YAYI 2 是中科闻歌研发的新一代开源大语言模型&#xff0c;中文名&#xff1a;雅意&#xff0c;采用了超过 2 万亿 Tokens 的高质量、多语言语料进行预训练。 开源地址&#xff1a;https://github.com/wenge-research/YAYI2 YAYI2-30B是其模型规模&#xff0c;是基…

文件上传进阶绕过(二)4个技巧和靶场实战

★★免责声明★★ 文章中涉及的程序(方法)可能带有攻击性&#xff0c;仅供安全研究与学习之用&#xff0c;读者将信息做其他用途&#xff0c;由Ta承担全部法律及连带责任&#xff0c;文章作者不承担任何法律及连带责任。 0、环境准备 请移步《文件上传靶场实战&#xff1a;upl…

C++ | 四、指针、链表

指针 指针用来储存地址定义方式&#xff0c;int *ptr;&#xff0c;使用*来表示所定义的变量是指针取地址符&#xff0c;ptr &a;&#xff0c;通过&来取得一个普通变量的地址&#xff0c;并储存到指针中取值&#xff08;解引用&#xff09;&#xff0c;想要取得一个指针…

【华为OD机试真题2023CD卷 JAVAJS】查找一个有向网络的头节点和尾节点

华为OD2023(C&D卷)机试题库全覆盖,刷题指南点这里 查找一个有向网络的头节点和尾节点 知识点图DFS搜索 时间限制:1s 空间限制:256MB 限定语言:不限 题目描述: 给定一个有向图,图中可能包含有环,图使用二维矩阵表示,每一行的第一列表示起始节点,第二列表示终止节…

Java 线程

1. 实现多线程的 2 种方式 Oracle 官网的文档中给出了 2 种实现多线程的方式&#xff1a; 实现 Runnable 接口&#xff1b;继承 Thread 类。 以上两种方式都会调用 Thread.run() 方法&#xff0c;区别是&#xff1a; 实现 Runnable 接口&#xff0c;只是执行 Thread.run() …

【新】Unity Meta Quest MR 开发(一):Passthrough 透视配置

文章目录 &#x1f4d5;教程说明&#x1f4d5;配置透视的串流调试功能&#x1f4d5;第一步&#xff1a;设置 OVRManager&#x1f4d5;第二步&#xff1a;添加 OVRPassthroughLayer 脚本&#x1f4d5;第三步&#xff1a;在场景中添加虚拟物体&#x1f4d5;第四步&#xff1a;设置…

202406读书笔记|《沉睡的线条世界》——翻山越岭,只为与你分享点滴的快乐

《沉睡的线条世界》登登登Dn绘著&#xff0c;简简单单的小画&#xff0c;简简单单的线条&#xff0c;简简单单的语言&#xff0c;温馨又有一点暖心。 怎样的你都好&#xff0c;做最真实的自己。 部分节选如下&#xff1a; 愿你我永远有热情&#xff0c;永远能为生活的每一个小惊…

阳光抑郁症测试

大部分人对抑郁症的理解&#xff0c;就是每天无精打采&#xff0c;死气沉沉&#xff0c;可实际上&#xff0c;还有一种阳光抑郁症&#xff0c;完全不是这个样子。这种抑郁症的人&#xff0c;做事情非常有活力&#xff0c;魅力十足&#xff0c;给人感觉十分有自信&#xff0c;但…