【高阶数据结构】AVL树 {概念及实现;节点的定义;插入并调整平衡因子;旋转操作:左单旋,右单旋,左右双旋,右左双旋;AVL树的验证及性能分析}

AVL树

一、AVL树的概念

  • 二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:

  • AVL树:又被称为高度平衡搜索二叉树,当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

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

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

在这里插入图片描述

如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在log_2 n,搜索时间复杂度O(log_2 n)


二、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; //平衡因子balance factor            AVLTreeNode(const pair<K,V> &kv)    :_left(nullptr),    _right(nullptr),                                                                         _parent(nullptr),    _kv(kv),    _bf(0)                                                                        {}    
};                                                                                

三、AVL树的插入

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

  1. 按照二叉搜索树的方式插入新节点
  2. 调整节点的平衡因子
  3. 如果节点所在的二叉树不再平衡,通过旋转恢复平衡。

在这里插入图片描述

template <class K, class V>
bool AVLTree<K,V>::Insert(const pair<K,V> &kv)
{//1.按照二叉搜索树的方式插入新节点if(_root == nullptr){_root = new Node(kv);return true;}Node *cur = _root;Node *parent = nullptr; //cur要向下一直遍历到null,所以要记录父节点的指针while(cur != nullptr){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->_right = cur;}else{parent->_left = cur;}cur->_parent = parent; //不要忘了修改父节点指针//2.调整节点的平衡因子while(parent!=nullptr) //只影响插入节点的所有祖先节点的平衡因子,parent不断向上一直遍历到根节点{//更新父节点的平衡因子//平衡因子=右树的高度-左树的高度if(cur == parent->_right){++parent->_bf; //插入右节点,bf++;      }else{--parent->_bf; //插入左节点,bf--;}//更新后检测双亲的平衡因子if(parent->_bf == 0){//由1/-1更新为0,说明以父节点为根的二叉树高度不变,无需继续向上调整。 break; }else if(abs(parent->_bf) == 1){//由0更新为1/-1,说明以父节点为根的二叉树高度增加了一层,需要继续向上调整。 parent = parent->_parent;cur = cur->_parent;}else if(abs(parent->_bf) == 2){//3.更新后为2/-2,说明parent所在的子树已经不平衡了,需要通过旋转恢复平衡。//......//下面的内容会有讲解↓↓↓}else{//除非代码有错,否则不可能有其他情况。assert(false);}}return true;
}

四、AVL树的旋转

如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:

4.1 左单旋

新节点插入较高右子树的右侧—右右:左单旋

在这里插入图片描述

解释:

  1. 上图在插入前,AVL树是平衡的。a,b,c是高度为h的AVL子树(h>=0)。新节点插入到60的右子树c使c树增加了一层,最终导致以30为根的二叉树不平衡。
  2. 要让30平衡,就需要将30向左旋转,将60提上去。让30的左子树增加一层,右子树减少一层。也就是让30做60的左子树。
  3. 如果60有左子树b,b树的根一定大于30小于60,刚好做30的右子树。旋转完成后,更新节点的平衡因子即可。
  4. 在旋转过程中,有以下几种情况需要考虑:
    1. 60节点的左子树b可能存在,也可能为空。
    2. 30可能是根节点,也可能是子树
    • 如果是根节点,旋转完成后,要更新根节点指针_root。
    • 如果是子树,可能是某个节点的左子树,也可能是右子树。要更新父节点的指针。
template <class K, class V>
void AVLTree<K,V>::RotateL(Node *parent){ //parent对应30Node *subR = parent->_right; //subR对应60Node *subRL = subR->_left; //subRL对应b树的根Node *ppNode = parent->_parent; //记录30的父节点,便于旋转后进行连接。//30和b树进行连接parent->_right = subRL;         if(subRL != nullptr) //b树可能为空{subRL->_parent = parent;}//30和60重新连接subR->_left = parent;parent->_parent = subR;//60和30的父节点进行连接//如果30是根节点,更新根节点指针_root指向60//if(_root == parent)if(ppNode == nullptr){_root = subR;}else{//60和30的父节点进行连接,先要确定30是父节点的左子树还是右子树if(ppNode->_left == parent){ppNode->_left = subR;}else{                  ppNode->_right = subR;}}subR->_parent = ppNode;//更新平衡因子,进过旋转60和30的平衡因子变为0subR->_bf = parent->_bf = 0;
}

