【C++】AVL树

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

个人主页:🍝在肯德基吃麻辣烫
我的gitee:C++仓库
个人专栏:C++专栏

文章目录

  • 前言
  • 一、什么是AVL树?
    • 设计AVL树的原因
  • 二、AVL树的性质
  • 三、二叉树节点的定义
  • 四、AVL树的插入
    • 旋转
      • 1)右单旋
      • 2)左单旋
      • 3)左右双旋
      • 4)右左双旋
    • AVL树插入完整代码
    • 验证一棵树为AVL树
  • AVL树的性能分析
  • 总结


前言

本文章将会模拟实现一棵AVL树。


以下是本篇文章正文内容

一、什么是AVL树?

AVL树也是一个二叉搜索树,只不过是在二叉搜索树的基础上,增加了一个条件:

任意一棵子树的左右高度差的绝对值不大于1。


设计AVL树的原因

在二叉平衡搜索树中,在正常情况下,对该树的任意节点进行查找,最多查找OlogN次,但如果在极端情况下,该树退化成了单只树,则会极大降低搜索效率,变成O(n),所以为了避免让二叉搜索树出现极端情况,设计出一棵具有平衡性质的二叉搜索树:AVL树


二、AVL树的性质

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

平衡因子:右子树高度 - 左子树的高度(注意是右 - 左)

由此可知,一棵AVL树是高度平衡的,它的高度可保持在logN,所以搜索效率可以保持在logN


三、二叉树节点的定义

template<class K, class V>
class AVLTreeNode
{
public:AVLTreeNode(const pair<K, V> kv):_kv(kv),_left(nullptr),_right(nullptr),_parent(nullptr),_bf(0){}pair<K, V> _kv;AVLTreeNode<K, V>* _left;AVLTreeNode<K, V>* _right;AVLTreeNode<K, V>* _parent;int _bf;
};
  • 1)需要有一个平衡因子存在,即_bf
  • 2)_data值可以是一个键值对,也可以是其他类型,这里我选择了pair
  • 3)在后面的其他操作中,会频繁用到一个节点的父亲,所以直接在节点中添加一个_parent成员。

四、AVL树的插入

插入大致分为几个步骤:

如果根节点为空,则直接插入到根节点的位置

1)先找到插入位置,因为这是一棵搜索树,如果该节点的值比根小,则往左走,如果比根大,往右边走。

2)找到待插入位置后,插入该节点,然后调整平衡因子。

如果插入的是左边,则parent的平衡因子–,如果插入的是右边,则parent的平衡因子++。

如果parent的平衡因子是1或-1,说明父亲的子树有一边高了,则需要继续向上调整。(最坏情况就是调整平衡因子到根的位置)

如果parent的parent的平衡因子大于1或者小于-1了,则不满足AVL树的特性,需要进行旋转。

旋转

1)右单旋

在这里插入图片描述
新插入的节点在根节点的左侧,导致根节点的平衡因子变成-2。
则需要将根节点进行右旋。
规则如下:

在这里插入图片描述

  • 1)cur的right给parent的left
  • 2)parent变成了cur的right
    调整后,cur变成了新的根,parent变成cur的right
    此时需要注意一些细节:

如果在旋转前,parent不是根,也就是如果parent还是某一个节点的孩子,则在旋转后cur变成新的根,需要替代parent的位置,变成那个节点的孩子。

如果旋转前parent就是根,则直接让旋转后的cur的parent为空即可。

调整后cur和parent的平衡因子变成0。

2)左单旋

在这里插入图片描述
新插入的节点在根节点的右侧,导致根节点的平衡因子变成2。
则需要将根节点进行左旋。
规则如下:

  • 1)cur的left给parent的right
  • 2)parent变成了cur的left
    调整后,cur变成了新的根,parent变成curr的left
    此时需要注意一些细节:

如果在旋转前,parent不是根,也就是如果parent还是某一个节点的孩子,则在旋转后cur变成新的根,需要替代parent的位置,变成那个节点的孩子。

如果旋转前parent就是根,则直接让旋转后的cur的parent为空即可。

调整后cur和parent的平衡因子变成0。

3)左右双旋

在这里插入图片描述

