数据结构:AVL树

前言

学习了普通二叉树,发现普通二叉树作用不大,于是我们学习了搜索二叉树,给二叉树新增了搜索、排序、去重等特性,
但是,在极端情况下搜索二叉树会退化成单边树,搜索的时间复杂度达到了O(N),这是十分不利的,
所以,牛人们又提出了新的数据结构:AVL树(平衡搜索二叉树),给搜索二叉树新增了平衡的特性,控制左右子树的高度,是搜索二叉树处于平衡的状态,避免出现极端情况。

AVL树的发明者是两位俄罗斯数学家G.M.Adelson-Velskii和E.M.Landis,为了几年他们在1962年提出该数据结构,就命名为AVL树。


AVL树的特性

AVL树要求任意节点的左右子树的高度差的绝对值不超过1.
为什么拥有该特性AVL树就可以保持平衡呢?这里需要数学证明,就不解释了,理解原理后,就很容易理解。

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

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

在这里插入图片描述
注意:在这里,我们引入一个变量,平衡因子(balance factor),用来记录左右子树的高度差,
这里我们记录右子树的高度减左子树的高度。
当然,也可以有其他的思路来替代平衡因子。


AVL树节点的定义(以KV型为例)

template<class K, class V>
struct AVLTreeNode
{AVLTreeNode<K,V>* _left = nullptr;//左子节点AVLTreeNode<K, V>* _right = nullptr;//右子节点AVLTreeNode<K, V>* _parent = nullptr;//父亲节点pair<K, V> _kv;//数据int _bf = 0;//平衡因子AVLTreeNode(const pair<K, V>& kv):_kv(kv){}
};

注意:AVL树使用三叉链实现,多了一个parent指针,用来记录父亲节点,方便使用。


AVL树的插入(核心)

AVL树是在搜索二叉树上面进行升级,所以查找的方法和搜索二叉树类似,大了就向右子树走,小了就往左子树走。

因为这里多了parent指针和bf,在插入之后都需要进行维护。(天下没有白吃的午餐,使用的时候方便,维护起来就麻烦了QAQ)

parent指针处理起来很简单,我们这里主要讲bf如何控制。

bf有三种取值 0 , - 1 , 1
当我们在节点的右侧插入的时候,节点的bf++,在节点的左侧插入的时候,节点的bf–。

在右边插入,右子树的高度增加,bf++,在左边插入的时候,左子树的高度增加,bf–

这是毫无疑问的,以下都基于此。

OK,更新完当前节点之后,bf就维护好了吗?
哪有这么简单QAQ

在这里插入图片描述
在插入后cur的bf更新为1,仅仅如此吗?
看这张图,parent的bf也要从1变成2了。

所以,在更新完cur节点的bf后,还需要根据情况确定是否要继续向上更新

具体是否要更新,主要是看子树的高度有没有发生变化