4.2 右单旋

新节点插入较高左子树的左侧—左左:右单旋

在这里插入图片描述

详细解释参考左单旋。

template <class K, class V>
void AVLTree<K,V>::RotateR(Node *parent){Node *subL = parent->_left;Node *subLR = subL->_right;Node *ppNode = parent->_parent;parent->_parent = subL;subL->_right = parent;                                                                                                         parent->_left = subLR;if(subLR != nullptr)subLR->_parent = parent;//if(_root == parent)if(ppNode == nullptr){_root = subL;}else{if(ppNode->_left == parent){ppNode->_left = subL;}else{ppNode->_right = subL;}}subL->_parent = ppNode;subL->_bf = parent->_bf = 0;
}

旋转的作用:1. 平衡二叉树 2. 降低二叉树高度(恢复到插入之前的高度h+2)


4.3 左右双旋

新节点插入较高左子树的右侧—左右:先左单旋再右单旋

在这里插入图片描述

将双旋变成单旋后再旋转,即:先对30进行左单旋,然后再对90进行右单旋,旋转完成后再考虑平衡因子的更新。

左右双旋又能细分为3种情况:

  1. a,b,c,d是空树60是新增,引发双旋。
  2. 在b树插入新增,引发双旋。
  3. 在c树插入新增,引发双旋。

三种情况的双旋过程不变,只是平衡因子的更新需要分别处理:

在这里插入图片描述

双旋的关键在于更新平衡因子,30,60,90三个节点的平衡因子都在两次单旋过程中被错误的置为0(因为并没要满足单旋的条件)。要根据以上三种不同的情况重新调整三个节点的平衡因子。如何区分三种不同的情况?根据旋转之前60的平衡因子确认。

template <class K, class V>
void AVLTree<K,V>::RotateLR(Node *parent){ //parent对应90Node *subL = parent->_left; //subL对应30Node *subLR = subL->_right; //subLR对应60int bf = subLR->_bf; //记录旋转之前60的平衡因子RotateL(subL); //30左单旋RotateR(parent); //90右单旋//更新平衡因子subLR->_bf = 0; //60的平衡因子一定为0     switch(bf) //根据旋转之前60的平衡因子确认属于那种情况{case 1:subL->_bf = -1;parent->_bf = 0;break;case -1:subL->_bf = 0;parent->_bf = 1;break;case 0:subL->_bf = 0;parent->_bf = 0;break;default://除非代码有错,否则不可能有其他情况。assert(false);break;}
}

双旋最终的结果是将60作为二叉树的根,60的左右子树分别作了30和90的右左子树。30和90作了60的左右子树。


4.4 右左双旋

新节点插入较高右子树的左侧—右左:先右单旋再左单旋

在这里插入图片描述

详细解释参考左右双旋。

template <class K, class V>
void AVLTree<K,V>::RotateRL(Node *parent){Node *subR = parent->_right;Node *subRL = subR->_left;int bf = subRL->_bf;RotateR(subR);RotateL(parent);  //更新平衡因子                  subRL->_bf = 0;switch(bf){case 1:subR->_bf = 0;parent->_bf = -1;break;case -1:subR->_bf = 1;parent->_bf = 0;break;case 0:subR->_bf = 0;parent->_bf = 0;break;default://除非代码有错,否则不可能有其他情况。assert(false);break;}
}

双旋最终的结果是将60作为二叉树的根,60的左右子树分别作了30和90的右左子树。30和90作了60的左右子树。