插入新节点后,如果cur的平衡因子为1,parent的平衡因子为-2
说明整颗树的结构大概是这样的:
在这里插入图片描述
这就说明,此时这棵树不再是AVL树,所以我们需要对其进行旋转。

在这里插入图片描述

旋转操作如下:

  • 1)让cur的右指向curright的左,然后cur成为curright的左。此时curright的父亲就变成了原来的cur的父亲,cur的父亲变成了curright

以上操作是左旋的操作过程

  • 2)让parent的左指向curright的右,然后parent变成了curright的右,此时parent的父亲变成了curright。

以上操作是右旋的操作过程

这里还需要注意一些细节:
如果在旋转前,parent不是根,也就是如果parent还是某一个节点的孩子,则在旋转后curright变成新的根,需要替代parent的位置,变成那个节点的孩子。

平衡因子的处理:

    • 1)如果旋转之前,curright的平衡因子是0,则说明,curright这个节点,一定是新插入的节点。

(因为如果不是新插入的节点,在插入该节点前,这棵树就不是AVL树了)。

在这里插入图片描述

在进行左右双旋后,cur,curright,parent三个节点的平衡因子都是0。

    • 2)如果旋转之前,curright这个节点的平衡因子是-1,该情况就是上图画出的情况,说明新插入节点在curright的左子树,在左右双旋后,cur和curright的平衡因子变成0,parent的平衡因子是1
    • 3)如果旋转之前,curright这个节点的平衡因子是1,说明新插入节点在curright的右子树,在左右双旋后,parent和curright的平衡因子变成0,cur的平衡因子是-1

4)右左双旋

在这里插入图片描述

插入新节点后,如果cur的平衡因子为-1,parent的平衡因子为2
说明整颗树的结构大概是这样的:
在这里插入图片描述

这就说明,此时这棵树不再是AVL树,所以我们需要对其进行右左旋转。

在这里插入图片描述

旋转操作如下:

  • 1)让cur的左指向curleft的右,然后cur成为curleft的右。此时culeft的父亲就变成了原来的cur的父亲,cur的父亲变成了curleft

以上操作是右旋的操作过程

  • 2)让parent的右指向curleft的左,然后parent变成了curleft的左,此时parent的父亲变成了curleft。

以上操作是左旋的操作过程

这里还需要注意一些细节:
如果在旋转前,parent不是根,也就是如果parent还是某一个节点的孩子,则在旋转后currleft变成新的根,需要替代parent的位置,变成那个节点的孩子。

平衡因子的处理:

    • 1)如果旋转之前,curleft的平衡因子是0,则说明,curleft这个节点,一定是新插入的节点。

(因为如果不是新插入的节点,在插入该节点前,这棵树就不是AVL树了)。

在这里插入图片描述

在进行左右双旋后,cur,curleft,parent三个节点的平衡因子都是0。

    • 2)如果旋转之前,curleft这个节点的平衡因子是1,该情况就是上图画出的情况,说明新插入节点在curleft的右子树,在右左双旋后,cur和curleft的平衡因子变成0,parent的平衡因子是-1
    • 3)如果旋转之前,curleft这个节点的平衡因子是-1,说明新插入节点在curleft的左子树,在右左双旋后,parent和curleft的平衡因子变成0,cur的平衡因子是1

以上就是旋转的四种情况。

AVL树插入完整代码

