🐱作者:一只大喵咪1201
🐱专栏:《数据结构与算法》
🔥格言:你只管努力,剩下的交给时间!
AVL树
我们知道,二叉搜索树的搜索效率非常高,平均时间复杂度是O(log2N),但是当数据原本就有序时,插入二叉树中就会形成单支结构,此时搜索的时间复杂度就是O(N)。
为了避免二叉搜索树的这个缺陷,在它的基础上提出了AVL树(高度平衡二叉搜索树)和红黑树。
- AVL树:当向二叉搜索树中插入新节点后,要保证每个节点的左右子树高度差的绝对值不超过1。
根据高度差不超过1的规制,可以避免二叉搜索树出现单支的情况,使其更加接近完全二叉树,保证搜索效率是O(log2N)。
AVL树的性质:
注意: 一颗空树或者只有一个根的树也属于AVL树。
- 平衡因子 = 右子树高度 - 左子树高度

在AVL树中,每个节点的平衡因子只能是1,0,-1三种情况,一旦不是这三种就需要进行调整,保证平衡因子不会出现第四种情况。

插入新节点10以后,导致多个节点的平衡因子发生了变化:
- 节点9的平衡因子从0变成了1,说明新节点插入到了节点9的右边。
- 节点8的平衡因子从1变成了2,因为新节点插入到了节点8的右子树中。
- 节点8的平衡因子不再是1,0,-1三个数中的一个,所以就需要进行调整。
至于怎么调整一会儿本喵再详细讲解。
破坏二叉搜索树平衡的操作主要就是插入,所以我们主要来看看AVL树是如何插入的,是如何在插入过程中保证平衡的。
节点的定义:
template<class K, class V>
struct AVLTreeNode
{
pair<K, V> _kv;//键值对
AVLTreeNode* _left;//左子树
AVLTreeNode* _right;//右子树
AVLTreeNode* _parent;//父节点
int _bf;//平衡因子balance factor
//节点的构造函数
AVLTreeNode(const pair<K, V>& kv)
:_kv(kv)
, _left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_bf(0)
{}
};
AVL树的定义:
template<class K,class V>
class AVLTree
{
typedef AVLTreeNode Node;
public:
bool insert(const pair<K, V>& kv)
{
//............
}
protected:
Node* _root = nullptr;//缺省值
};
和二叉搜索树一样,AVL树种也是只有一个成员变量根,给根一个缺省值,默认情况下它是空树。
插入:
AVL树的插入和二叉搜索树的插入在前半部分一模一样,大于根的插入到右边,小于根的插入到左边,区别在于AVL树插入后的调整。
template<class K,class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
bool insert(const pair<K, V>& kv)
{
//空树时直接插入
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
{
//不允许插入
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;
}
//更新平衡因子,进行调整
break;
}
}
上面代码是将节点插入到二叉搜索树中的代码,不再解释,最重要的是后面的更新平衡因子,这才是AVL树的重点。

平衡因子不是-1/0/1的节点进行调整,调整的方式是旋转,下面本喵来详细介绍一下如何旋转。
每一个子树都是一个AVL树,所以子树的平衡因子发生变化,势必会对其父节点以及祖父节点等祖宗节点有影响,可能会引发一系列的调整。
当子树更新完毕后,是否继续向上更新平衡因子的依据是子树的高度是否发生变化:
更新平衡因子的代码:
//更新平衡因子,进行调整
while (parent)//最差更新到根
{
//左边新插入,平衡因子减一
if (cur == parent->_left)
{
parent->_bf--;
}
//右边新插入,平衡因子加一
else
{
parent->_bf++;
}
//跟新后的平衡因子是0,说明高度没有变化,不用继续更新
if (parent->_bf == 0)
{
break;
}
//新插入节点,高度发生了变化,向上更新
else if(parent->_bf==1 || parent->_bf == -1)
{
//向上更新父节点
cur = parent;
parent = parent->_parent;
}
//高度差超出1,进行旋转
else if(parent->_bf == 2 || parent->_bf == -2)
{
//旋转
//更新旋转后的平衡因子
}
//前面出错,正常情况下不会进入这里
else
{
//出错直接退出
assert(false);
}
旋转的作用:

旋转过程:
- 30变成60的左子节点,60变成根节点。
- 30和60的平衡因子都变成了0。
在上图的基础上,左右子树同时增加一层节点,如下图:

旋转过程:
- 40变成30的右子节点
- 30变成60的左子节点
- 60变成根节点
- 30和60的平衡因子变成0。
在上图基础上再增加一层节点,如下图:

此时30的左子树有两层,右子树有3层。
旋转过程:
- 60的左子树变成30的右子树。
- 30变成60的左子树。
- 60变成根。
- 30和60的平衡因子变成0。
从新增两层开始就有多种情况了,当层数越多,情况就越多,所以使用抽象图来代表有多层子树的情况:

a,b,c都是高度为h的AVL子树。
旋转过程:
- 60的左子树变成30的右子树。
- 30变成60的左子树。
- 60变成根。
- 30和60的平衡因子变成0。
通过上面具象图和抽象图插入节点后的旋转,我们可以总结出一些规律:

//用左单旋的代码条件
parent->_bf == 2 && cur->_bf == 1;
旋转过程:
- subRL成为parent的右子树。
- parent成为subR的左子树。
- subR成为根。
- parent个subR的平衡因子变成0。
上面所述的旋转就左旋。形象的理解就是将左边高的一端按下去。

将左单旋的具体实现封装在一个函数中,在更新平衡因子的过程中调用左单旋来调整结构。
右单旋的结构只是和左单旋的结构方向不一样,其他都一样,本喵就不再画具象图推演了,直接上抽象图:

a,b,c都是高度为h的AVL子树。
旋转过程:
- 30的右子树成员60的左子树。
- 60成为30的右子树。
- 30成为根。
- 60和30的平衡因子变成0。
右单旋的规律:

//用右单旋的代码条件
parent->_bf == -2 && cur->_bf == -1;
旋转过程:
- subLR成为parent的左子树。
- parent成为subL的右子树。
- subL成为根。
- parent和subL的平衡因子成为0。
形象的理解就是右边高,将右边按下去。

右单旋实现代码:
//右旋实现
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
{
//不为空才会链接
subLR->_parent = parent;
}
Node* ppNode = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
//旋转后与旧树的链接
if (ppNode == nullptr)
{
_root = subL;
subL->_parent = nullptr;
}
//新根是子树
else
{
//在左子树插入的
if (ppNode->_left == parent)
{
ppNode->_left = subL;
}
//在右子树插入的
else
{
ppNode->_right = subL;
}
subL->_parent = ppNode;
}
//更新平衡因子
parent->_bf = subL->_bf = 0;
}
只是逻辑和左单旋相反,其他一样,不再进行详细讲解。