4.5 分情况旋转

    else if(abs(parent->_bf) == 2){ //3.更新后为2/-2,说明parent所在的子树已经不平衡了,需要通过旋转恢复平衡。if(parent->_bf == 2 && cur->_bf == 1) //右右,左单旋{RotateL(parent);}else if(parent->_bf == 2 && cur->_bf == -1) //右左,右左双旋{RotateRL(parent);}else if(parent->_bf == -2 && cur->_bf == -1) //左左,右单旋{RotateR(parent);                              }else if(parent->_bf == -2 && cur->_bf == 1) //左右,左右双旋{RotateLR(parent);}else{//除非代码有错,否则不可能有其他情况。assert(false);}break; //注意:旋转完成后,原parent为根的子树个高度降低,已经平衡,不需要再向上更新。}

总结:
假如以parent为根的子树不平衡,即parent的平衡因子为2或者-2,分以下情况考虑:

  1. parent的平衡因子为2,说明parent的右子树高,设parent的右子树的根为subR

    • 当subR的平衡因子为1时(右右),执行左单旋

    • 当subR的平衡因子为-1时(右左),执行右左双旋

  2. parent的平衡因子为-2,说明parent的左子树高,设parent的左子树的根为subL

    • 当subL的平衡因子为-1是(左左),执行右单旋

    • 当subL的平衡因子为1时(左右),执行左右双旋

经过旋转后可以直接break;因为经过旋转,插入元素前后子树的高度未发生变化都是h+2,不需要再调整上层节点的平衡因子。一次插入最多一次旋转。

所以,AVL树插入元素的时间复杂度:找插入位置O(log_2N) + 更新平衡因子O(log_2N) + 旋转O(1) = O(log_2N)。


五、AVL树的验证

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

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

  2. 验证其为平衡树

  • 每个节点子树高度差的绝对值不超过1

  • 节点的平衡因子是否计算正确

    template <class K, class V>
    bool AVLTree<K,V>::_Isbalance(Node *root){if(root == nullptr) return true; //注意,空树也是AVL树int lh = _Height(root->_left); //_Height返回二叉树的高度int rh = _Height(root->_right);int diff = rh-lh; //计算得到平衡因子                                                                                                              if(diff != root->_bf){cout << "Key: " << root->_kv.first << "bf: " << root->_bf << " 平衡因子异常" << endl;return false;}return abs(diff) < 2 && _Isbalance(root->_left) && _Isbalance(root->_right);
    }
    
  1. 测试用例

    //插入一两组序列测试
    void Test1(){//int arr[] = {16, 3, 7, 11, 9, 26, 18, 14, 15};int arr[] = {1,2,3,4,5,6,7,8,9,10};AVLTree<int, int> avl;for(auto e : arr){avl.Insert(make_pair(e, e));          }avl.Inorder();cout << "Isbalance: " << avl.Isbalance() << endl;
    }//插入10000个随机值测试
    void Test2(){srand(time(NULL));AVLTree<int, int> avl;const int N = 10000;for(int i = 0; i<N; ++i){int x = rand();avl.Insert(make_pair(x, i));}cout << "Isbalance: " << avl.Isbalance() << endl;
    }
    

六、AVL树的删除(了解)

AVL树节点的删除步骤如下:

  1. 在AVL树中找到要删除的节点。
  2. 如果要删除的节点是叶子节点,直接删除即可。
  3. 如果要删除的节点只有一个子节点,先使前驱节点指向该节点的子节点,然后删除该节点。
  4. 如果要删除的节点有两个子节点,需要找到该节点的替换节点(即该节点右子树中最小的节点或左子树中最大的节点),然后交换与替换节点的值,最后删除替换节点。
  5. 在删除节点后,需要更新从该节点到根节点路径上所有节点的平衡因子,并进行平衡调整,使得整棵树重新满足AVL树的性质。

删除操作的平衡调整方法和AVL树的插入操作相似,但在实现时需要注意一些细节上的差异。需要注意的是,删除操作可能会导致多个节点的平衡因子发生变化,因此需要一直向上循环更新和平衡调整,直到根节点。具体实现大家可以参考《算法导论》或《数据结构-用面向对象方法与C++描述》殷人昆版。


七、AVL树的性能

  • AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log_2 (N)

  • 但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多;更差的是在删除时,有可能一直要让旋转持续到根的位置。

  • 因此,如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树。但一个结构经常修改,就不太适合。

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

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

相关文章

Java泛型机制

✅作者简介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Leo的博客 &#x1f49e;当前专栏&#xff1a;每天一个知识点 ✨特色专栏&#xff1a…

【UE 材质】模型部分透明

材质节点如下&#xff0c;这里简单解释一下。首先通过“Mask”节点将"Texture Coordinate" 节点中的“G”通道分离出来&#xff0c;然后通过“if”节点进行判断&#xff0c;当值小于0.5时为透明&#xff0c;当颜色不小于5时为不透明。可以通过一个参数来控制模型透明…

云计算的三个主要服务模型:IaaS、PaaS 和 SaaS

文章目录 介绍基础设施即服务&#xff08;Infrastructure as a Service&#xff0c;IaaS&#xff09;平台即服务&#xff08;Platform as a Service&#xff0c;PaaS&#xff09;软件即服务&#xff08;Software as a Service&#xff0c;SaaS&#xff09; 区别基础设施即服务&…

Java“牵手”天猫淘口令转换API接口数据,天猫API接口申请指南

天猫平台商品淘口令接口是开放平台提供的一种API接口&#xff0c;通过调用API接口&#xff0c;开发者可以获取天猫商品的标题、价格、库存、商品快递费用&#xff0c;宝贝ID&#xff0c;发货地&#xff0c;区域ID&#xff0c;快递费用&#xff0c;月销量、总销量、库存、详情描…

etcd读写请求的执行过程

etcd读请求如何执行 首先&#xff0c;etcdctl 会对命令中的参数进行解析。在解析完请求中的参数后&#xff0c;etcdctl 会创建一个 clientv3 库对象通过gRPC API来访问 etcd server。对应流程一。 然后通过负载均衡算法选择一个etcd server节点&#xff0c;然后调用 etcd ser…

java八股文面试[多线程]——线程池拒绝策略

四种线程池拒绝策略&#xff08;handler&#xff09; 当线程池的线程数达到最大线程数时&#xff0c;需要执行拒绝策略。拒绝策略需要实现 RejectedExecutionHandler 接口&#xff0c;并实现 rejectedExecution(Runnable r, ThreadPoolExecutor executor) 方法。不过…

如何设计微服务

一、序幕 最近在思考&#xff0c;自己哪些不足&#xff0c;需要学习点什么&#xff1f;看着Java基础知识&#xff0c;千遍一律&#xff0c;没有太大的动力需深挖&#xff0c;只能在写业务项目的时候边写边思考边夯实自己的基础。于是看了网上的一些资料&#xff0c;结合以前面试…

J1元器件的功能与应用 | 百能云芯

在现代科技和电子领域中&#xff0c;元器件是构建各种电子设备的基石。其中&#xff0c;J1元器件作为一个备受关注的焦点&#xff0c;在电子工程师和科技爱好者中引发了浓厚的兴趣。百能云芯将带您深入了解J1元器件在电子世界中的作用。 J1元器件是一种通用的连接器&#xff0c…

【深度学习实验】NumPy的简单用法

目录 一、NumPy介绍 1. 官网 2. 官方教程 二、实验内容 1. 导入numpy库 2. 打印版本号 3. arange 函数 4. array函数 5. reshape函数 6. 矩阵点乘&#xff08;逐元素相乘&#xff09; 7. 矩阵乘法 一、NumPy介绍 NumPy是一个常用于科学计算的Python库&#xff0c;尤…

Ubuntu系统下配置 Qt Creator 输入中文、配置软件源的服务器地址、修改Ubuntu系统时间

上篇介绍了Ubuntu系统下搭建QtCreator开发环境。我们可以发现安装好的QtCreator不能输入中文&#xff0c;也没有中文输入法供选择&#xff0c;这里需要进行设置。 文章目录 1. 配置软件源的服务器地址2. 先配置Ubuntu系统语言&#xff0c;设置为中文3. 安装Fcitx插件&#xff…

初始化列表

文章目录 一. 初始化列表是什么&#xff1f;二. 为什么要有初始化列表&#xff1f;三. 初始化列表的特性四. explicit关键字五. statis成员六. 友元七. 内部类八. 匿名对象九. 编译器优化总结&#xff1a; 一. 初始化列表是什么&#xff1f; 初始化列表是构造函数真正初始化的地…

ctfshow 红包题

前言&#xff1a; 最近一直在搞java很少刷题&#xff0c;看见ctfshow的活动赶紧来复现一波~ ctfshow 红包挑战7 <?php highlight_file(__FILE__); error_reporting(2); extract($_GET); ini_set($name,$value); system("ls ".filter($_GET[1])."" )…

集合框架-(Collection/Map)

1.单列集合 1.1基础概要 集合中存储的是对象的地址信息&#xff0c;想要输出对象的信息&#xff0c;需要在具体的类中重写toString&#xff08;&#xff09;方法 Collection代表单列集合&#xff0c;每个元素数据只包含一个值 List集合&#xff1a;添加的元素可以是有序、可…

ZooKeeper技术内幕

文章目录 1、系统模型1.1、数据模型1.2、节点特性1.2.1、节点类型 1.3、版本——保证分布式数据原子性操作1.4、 Watcher——数据变更的通知1.5、ACL——保障数据的安全1.5.1、权限模式&#xff1a;Scheme1.5.2、授权对象&#xff1a;ID1.5.3、权限扩展体系 2、序列化与协议2.1…

【狂神】Spring5笔记(1-9)

目录 首页&#xff1a; 1.Spring 1.1 简介 1.2 优点 2.IOC理论推导 3.IOC本质 4.HelloSpring ERROR 5.IOC创建对象方式 5.1、无参构造 这个是默认的 5.2、有参构造 6.Spring配置说明 6.1、别名 6.2、Bean的配置 6.3、import 7.DL依赖注入环境 7.1 构造器注入 …

如何利用Python代码优雅的进行文件下载

如何利用Python代码优雅的进行文件下载 一、什么是wget&#xff1f;二、使用wget.exe客户端进行文件下载三、使用Python脚本进行文件下载 欢迎学习交流&#xff01; 邮箱&#xff1a; z…1…6.com 网站&#xff1a; https://zephyrhours.github.io/ 一、什么是wget&#xff1f;…

yolov3

yolov1 传统的算法 最主要的是先猜很多候选框&#xff0c;然后使用特征工程来提取特征&#xff08;特征向量&#xff09;,最后使用传统的机器学习工具进行训练。然而复杂的过程可能会导致引入大量的噪声&#xff0c;丢失很多信息。 从传统的可以总结出目标检测可以分为两个阶…

Java 读取TIFF JPEG GIF PNG PDF

Java 读取TIFF JPEG GIF PNG PDF 本文解决方法基于开源 tesseract 下载适合自己系统版本的tesseract &#xff0c;官网链接&#xff1a;https://digi.bib.uni-mannheim.de/tesseract/ 2. 下载之后安装&#xff0c;安装的时候选择选择语言包&#xff0c;我选择了中文和英文 3.…

提高Python并发性能 - asyncio/aiohttp介绍

在进行大规模数据采集时&#xff0c;如何提高Python爬虫的并发性能是一个关键问题。本文将向您介绍使用asyncio和aiohttp库实现异步网络请求的方法&#xff0c;并通过具体结果和结论展示它们对于优化爬虫效率所带来的效果。 1. 什么是异步编程&#xff1f; 异步编程是一种非阻…

vue使用打印组件print-js

项目场景&#xff1a; 由于甲方要求&#xff0c;项目需要打印二维码标签&#xff0c;故开发此功能 开发流程 安装包&#xff1a;npm install print-js --saveprint-js的使用 <template><div id"print" ref"print" ><p>打印内容<p&…