template<class K,class V>
class AVLTree
{typedef AVLTreeNode<K, V> Node;
public:AVLTree():_root(nullptr){}bool Insert(const pair<K,V> kv){if(_root == nullptr){_root = new Node(kv);return true;}Node* cur = _root;Node* cur_parent = _root;//1.找到待插入位置while (cur){if (cur->_kv.first < kv.first){cur_parent = cur;cur = cur->_right;}else if (cur->_kv.first > kv.first){cur_parent = cur;cur = cur->_left;}elsereturn false;}//2.先判断待插入节点是在parent的左边还是右边cur = new Node(kv);if (cur_parent->_kv.first > kv.first){cur_parent->_left = cur;}else{cur_parent->_right = cur;}cur->_parent = cur_parent;//下面为调整二叉树的平衡//1.更新平衡因子//while (cur_parent){//左子树--if (cur_parent->_left == cur){cur_parent->_bf--;}//右子树++else{cur_parent->_bf++;}//平衡因子=0,不再影响祖先if (cur_parent->_bf == 0){break;}//不平衡了,影响祖先,要向上也调整else if (cur_parent->_bf == 1 || cur_parent->_bf == -1){cur = cur_parent;cur_parent = cur_parent->_parent;}//此时该树出问题了,需要旋转进行平衡else if (cur_parent->_bf == 2 || cur_parent->_bf == -2){//右边高,向左旋转if (cur_parent->_bf == 2 && cur->_bf == 1){RotateL(cur_parent);}//左边高,向右旋转else if (cur_parent->_bf == -2 && cur->_bf == -1){RotateR(cur_parent);}//折线型,右边高,右左双旋else if (cur_parent->_bf == 2 && cur->_bf == -1){RotateRL(cur_parent);}//折线形,左边高,左右双旋else if (cur_parent->_bf == -2 && cur->_bf == 1){RotateLR(cur_parent);}//旋转完成后一定平衡了,则breakbreak;}else{assert(false);}}cout << kv.first << endl;return true;}//左单旋void RotateL(Node* parent){Node* cur = parent->_right;Node* curleft = cur->_left;Node* ppNode = parent->_parent;parent->_right = curleft;if (curleft)curleft->_parent = parent;cur->_left = parent;parent->_parent = cur;if (!ppNode){_root = cur;cur->_parent = nullptr;}else{if (ppNode->_right == parent){ppNode->_right = cur;}else{ppNode->_left = cur;}cur->_parent = ppNode;}cur->_bf = parent->_bf = 0;}//右单旋void RotateR(Node* parent){Node* cur = parent->_left;Node* curright = cur->_right;Node* ppNode = parent->_parent;parent->_left = curright;//如果cur的右子树是空if(curright)curright->_parent = parent;cur->_right = parent;parent->_parent = cur;if (!ppNode){_root = cur;cur->_parent = nullptr;}else{cur->_parent = ppNode;//要知道根的左边是cur还是右边是curif (ppNode->_left == parent){ppNode->_left = cur;}else{ppNode->_right = cur;}}cur->_bf = parent->_bf = 0;}void RotateLR(Node* parent){Node* cur = parent->_left;Node* curright = cur->_right;int bf = curright->_bf;RotateL(parent->_left);RotateR(parent);//该节点为新插入的节点if (bf == 0){curright->_bf = 0;cur->_bf = 0;parent->_bf = 0;}//新插入节点在curright的右边,右边高了//     p//   c//     crelse if (bf == 1){curright->_bf = 0;cur->_bf = -1;parent->_bf = 0;}//新插入节点在curright的左边,左边高了else if (bf == -1){curright->_bf = 0;cur->_bf = 0;parent->_bf = 1;}}void RotateRL(Node* parent){Node* cur = parent->_right;Node* curleft = cur->_left;int bf = curleft->_bf;RotateR(parent->_right);RotateL(parent);//该节点为新插入的节点if (bf == 0){curleft->_bf = 0;cur->_bf = 0;parent->_bf = 0;}//新插入节点在curleft的右边,右边高了//     p//		  c//     clelse if (bf == 1){curleft->_bf = 0;cur->_bf = 0;parent->_bf = -1;}//新插入节点在curleft的左边,左边高了else if (bf == -1){curleft->_bf = 0;cur->_bf = 1;parent->_bf = 0;}}private:Node* _root = nullptr;
};

验证一棵树为AVL树

  • 1)验证这棵树的中序遍历是有序的。
  • 2)验证每一棵树的左右子树高度差不大于1
int Height(Node* root)
{if (root == nullptr)return 0;int left = Height(root->_left);int right = Height(root->_right);return left > right ? left + 1 : right + 1;
}bool IsBalanceTree()
{cout << "IsBalanceTree():";return _IsBalanceTree(_root);
}//通过高度判断是否为AVL树
//1.通过高度计算出真实的平衡因子,再与AVL树本身的平衡因子进行比较bool _IsBalanceTree(Node* root)
{if (root == nullptr)return true;int lefth = Height(root->_left);int righth = Height(root->_right);int bf = righth - lefth;if (bf != root->_bf || bf > 1 || bf < -1){return false;}return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}

AVL树的性能分析