有哪些情况呢?

  1. cur插入后更新为0
    说明原来是1 或者 -1,在较短的那一条边上新增了新节点,将cur节点变平衡了,
    这样的话,那高度就没有增加,不会影响到子树的高度,就不需要向上更新bf了。
  2. cur插入后更新为 1,-1
    说明原来是0,原来是平衡的,插入新节点后破坏了平衡,子树高度发生了变化,
    所以需要向上更新bf。
  3. cur插入后更新为 2,-2
    当更新为2,-2后,发现违反了AVL树的规则,不再是一颗AVL树,我们就需要进
    行特殊操作来维护AVL树的性质了。(这里,我们采用旋转

AVL树的旋转(最难的部分)

AVL树的旋转是AVL树这个数据结构的亮点,掌握了这一点之后,才会理解这种数据结构有多么精妙。

从深层上看,AVL树的旋转分为四种情况,我们画图来分析。(由于实际情况太多太多,我们这里画抽象图)

左单旋

在这里插入图片描述

这里h >= 0
我们在最右边插入一个节点,使AVL树的右侧完全倾斜,那么我们就需要向左侧旋转了。
如何向左旋转呢?
在这里插入图片描述

我们将三个需要用到的节点命名一下,分别为parent,subR,以及subRL
根据搜索二叉树的性质,我们知道subRL > parent ,但是小于subR,所以就可以进行左单旋而不会破坏搜索的性质

左单旋就是
先将subRL插入到parent的右边
接着将parent插入到subR的左边

成功旋转之后图形变成下面的样子
在这里插入图片描述
这样左右子树的高度就一样了,重新将AVL树调整平衡了
这里还有非常多的代码细节,例如空指针,parent指针的维护等等需要处理,这里大家可以先尝试根据思路写出代码,再跟后面的代码进行比较,看看是否写对了。

这里只简单讲一下平衡因子的维护。
可以看到,在左单旋只会影响parent和subR两个节点的bf,且旋转后皆为0,故不用向上继续调整。


右单旋

右单旋和左单旋类似,是对称的,这里只简单画出抽象图,相信读者能够自己理解。
在这里插入图片描述
右单旋就是处理左边完全倾斜的情况,向右边旋转,进而调整平衡。
代码依旧在文末尾给出。


右左双旋

顾名思义,先右单旋后左单旋构成右左双旋。
相信聪明的小伙伴,在看到左单旋和右单旋的时候,就会发现这两种情况都是插入在最右边和最左边造成完全倾斜。
而插入在右子树的左边和左子树的右边都没有列举出来。

这里右左双旋就是应对插入在右子树的左边这种情况的,显然,左右双旋就是应对插入在左子树的右边那种情况的,这里讲右左双旋,左右双旋留给大家自己思考。

在这里插入图片描述
左单旋和右单旋都不适合这种情况,根本原因在于,这种情况并不是完全倾斜,很简单嘛,
你不是完全倾斜,我强行让你变成完全倾斜,不就可以使用左单旋或者右单旋了嘛。

在这里插入图片描述
对于这种情况,我们可以看到是subRL太高了,导致的不平衡,我们将subRL旋转到subR的位置岂不是可以造成完全倾斜了吗?

这右单旋不就来了嘛,使用右单旋就将subRL的右插入到subR的左,再将subR插入到subRL的右即可。

右单旋完了之后,就是下面的图形
在这里插入图片描述
映入眼帘的就是右边太高了,出现了完全倾斜,直接左单旋调整平衡即可。

这里同样有很多代码细节,要维护好parent和bf,以及处理好空指针的特殊情况。

这里还是只简单讲一下bf的维护
我们以终为始,只看旋转前和旋转后的两幅图。
在这里插入图片描述

我们可以看到只有三个节点60 30 90的bf发生了变化。
也就是只有parent,subR,subRL三个节点的bf发生了变化。
这里最后subRL和subR的bf 变成了0,parent的bf变成了-1。
但是,一定是这样吗?
这中情况是在c子树上插入新节点,如果在b上插入新节点。
a和b的高度都是h,parent的bf就是0,c的高度是h-1,d的高度是h,subR的bf就是1

问题就在于是在subRL的左子树插入还是右子树插入新节点。
如何分辨?
很简单,去观察subRL的旋转前的bf
如果是1,就是在右子树插入,如果是-1,就是在左子树插入


左右双旋

大体和右左双旋类似,所以这里简单画个抽象图,相信读者能够自行理解。
在这里插入图片描述

这里就是新节点插入在了左子树的右边,导致不平衡,先左单旋调整成完全倾斜,在右单旋调整至平衡。


代码

#include <assert.h>
#include <iostream>using namespace std;namespace Avltree//命名空间名不能和类名相同,不然会发生命名冲突
{template<class K, class V>struct AVLTreeNode{AVLTreeNode<K,V>* _left = nullptr;AVLTreeNode<K, V>* _right = nullptr;AVLTreeNode<K, V>* _parent = nullptr;pair<K, V> _kv;int _bf = 0;//平衡因子AVLTreeNode(const pair<K, V>& kv):_kv(kv){}};template<class K,class V>class AVLTree{typedef AVLTreeNode<K, V> Node;private:Node* _root = nullptr;//开始的时候要给空,不然就是野指针了void RotateL(Node* parent)//左单旋{Node* subR = parent->_right;Node* subRL = subR->_left;Node* pparent = parent->_parent;parent->_right = subRL;if (subRL)subRL->_parent = parent;subR->_left = parent;if (pparent == nullptr){subR->_parent = nullptr;_root = subR;}else{if (pparent->_left == parent){pparent->_left = subR;}else{pparent->_right = subR;}subR->_parent = pparent;}parent->_bf = 0;subR->_bf = 0;}void RotateR(Node* parent)//右单旋{Node* subL = parent->_left;Node* subLR = subL->_right;Node* pparent = parent->_parent;parent->_left = subLR;if (subLR)subLR->_parent = parent;subL->_right = parent;if (pparent == nullptr){subL->_parent = nullptr;_root = subL;}else{if (pparent->_left == parent){pparent->_left = subL;}else{pparent->_right = subL;}subL->_parent = pparent;}parent->_bf = 0;subL->_bf = 0;}void RotateRL(Node* parent){RotateR(parent->_right);RotateL(parent);}void RotateLR(Node* parent){RotateL(parent->_left);RotateR(parent);}void InOrder(Node* root){if (root == nullptr)return;InOrder(root->_left);cout << root->_kv.first << ":" << root->_kv.second << endl;InOrder(root->_right);}public:void inorder(){InOrder(_root);}bool find(const K& key){Node* cur = _root;while (cur){if (key > cur->_kv.first){cur = cur->_right;}else if (key < cur->_kv.first){cur = cur->_left;}else{return true;}}return false;}bool insert(const pair<K, V>& kv){if (_root == nullptr)//插入的时候要特殊处理空树{_root = new Node(kv);return true;}Node* cur = _root;Node* parent = nullptr;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;//如果已经在,那就插入失败,返回false}}//找到了,开始插入;cur = new Node(kv);cur->_parent = parent;if (kv.first > parent->_kv.first)//插入的节点很大,插在右边{parent->_bf++;parent->_right = cur;}else{parent->_bf--;parent->_left = cur;}//插入完成,调整平衡因子;while (parent){if (parent->_bf == 0){break;}else if (parent->_bf == 1 || parent->_bf == -1){cur = parent;parent = parent->_parent;}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)//右边高,但是偏了{Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;RotateRL(parent);if (bf == 0){parent->_bf = subR->_bf = subRL->_bf = 0;}else if (bf == 1){subRL->_bf = subR->_bf = 0;parent->_bf = -1;}else if(bf == -1){subRL->_bf = parent->_bf = 0;subR->_bf = 1;}else{assert(false);}}else if (parent->_bf == -2 && cur->_bf == 1)//左边高但是偏向右边{Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;RotateLR(parent);if (bf == 0){parent->_bf = subL->_bf = subLR->_bf = 0;}else if (bf == 1){subL->_bf = -1;parent->_bf = subLR->_bf = 0;}else if(bf == -1){subLR->_bf = subL->_bf = 0;parent->_bf = 1;}else{assert(false);}}}else{assert(false);//走到这里来就出现错误了}}return true;}};
}

对于AVL树的删除,和插入类似,但是更加复杂些,限于篇幅,就暂且不讲AVL树的删除了,感兴趣的读者可以参考《算法导论》和《数据结构(用面向对象方法与C++语言描述)》(殷人昆版)。


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

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

相关文章

【EXCEL数据处理】000020 案例 保姆级教程,附多个操作案例。EXCEL使用表格。

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享一篇文章&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 目录 【EXCEL数据处理】000020 案例 保姆级教程&#xff0c;附多个操作案例。…

vulnhub-digitalworld.local DEVELOPMENT靶机

vulnhub&#xff1a;digitalworld.local: DEVELOPMENT ~ VulnHub 导入靶机&#xff0c;放在kali同网段&#xff0c;扫描 靶机在192.168.114.129&#xff0c;扫描端口 开了几个端口&#xff0c;8080端口有网页&#xff0c;访问 说是让访问html_pages 似乎把页面都写出来了&…

2-115 基于matlab的瞬态提取变换(TET)时频分析

基于matlab的瞬态提取变换&#xff08;TET&#xff09;时频分析&#xff0c;瞬态提取变换是一种比较新的TFA方法。该方法的分辨率较高&#xff0c;能够较好地提取出故障的瞬态特征&#xff0c;用于故障诊断领域。通过对原始振动信号设置不同信噪比噪声&#xff0c;对该方法的抗…

面向对象特性中 继承详解

目录 概念&#xff1a; 定义&#xff1a; 定义格式 继承关系和访问限定符 基类和派生类对象赋值转换&#xff1a; 继承中的作用域&#xff1a; 派生类的默认成员函数 继承与友元&#xff1a; 继承与静态成员&#xff1a; 复杂的菱形继承及菱形虚拟继承&#xff1a; 虚…

学MybatisPlus

1.设置MySql的数据库 spring:datasource:url: jdbc:mysql://127.0.0.1:3306/mp?useUnicodetrue&characterEncodingUTF-8&autoReconnecttrue&serverTimezoneAsia/Shanghaidriver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: MySQL123 logging:l…

IDEA搭建JDK1.8源码调试环境

大家好 下载源码 安装好 JDK 后&#xff0c;源码目录下面有 src.zip 文件&#xff0c;这个文件就是 JDK 的源码 搭建调试环境 新建 Maven 工程&#xff0c;包含以下文件 source&#xff1a;源码文件夹&#xff08;手动新建&#xff09;test&#xff1a;单元测试文件夹&…

Linux文件重定向文件缓冲区

目录 一、C文件接口 二、系统文件I/O 2.1认识系统文件I/O 2.2系统文件I/O 2.3系统调用和库函数 2.4open( )的返回值--文件描述符 2.5访问文件的本质 三、文件重定向 3.1认识文件重定向 3.2文件重定向的本质 3.3在shell中添加重定向功能 3.4stdout和stderr 3.5如何理…

JS测试框架——Jest

文章目录 安装yarn安装jestvscode支持jest的智能提示创建JS测试用例 安装yarn yarn是meta发布的一款取代npm的包管理工具。 npm install -g yarn查看yarn软件源 yarn config get registry换源 yarn config set registry https://registry.npmmirror.com恢复官方源 yarn co…

中广核CGN25届校招网申SHL测评题库、面试流程、招聘对象,内附人才测评认知能力真题

​中国广核集团校园招聘在线测评攻略&#x1f680; &#x1f393; 校园招聘对象 2024届、2025届海内外全日制应届毕业生&#xff0c;大专、本科、硕士、博士&#xff0c;广核集团等你来&#xff01; &#x1f4c8; 招聘流程 投递简历 简历筛选 在线测评&#xff08;重点来啦…

个人项目简单https服务配置

1.SSL简介 SSL证书是一种数字证书&#xff0c;由受信任的证书颁发机构&#xff08;CA&#xff09;颁发&#xff0c;用于在互联网通信中建立加密链接。SSL代表“安全套接层”&#xff0c;是用于在互联网上创建加密链接的协议。SSL证书的主要目的是确保数据传输的安全性和隐私性…

看Threejs好玩示例,学习创新与技术(LiquidRaymarching)

今天的示例有点超出我的想象&#xff0c;首先会科普下WGSL这种新的着色器脚本&#xff0c;然后说说示例《Liquid Raymarching Scene with Three.js Shading Language | Codrops (tympanus.net)》的技术流程。本示例最终呈现的效果如下。可以看到他跟QQ那个消息拖拽消灭的效果非…

基于STM32的数字温度传感器设计与实现

引言 STM32 是由意法半导体&#xff08;STMicroelectronics&#xff09;开发的基于 ARM Cortex-M 内核的微控制器系列&#xff0c;以其强大的处理能力、丰富的外设接口和低功耗著称&#xff0c;广泛应用于嵌入式系统设计中。在这篇文章中&#xff0c;我们将介绍如何基于 STM32…

考研论坛平台|考研论坛小程序系统|基于java和微信小程序的考研论坛平台小程序设计与实现(源码+数据库+文档)

考研论坛平台小程序 目录 基于java和微信小程序的考研论坛平台小程序设计与实现 一、前言 二、系统功能设计 三、系统实现 四、数据库设计 1、实体ER图 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff1a;✌️大厂…

联想电脑怎么开启vt_联想电脑开启vt虚拟化教程(附intel和amd主板开启方法)

最近使用联想电脑的小伙伴们问我&#xff0c;联想电脑怎么开启vt虚拟。大多数可以在Bios中开启vt虚拟化技术&#xff0c;当CPU支持VT-x虚拟化技术&#xff0c;有些电脑会自动开启VT-x虚拟化技术功能。而大部分的电脑则需要在Bios Setup界面中&#xff0c;手动进行设置&#xff…

【Android】Handler消息机制

文章目录 前言概述核心组件概述Android消息机制概述 Android消息机制分析ThreadLocal的工作原理ThreadLocal基础ThreadLocal实现原理 MessageQueueLooperHandler的工作原理总结 前言 本文用于记录Android的消息机制&#xff0c;主要是指Handler的运行机制。部分内容参考自《An…

数据库管理-第248期 23ai:全球分布式数据库-分片数据分布方法(20241006)

数据库管理248期 2024-10-06 数据库管理-第248期 23ai&#xff1a;全球分布式数据库-分片数据分布方法&#xff08;20241006&#xff09;1 系统管理分片2 用户定义分片2.1 分片空间2.2 在用户定义分片配置中添加分片空间2.3 为用户定义分片创建表空间2.4 用户定义分片创建分片表…

使用bert模型进行命名实体识别任务

一、实验内容 本实验使用预训练的 BERT 模型进行命名实体识别&#xff08;NER&#xff09;任务&#xff0c;并且使用 Hugging Face 的 Transformers 库完成模型的训练、验证和测试。最后&#xff0c;使用测试集评估模型性能&#xff0c;计算NER指标。 二、算法介绍 Bert是一种…

Python技巧:如何处理未完成的函数

一、问题的提出 写代码的时候&#xff0c;我们有时候会给某些未完成的函数预留一个空位&#xff0c;等以后有时间再写具体内容。通常&#xff0c;大家会用 pass 或者 ... &#xff08;省略号&#xff09;来占位。这种方法虽然能让代码暂时不报错&#xff0c;但可能在调试的时候…

毕业设计 深度学习水果识别

文章目录 1 前言2 开发简介3 识别原理3.1 传统图像识别原理3.2 深度学习水果识别 4 数据集5 部分关键代码5.1 处理训练集的数据结构5.2 模型网络结构5.3 训练模型 6 识别效果 1 前言 Hi&#xff0c;大家好&#xff0c;这里是丹成学长&#xff0c;今天做一个 基于深度学习的水果…

《如何高效学习》

有道云笔记 第一部分 整体性学习策略 结构 结构就像思想中的一座城市&#xff0c;有很多建筑物&#xff0c;建筑物之间有道路相连&#xff0c;有高大而重要的与其他建筑有上百条路相连&#xff0c;无关紧要的建筑只有少数泥泞的小道与外界相通。 建立良好的知识结构就是绘制…