【C++】平衡二叉搜索(AVL)树的模拟实现
一、 AVL树的概念
map、multimap、set、multiset 在其文档介绍中可以发现,这几个容器有个共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N)O(N)O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。
因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年
发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
- 它的左右子树都是AVL树
- 左右子树高度之差(简称平衡因子,balance factor)的绝对值不超过1(-1/0/1)
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在O(log2n)O(log_2 n)O(log2n),搜索时间复杂度O(log2nlog_2 nlog2n)。
二、AVL树节点的定义
AVL树的节点是三叉链结构:即parent、left和right,它们分别指向当前节点的父节点、左子节点和右子节点。通过这种方式,可以在O(1)O(1)O(1)的时间内找到一个节点的父节点、左子节点和右子节点。
namespace 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){}};template<class K, class V>class AVLTree{typedef AVLTreeNode<K, V> Node;public:private:Node* _root = nullptr;};
}
三、AVL树的插入
AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:
- 按照二叉搜索树的方式插入新节点
- 调整节点的平衡因子
插入在左平衡因子-1,插入在右平衡因子+1
是否继续更新的依据:parent所在子树的高度是否变化
-
parent->_bf == 0
说明之前parent->_bf
是 1 或者 -1 说明之前parent一边高一边低,这次插入填上矮的那边,parent所在子树高度不变,不需要继续往上更新
-
parent->_bf == 1
或-1
说明之前是parent->_bf = 0
,两边一样高,现在插入一边更高了,parent所在子树高度变了,继续往上更新
-
parent->_bf == 2
或-2
,说明之前parent->_bf == 1
或者-1
,现在插入严重不平衡,违反规则,就地处理–旋转
bool insert(const pair<K, V>& kv)
{// 1. 先按照二叉搜索树的规则将节点插入到AVL树中// 空树直接构建根if (_root == nullptr){_root = new Node(kv);return true;}Node* parent = nullptr;Node* cur = _root;while (cur){if (kv.first > cur->_kv.first) // 大了往右边走{parent = cur;cur = cur->_right;}else if (kv.first < cur->_kv.first) // 小了往左边走{parent = cur;cur = cur->_left;}else{return false;// 相等不插入}}//开始插入cur = new Node(kv);// 新插入的节点// 小的插入左,大的插入右if (kv.first < parent->_kv.first){parent->_left = cur;cur->_parent = parent;// 三叉链,不要忘记更新父指针}else{parent->_right = cur;cur->_parent = parent;}// 2. 新节点插入后,AVL树的平衡性可能会遭到破坏// 此时需要更新平衡因子,并检测是否破坏了AVL树的平衡性while (parent) // parent为空,也就更新到根停止{// 更新平衡因子// 新增在左,parent->bf--;// 新增在右,parent->bf++;if (cur == parent->_left){parent->_bf--;}else{parent->_bf++;}//检测平衡因子if (parent->_bf == 0){break;// 无需继续更新}else if (parent->_bf == 1 || parent->_bf == -1){// 插入前parent的平衡因子是0,插入后parent的平衡因为为1 或者 - 1 ,说明以parent为根的二叉树// 的高度增加了一层,因此需要继续向上调整cur = parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2){// parent的平衡因子为-2/2,违反了AVL树的平衡性// 需要对以 parent 为根的树进行 旋转 处理// 旋转break; // 旋转完成后,原 parent 为根的子树高度降低,已经平衡,不需要再向上更新}else{assert(false); // 平衡因子异常:绝对值大于2}}return true;
}
四、AVL树的旋转
在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时可通过旋转调整树的结构,使之平衡化。
旋转的目的:
- 让这颗子树左右高度不超过1
- 旋转过程中继续保持是搜索树
- 更新调整孩子节点的平衡因子
- 让这颗子树的高度跟插入前保持一致
根据节点插入位置的不同,AVL树的旋转分为四种:
- 新节点插入 较高 左左左子树的左左左侧—左左:右单旋
在插入前,图中AVL树是平衡的,新节点插入到30的左子树(注意:此处不是左孩子,图中a/b/c是高度为 h 的AVL子树)中,30左子树增加了一层,导致以60为根的二叉树不平衡
要让60平衡,只能将60左子树的高度减少一层,右子树增加一层,即将左子树往上提,这样60转下来,因为60比30大,只能将其放在30的右子树,而如果30有右子树,右子树根的值一定大于30,小于60,只能将其放在60的左子树,旋转完成后,更新节点的平衡因子即可。
在旋转过程中,有以下几种情况需要考虑:
- 30节点的右孩子可能存在,也可能不存在
- 60可能是根节点,也可能是子树,如果是根节点,旋转完成后,要更新根节点,如果是子树,可能是某个节点的左子树,也可能是右子树
这里举一些详细的例子进行画图,考虑各种情况,加深旋转的理解
h == 0,则a/b/c是空树:
h == 1:
h == 2的情况已经有很多种了,随着h的增加情况会越来越复杂
看图写代码:
void RotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;// 30的右变成60的左parent->_left = subLR;if (subLR != nullptr) // 30的右不为空,更新_parent指针{subLR->_parent = parent;}Node* ppNode = parent->_parent;// 60变成30的右parent = subL->_right;parent->_parent = subL;//不要忘记更新parent的父指针if (_root == parent) // parent就是根//if (ppNode == nullptr) //也可以使用这个判断条件{_root = subL;_root->_parent = nullptr;}else // parent是左或右子树{// parent是左就把subL链接到左,是右就链接到右if (parent == ppNode->_left){ppNode->_left = subL;}else{ppNode->_right = subL;}subL->_parent = ppNode;// 同样不要忘记更新subL的父指针}// 最后更新parent和subL的平衡因子parent->_bf = subL->_bf = 0;
}
- 新节点插入较高右右右子树的右右右侧—右右:左单旋
左单旋实现及情况考虑可参考右单旋
h == 0的情况:
h == 1:
void RotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;// 60的左变成30的右parent->_right = subRL;// 更新subRL的父指针if (subRL){subRL->_parent = parent;}Node* ppNode = parent->_parent;// 30变成60的左subR->_left = parent;parent->_parent = subR;//if (_root == parent)if (ppNode == nullptr){_root = subR;_root->_parent = nullptr;}else{if (parent == ppNode->_left){ppNode->_left = subR;}else{ppNode->_right = subR;}subR->_parent = ppNode;}parent->_bf = subR->_bf = 0;
}
像下图的情况简单的单旋已经不能正确调整平衡,需要使用双旋(不同轴点的单旋):
- 新节点插入较高左左左子树的右右右侧—左右:先左单旋再右单旋
a/d是高度为 h 的AVL树
b/c是高度为 h - 1 的AVL树
h == 0:
h == 1:
看图写代码:
void RotateLR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;// 对30左单旋,对90右单旋RotateL(parent->_left);RotateR(parent);// 最后更新平衡因子if (bf == 0) // subLR自己是新增{parent->_bf = 0;subL->_bf = 0;subLR->_bf = 0;}else if (bf == -1) // 在subLR的左新增{parent->_bf = 1;subL->_bf = 0;subLR->_bf = 0;}else if (bf == 1) // 在subLR的右新增{parent->_bf = 0;subL->_bf = -1;subLR->_bf = 0;}else{assert(false);// 异常处理}
}
- 新节点插入较高右右右子树的左左左侧—右左:先右单旋再左单旋
h == 0:
h == 1:
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 = 0;subR->_bf = 0;subRL->_bf = 0;}else if (bf == -1){parent->_bf = 0;subR->_bf = 1;subRL->_bf = 0;}else if (bf == 1){parent->_bf = -1;subR->_bf = 0;subRL->_bf = 0;}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 为根的子树高度降低,已经平衡,不需要再向上更新。
insert 时平衡因子检测的整体代码:
while (parent) // parent为空,也就更新到根停止
{// 更新平衡因子// 新增在左,parent->bf--;// 新增在右,parent->bf++;if (cur == parent->_left){parent->_bf--;}else{parent->_bf++;}//检测if (parent->_bf == 0){break;// 无需继续更新}else if (parent->_bf == 1 || parent->_bf == -1){// 插入前parent的平衡因子是0,插入后parent的平衡因为为1 或者 - 1 ,说明以parent为根的二叉树// 的高度增加了一层,因此需要继续向上调整cur = parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2){// parent的平衡因子为-2/2,违反了AVL树的平衡性// 需要对以 parent 为根的树进行 旋转 处理if (parent->_bf == -2 && cur->_bf == -1) // 右单旋{RotateR(parent);}else if (parent->_bf == 2 && cur->_bf == 1) // 左单旋{RotateL(parent);}else if (parent->_bf == -2 && cur->_bf == 1) // 左右双旋{RotateLR(parent);}else if (parent->_bf == 2 && cur->_bf == -1) // 右左双旋{RotateRL(parent);}else{assert(false);// 平衡因子异常}break; // 旋转完成后,原 parent 为根的子树高度降低,已经平衡,不需要再向上更新}else{assert(false); // 平衡因子异常:绝对值大于2}
}
AVL树的整体代码:AVL树的简单模拟实现
五、AVL树的验证
AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:
- 验证其为二叉搜索树
如果中序遍历可得到一个有序的序列,就说明为二叉搜索树 - 验证其为平衡树
每个节点子树高度差的绝对值不超过1(注意节点中如果有平衡因子,还需验证节点的平衡因子是否计算正确
int Height(Node* root)
{if (root == nullptr)return 0;int lh = Height(root->_left);int rh = Height(root->_right);return lh > rh ? lh + 1 : rh + 1;
}bool IsBalance()
{return IsBalance(_root);
}bool IsBalance(Node* root)
{if (root == nullptr){return true;}int leftHeight = Height(root->_left);int rightHeight = Height(root->_right);if (rightHeight - leftHeight != root->_bf){std::cout << root->_kv.first << " 平衡因子异常" << std::endl;return false;}return abs(rightHeight - leftHeight) < 2&& IsBalance(root->_left)&& IsBalance(root->_right);
}
六、AVL树的性能
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log2(N)log_2 (N)log2(N)。
但是如果要对AVL树做一些结构修改的操 作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。
相关文章:

【C++】平衡二叉搜索(AVL)树的模拟实现
一、 AVL树的概念 map、multimap、set、multiset 在其文档介绍中可以发现,这几个容器有个共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树…...

[2019红帽杯]childRE
题目下载:下载 参考:re学习笔记(24)BUUCTF-re-[2019红帽杯]childRE_Forgo7ten的博客-CSDN博客 这道题涉及到c函数的修饰规则,按照规则来看应该是比较容易理解的。上面博客中有总结规则,可以学习一下。 载…...
2D图像处理:九点标定_下(机械手轴线与法兰轴线不重合)(附源码)
文章目录 2. 机械手轴线与法兰轴线不重合2.1 两次拍照避免标定旋转中心2.2 旋转中心标定2.3 非标定中心的方法2.3.1 预备内容-点坐标旋转计算2.3.2 工件存在平移和旋转3. 代码(待更新)上一篇:2D图像处理:九点标定_上(机械手轴线与法兰轴线重合)(附源码) 2. 机械手轴线…...

【二分查找】分巧克力、机器人跳跃、数的范围
Halo,这里是Ppeua。平时主要更新C语言,C,数据结构算法......感兴趣就关注我吧!你定不会失望。 🌈个人主页:主页链接 🌈算法专栏:专栏链接 我会一直往里填充内容哒! &…...

Hyperf使用RabbitMQ消息队列
Hyperf连接使用RabbitMQ消息中间件 传送门 使用Docker部署RabbitMQ,->传送门<使用Docker部署Hyperf,->传送门-< 部署环境 安装amqp扩展 composer require hyperf/amqp安装command命令行扩展 composer require hyperf/command配置参数 假…...

【Linux】P3 用户与用户组
用户与用户组root 超级管理员设置超级管理员密码切换到超级管理员sudo 临时使用超级权限用户与用户组用户组管理用户管理getentroot 超级管理员 设置超级管理员密码 登陆后不会自动开启 root 访问权限,需要首先执行如下步骤设定 root 超级管理员密码 1、解除 roo…...

Spring核心模块——Aware接口
Aware接口前言基本内容例子结尾前言 Spring的依赖注入最大亮点是所有的Bean对Spring容器对存在都是没有意识到,Spring容器中的Bean的耦合度是很低的,我们可以将Spring容器很容易换成其他的容器。 但是实际开发的时候,我们经常要用到Spring容…...

Linux网络编程 第六天
目录 学习目标 libevent介绍 libevent的安装 libevent库的使用 libevent的使用 libevent的地基-event_base 等待事件产生-循环等待event_loop 使用libevent库的步骤: 事件驱动-event 编写一个基于event实现的tcp服务器: 自带buffer的事件-buff…...

STM32开发(六)STM32F103 通信 —— RS485 Modbus通信编程详解
文章目录一、基础知识点二、开发环境三、STM32CubeMX相关配置1、STM32CubeMX基本配置2、STM32CubeMX RS485 相关配置四、Vscode代码讲解五、结果演示以及报文解析一、基础知识点 了解 RS485 Modbus协议技术 。本实验是基于STM32F103开发 实现 通过RS-485实现modbus协议。 准备…...

AcWing1049.大盗阿福题解
前言如果想看状态机的详解,点机这里:dp模型——状态机模型C详解1049. 大盗阿福阿福是一名经验丰富的大盗。趁着月黑风高,阿福打算今晚洗劫一条街上的店铺。这条街上一共有 N家店铺,每家店中都有一些现金。阿福事先调查得知,只有当…...
python日志模块,loggin模块
python日志模块,loggin模块loggin模块日志的格式处理器种类日志格式的参数使用loggin模块 logging库采用模块化方法,并提供了几类组件:记录器,处理程序,过滤器和格式化程序。 记录器(Logger)&a…...

接口自动化入门-TestNg
目录1.TestNg介绍2、TestNG安装3、TestNG使用3.1 编写测试用例脚本3.2 创建TestNG.xml文件(1)创建testng.xml文件(2)修改testng.xml4、测试报告生成1.TestNg介绍 TestNg是Java中开源的自动化测试框架,灵感来源于Junit…...

Spring AOP —— 详解、实现原理、简单demo
目录 一、Spring AOP 是什么? 二、学习AOP 有什么作用? 三、AOP 的组成 3.1、切面(Aspect) 3.2、切点(Pointcut) 3.3、通知(Advice) 3.4、连接点 四、实现 Spring AOP 一个简…...

(蓝桥真题)异或数列(博弈)
题目链接:P8743 [蓝桥杯 2021 省 A] 异或数列 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 样例输入: 4 1 1 1 0 2 2 1 7 992438 1006399 781139 985280 4729 872779 563580 样例输出: 1 0 1 1 分析:容易想到对于异或最大值…...

4万字数字政府建设总体规划方案WORD
本资料来源公开网络,仅供个人学习,请勿商用。部分资料内容: 我省“数字政府”架构 (一) 总体架构。 “数字政府”总体架构包括管理架构、业务架构、技术架构。其中,管理架构体现“管运分离”的建设运营模式…...
CCF/CSP 201709-2公共钥匙盒100分
试题编号:201709-2试题名称:公共钥匙盒时间限制:1.0s内存限制:256.0MB问题描述:问题描述 有一个学校的老师共用N个教室,按照规定,所有的钥匙都必须放在公共钥匙盒里,老师不能带钥…...
【OC】Blocks模式
1. Block语法 Block语法完整形式如下: ^void (int event) {printf("buttonId:%d event%d\n", i, event); }完整形式的Block语法与一般的C语言函数定义相比,仅有两点不同。 没有函数名。带有“^”(插入记号)。 因为O…...

软件设计师教程(七)计算机系统知识-操作系统知识
软件设计师教程 软件设计师教程(一)计算机系统知识-计算机系统基础知识 软件设计师教程(二)计算机系统知识-计算机体系结构 软件设计师教程(三)计算机系统知识-计算机体系结构 软件设计师教程(…...

蓝桥杯2023/3/2
1. 小蓝正在学习一门神奇的语言,这门语言中的单词都是由小写英文字母组 成,有些单词很长,远远超过正常英文单词的长度。小蓝学了很长时间也记不住一些单词,他准备不再完全记忆这些单词,而是根据单词中哪个字母出现得最…...

【IoT】创业成功不可或缺的两个因素:能力和趋势
今天就来谈谈能力和趋势究竟哪个更重要的问题。 在谈成功的十大要素时,我曾经讲到: 一命、二运、三风水,这三个要素几乎不涉及任何个人的努力。 而趋势跟这三个要素又是息息相关的,这也类似雷军所说的飞猪理论。 只要风足够大&…...

【Axure高保真原型】引导弹窗
今天和大家中分享引导弹窗的原型模板,载入页面后,会显示引导弹窗,适用于引导用户使用页面,点击完成后,会显示下一个引导弹窗,直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…...
模型参数、模型存储精度、参数与显存
模型参数量衡量单位 M:百万(Million) B:十亿(Billion) 1 B 1000 M 1B 1000M 1B1000M 参数存储精度 模型参数是固定的,但是一个参数所表示多少字节不一定,需要看这个参数以什么…...

2025年能源电力系统与流体力学国际会议 (EPSFD 2025)
2025年能源电力系统与流体力学国际会议(EPSFD 2025)将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会,EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...

【大模型RAG】Docker 一键部署 Milvus 完整攻略
本文概要 Milvus 2.5 Stand-alone 版可通过 Docker 在几分钟内完成安装;只需暴露 19530(gRPC)与 9091(HTTP/WebUI)两个端口,即可让本地电脑通过 PyMilvus 或浏览器访问远程 Linux 服务器上的 Milvus。下面…...

[ICLR 2022]How Much Can CLIP Benefit Vision-and-Language Tasks?
论文网址:pdf 英文是纯手打的!论文原文的summarizing and paraphrasing。可能会出现难以避免的拼写错误和语法错误,若有发现欢迎评论指正!文章偏向于笔记,谨慎食用 目录 1. 心得 2. 论文逐段精读 2.1. Abstract 2…...

技术栈RabbitMq的介绍和使用
目录 1. 什么是消息队列?2. 消息队列的优点3. RabbitMQ 消息队列概述4. RabbitMQ 安装5. Exchange 四种类型5.1 direct 精准匹配5.2 fanout 广播5.3 topic 正则匹配 6. RabbitMQ 队列模式6.1 简单队列模式6.2 工作队列模式6.3 发布/订阅模式6.4 路由模式6.5 主题模式…...
代码随想录刷题day30
1、零钱兑换II 给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。 请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。 假设每一种面额的硬币有无限个。 题目数据保证结果符合 32 位带…...

Linux nano命令的基本使用
参考资料 GNU nanoを使いこなすnano基础 目录 一. 简介二. 文件打开2.1 普通方式打开文件2.2 只读方式打开文件 三. 文件查看3.1 打开文件时,显示行号3.2 翻页查看 四. 文件编辑4.1 Ctrl K 复制 和 Ctrl U 粘贴4.2 Alt/Esc U 撤回 五. 文件保存与退出5.1 Ctrl …...

【MATLAB代码】基于最大相关熵准则(MCC)的三维鲁棒卡尔曼滤波算法(MCC-KF),附源代码|订阅专栏后可直接查看
文章所述的代码实现了基于最大相关熵准则(MCC)的三维鲁棒卡尔曼滤波算法(MCC-KF),针对传感器观测数据中存在的脉冲型异常噪声问题,通过非线性加权机制提升滤波器的抗干扰能力。代码通过对比传统KF与MCC-KF在含异常值场景下的表现,验证了后者在状态估计鲁棒性方面的显著优…...
在 Spring Boot 项目里,MYSQL中json类型字段使用
前言: 因为程序特殊需求导致,需要mysql数据库存储json类型数据,因此记录一下使用流程 1.java实体中新增字段 private List<User> users 2.增加mybatis-plus注解 TableField(typeHandler FastjsonTypeHandler.class) private Lis…...