由于AVL树的绝对平衡(每棵树高度差不大于1),每次在插入数据时,难免会遇到多次旋转,最坏情况需要旋转到根部。虽然旋转时间复杂度O(1),但如果旋转次数过多,也会造成效率下降。在本文中没有提到AVL树的删除,删除操作更加复杂,我没有研究过hh,不过同样每删除一个数据,都必须保证整棵树是AVL树,这又需要大量旋转来维持它的平衡。

所以在面对大量数据,并且不再有新数据的插入时,可以使用AVL树进行查找,效率为O(logN).

总结

本文主要讲述了AVL树的插入过程及其效率分析。

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

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

相关文章

目标检测中生成锚框函数详解

%matplotlib inline import torch from d2l import torch as d2l torch.set_printoptions(2) # 让pytorch打印张量时&#xff0c;只打印到小数点后两位将设一张图片&#xff0c;宽和高为2,2 X torch.rand(size(1,3,2,2)) Y generate_anchors(X,sizes[0.75,0.5,0.25],ratios[…

HPC集群自动弹性扩缩的两种实现方式

常青藤 HPC常青园 2023-07-28 19:48 发表于北京 弹性扩缩技术正在成为HPC集群中的一项重要技术。它可以根据实际需求动态调整集群资源&#xff0c;应对用户负载的波动。对于运维团队来说&#xff0c;自动弹性扩缩能够减轻集群运维负担&#xff0c;提高集群资源利用率&#xff0…

小程序Saas平台源码:开启电商小程序新时代,可视化后台自由DIY的无限可能

在当今数字化的时代&#xff0c;小程序已成为各行各业开展业务的重要工具。尤其在电商领域&#xff0c;小程序能有效地缩短消费者与商品之间的距离&#xff0c;提升营销效率。给大家分享一款针对电商行业的小程序Saas平台源码&#xff0c;它具有一键生成电商小程序、可视化后台…

Go基础八股

【Go面试】Go slice深拷贝和浅拷贝_哔哩哔哩_bilibili 基础篇 1.Go方法值接收者和指针接收者的区别 简单言之就是是否会影响调用的结构体&#xff0c;方法接收者是指针会影响 2.返回局部变量的指针 一般来说&#xff0c;局部变量会在函数返回后被销毁&#xff0c;因此被返回…

flink的网络缓冲区

背景 在flink的taskmanager进行数据交互的过程中&#xff0c;网络缓冲区是一个可以提升网络交换速度的设计&#xff0c;此外&#xff0c;flink还通过网络缓冲区实现其基于信用值credit的流量控制&#xff0c;以便尽可能的处理数据倾斜问题 网络缓冲区 在flink中每个taskmana…

C++ 太卷,转 Java?

最近看到知乎、牛客等论坛上关于 C 很多帖子&#xff0c;比如&#xff1a; 2023年大量劝入C 2023年还建议走C方向吗&#xff1f; 看了一圈&#xff0c;基本上都是说 C 这个领域唯一共同点就是都使用 C 语言&#xff0c;其它几乎没有相关性。 的确是这样&#xff0c;比如量化交…

微信小程序遇到的一些问题及解决方法(设备安装)