旋转过程:
- 左子树先进行左单旋:
- 30变成40的左子树。
- 40变成根节点。
- 再整体进行右单旋:
- 90变成40的右子树。
- 40变成根节点。
- 90和40的平衡因子变成0。
在上图基础上各个子树再增加一层节点:

插入的节点为红色圈,可插入的位置有两个。
旋转过程:
- 先进行左单旋:
- 50的左子树变成30的右子树(图中左子树为空,所以不用管)。
- 30变成50的左子树。
- 50变成子树的根。
- 再进行右单旋:
- 50的右子树变成90的左子树。
- 90变成50的右子树。
- 50变成根。
- 90的平衡因子变成0,30的平衡因子变成-1,50的平衡因子变成0。
在上图基础上再增加一层节点:

相对于最开始来说一共增加了两层,红色框表示两层,这两层右三种情况,如黑色简单所指。
旋转过程:
- 先进行左单旋:
- 50的左子树变成30的右子树。
- 30变成50的左子树。
- 50变成子树的根。
- 再进行右单旋:
- 50的右子树变成90的左子树。
- 90变成50的右子树。
- 50成为根。
- 30的平衡因子变成-1,90和50的平衡因子变成0。
将上面具象图画成抽象图:

h表示子树的高度,紫色框表示插入的节点。
//用左右双旋的代码条件
parent->_bf == -2 && cur->_bf == 1;
旋转过程:
- 先进行左单旋:
- 60的左子树变成30的右子树。
- 30变成60的左子树。
- 60成为子树的根。
- 再进行右单旋:
- 60的右子树成为90的左子树。
- 90成为60的右子树。
- 60成为根。
- 60的平衡因子变成0,90的平衡因子变成1,30的平衡因子变成0。
左右双旋规律:

平衡因子更新:
双旋中,旋转很好实现,直接复用前面的左单旋和右单旋就可以,难点在于双旋过后平衡因子的更新。从上面具象图和抽象图中看不出一点平衡因子的变化规律。
换个角度来看:

一步到位的来看,旋转就是将节点60的左右子树分摊给了30个90,而它自己做了新的根。
- 新插入的节点如果在子树b,那么旋转后30的右子树高度就会加一,导致30的平衡因子是0,90的平衡因子是1。
- 新插入的节点如果在子树c,那么旋转后90的左子树高度就会加一,导致90的平衡因子是0,30的平衡因子是-1。
- 新的根节点60的平衡因子是0。
自己是新增:
左右双旋代码实现:

重点在于平衡因子的更新,左单旋和右单旋直接复用前面的代码即可。
右左双旋和左右双旋逻辑相反,同样也不再画具象图了,直接看抽象图:

//右左双旋的代码条件
parent->_bf == 2 && cur->_bf == -1;
旋转过程:
- 先进行右单旋:
- 60的右子树变成90的左子树。
- 90变成60的右子树。
- 60成为子树根。
- 再进行左单旋:
- 60的左子树变成30的右子树。
- 30变成60的左子树。
- 60成为根。
- 90的平衡因子变成0,30的平衡因子变成-1,60的平衡因子变成0。
右左双旋规律:

平衡因子更新:
同样忽略旋转过程,直接对比最开始和旋转后的结构:

更新方法和左右双旋的方式一样,就不再对图详细解释了,直接看代码:
//右左双旋实现
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;//在单旋转之前拿到平衡因子
RotateR(subR);//先进行右单旋
RotateL(parent);//在进行左单旋
//更新平衡因子
//插入subRL的左边
if (bf == -1)
{
//右单旋后,该分支成为parent的右子树
//parent的平衡因子为0
parent->_bf = 0;
//左单旋后,另一个分支成为subR的左子树
//subR的平衡因子是1
subR->_bf = 1;
}
//插入subRL的右边
else if (bf == 1)
{
//右单旋后,另一分支成为parent的右子树
//parent的平衡因子为-1
parent->_bf = -1;
//左单旋后,该分支成为subR的左子树
//subR的平衡因子为0
subR->_bf = 0;
}
//subRL就是新插入的节点
else if (bf == 0)
{
//parent和subR的平衡因子都是0
parent->_bf = 0;
subR->_bf = 0;
}
//出错
else
{
//正常情况下不会进入这里
assert(false);
}
//新根的平衡因子为0
subRL->_bf = 0;
}
只是逻辑和左右双旋相反,就不再详细讲解了。
注意:
上面已经实现了AVL树的插入,包括旋转的插入,此时我们通过插入就能成功建立一颗AVL树。

写几个测试用例看看创建是否能够成功,插入的数值是键值对,如上图所示,并且按照升序打印出来。
为了证明这是AVL树需要专门写一个函数来检查一下。

如上图所示,是专门用来检测是否是AVL树的。

这样拿三个例子可能不具有代表性,下面我门用随机数来检测:

AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不过与删除不同的是,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。
情况比较复杂,有兴趣的小伙伴可以自行了解,推荐《数据结构-用面向对象方法与C++描述》殷人昆版。
AVL数是二叉搜索树,而且左右子树的高度差不会超过1,所以它非常接近完全二叉树,可以保证搜索的时间复杂度在O(log2N),而不会出现单只的情况。
但是还是存在一定的效率损失问题:
虽然旋转保证了搜索的时间复杂度在O(log2N),但是又增加了旋转的时间复杂度,主要是体现在插入数据时。
也就是说,AVL树的结构在修改时会导致效率低下。
AVL树是在二叉搜索树的基础上增加左右子树高度不超过1的限制,但是在修改结构的时候又因为旋转导致了效率降低,后面的红黑树就克服了这个问题,下篇文章见。
我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳
给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最
我正在尝试使用Curbgem执行以下POST以解析云curl-XPOST\-H"X-Parse-Application-Id:PARSE_APP_ID"\-H"X-Parse-REST-API-Key:PARSE_API_KEY"\-H"Content-Type:image/jpeg"\--data-binary'@myPicture.jpg'\https://api.parse.com/1/files/pic.jpg用这个:curl=Curl::Easy.new("https://api.parse.com/1/files/lion.jpg")curl.multipart_form_
无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD
本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01 客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02 数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit
文章目录一、概述简介原理模块二、配置Mysql使用版本环境要求1.操作系统2.mysql要求三、配置canal-server离线下载在线下载上传解压修改配置单机配置集群配置分库分表配置1.修改全局配置2.实例配置垂直分库水平分库3.修改group-instance.xml4.启动监听四、配置canal-adapter1修改启动配置2配置映射文件3启动ES数据同步查询所有订阅同步数据同步开关启动4.验证五、配置canal-admin一、概述简介canal是Alibaba旗下的一款开源项目,Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。Git地址:https://github.co
HashMap中为什么引入红黑树,而不是AVL树呢1.概述开始学习这个知识点之前我们需要知道,在JDK1.8以及之前,针对HashMap有什么不同。JDK1.7的时候,HashMap的底层实现是数组+链表JDK1.8的时候,HashMap的底层实现是数组+链表+红黑树我们要思考一个问题,为什么要从链表转为红黑树呢。首先先让我们了解下链表有什么不好???2.链表上述的截图其实就是链表的结构,我们来看下链表的增删改查的时间复杂度增:因为链表不是线性结构,所以每次添加的时候,只需要移动一个节点,所以可以理解为复杂度是N(1)删:算法时间复杂度跟增保持一致查:既然是非线性结构,所以查询某一个节点的时候
我正在尝试在Rails上安装ruby,到目前为止一切都已安装,但是当我尝试使用rakedb:create创建数据库时,我收到一个奇怪的错误:dyld:lazysymbolbindingfailed:Symbolnotfound:_mysql_get_client_infoReferencedfrom:/Library/Ruby/Gems/1.8/gems/mysql2-0.3.11/lib/mysql2/mysql2.bundleExpectedin:flatnamespacedyld:Symbolnotfound:_mysql_get_client_infoReferencedf