微信小程序遇到的一些问题及解决方法 1、[js将字符串按照换行符分隔成数组](https://blog.csdn.net/pgzero/article/details/108730175)2、[vue byte数组](https://www.yzktw.com.cn/post/1202765.html)3、使用vant-weapp的文件上传capture"camera" 无法直接调用摄像…

OPC UA协议报文,基础介绍+Hello报文解析

消息主要分为&#xff1a;消息头和附加字段 通讯过程 协议标准第一部分进行总体介绍&#xff1b;协议标准第四部分有详细介绍通讯过程 流程介绍 整体流程 连接套接字》Hello》打开安全信道》创建会话》关闭安全信道》关闭套接字 订阅等事件 服务器审核行为 聚合的服务器审…

基于未知环境碰撞冲突预测的群机器人多目标搜索研究

源自&#xff1a;指挥与控制学报 作者&#xff1a;边晓荟 周少武 张红强 吴亮红 王汐 王茂 刘朝华 陈磊 “人工智能技术与咨询” 发布 摘 要 群机器人在未知动态环境下进行多目标搜索时&#xff0c;存在碰撞预测和搜索效率不高等问题。提出了一种碰撞几何锥和改进惯性权重…

中秋特辑:Java事件监听实现一个猜灯谜小游戏

众所周知&#xff0c;JavaSwing是Java中关于窗口开发的一个工具包&#xff0c;可以开发一些窗口程序&#xff0c;然后由于工具包的一些限制&#xff0c;导致Java在窗口开发商并没有太多优势&#xff08;当然也有一些第三方的工具包也很好用&#xff09;&#xff0c;不过&#x…

一款适用于教培机构的微信CRM系统

在教育培训行业中&#xff0c;有效的客户关系管理&#xff08;CRM&#xff09;系统至关重要。微信作为一种流行的社交媒体平台&#xff0c;具有巨大的潜在价值&#xff0c;可以被用来提升教培机构的客户管理和销售效率。 一些教育培训行业存在的问题 ①每年开班收学员太多&…

二叉树的几个递归问题

我的主页&#xff1a;Lei宝啊 愿所有美好如期而遇 前言&#xff1a; 二叉树的递归是二叉树很重要的问题&#xff0c;几乎解决二叉树的问题都要使用递归&#xff0c;接下来我们将解决二叉树几个最基础的递归问题。 目录 前言&#xff1a; 二叉树的前序&#xff0c;中序&…

JDK jps命令复习

之前写过jdk命令工具的博文&#xff0c;下面复习jps命令&#xff1b; jps 是 Java Process Status Tool 的简称,它的作用是为了列出所有正在运行中的 Java 虚拟机进程和相关信息&#xff1b; jps 命令参数 -q 只输出进程 ID,省略主类的名称 -m 输出虚拟机进程启动时传递…

基于Java新枫之谷游戏攻略设计实现(源码+lw+部署文档+讲解等)

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

数据结构---链表(java)

目录 1. 链表 2. 创建Node 3. 增加 4. 获取元素 5. 删除 6. 遍历链表 7. 查找元素是否存在 8. 链栈的实现 9. 链队的实现 1. 链表 数据存放在"Node"结点中 优点&#xff1a;不用考虑扩容和缩容的问题&#xff0c;实现了动态存储数据 缺点&#xff1a;没有…

在Python中 作用域与命名空间的坑

前言&#xff1a; 嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! python更多源码/资料/解答/教程等 点击此处跳转文末名片免费获取 1. 命名空间 1.1 什么是命名空间 Namespace命名空间&#xff0c;也称名字空间&#xff0c;是从名字到对象的映射。 Python中&#xff0c;大…

【多卡训练报错】:The server socket has failed to listen on any local network address.

错误&#xff1a; RuntimeError: The server socket has failed to listen on any local network address. The server socket has failed to bind to [::]:16664 (errno: 98 - Address already in use). The server socket has failed to bind to 0.0.0.0:16664 (errno: 98 -…

JMeter:接口测试基础介绍

一、什么是接口 接口是非常抽象的概念&#xff0c;先来看下中国最大的综合性辞典《辞海》是怎样定义接口的&#xff1a; 两个不同系统或系统中两个不同特性部分的交接部分。一般分硬件接口和软件接口两种。前者是为连接计算机各部分之间、计算机与计算机之间、计算机与外部系统…

虚拟机Ubuntu操作系统常用终端命令(3)(详细解释+详细演示)

本篇概要 本篇讲述了Ubuntu操作系统常用的几个功能&#xff0c;即修改文件权限、修改文件属性、可执行脚本、虚拟机网络、FTP服务器、SSH服务器、VIM等方面的知识。希望能够得到大家的支持。 文章目录 本篇概要1.修改文件权限2.修改文件属主3.可执行脚本3.1要点与细节3.2shell…

芯科蓝牙BG27开发笔记7-配置蓝牙参数

基础的要求 1. 设置广播参数为间隔1000ms&#xff0c;不停止 2. 添加广播消息&#xff0c;含01、03、09、FF TYPE 3. 设置蓝牙通信间隔参数为320ms、400ms、2、4000ms超时 3. 配置发射功率为较低 4. 配置GATT所有数据与原Nordic 配置一致 为了解决以上疑问&#xff0c;需…