AVL树的插入(C++实现)
1. 概念
AVL树(Adelson-Velsky and Landis Tree)于1962年被提出,是计算机科学中最早被发明的平衡二叉查找树。AVL树得名于它的发明者G. M. Adelson-Velsky和Evgenii Landis。
在AVL树中,任一节点对应的两棵子树的最大高度差为1,因此它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下的时间复杂度都是 O ( l o g N ) O(logN) O(logN)。增加和删除元素的操作则可能需要借由一次或多次树旋转,以实现树的重新平衡。
1.1 性质
AVL树有这样的性质:
- 任意一个结点的左右子树高度之差都不大于1;
- 任意子树都是AVL树。
高度之差,在这里也叫做平衡因子(balance factor):
平 衡 因 子 = 右 子 树 高 度 − 左 子 树 高 度 平衡因子 = 右子树高度 - 左子树高度 平衡因子=右子树高度−左子树高度
所以平衡因子的值只有三个:-1/0/1。
为什么要限制左右子树的高度呢?
高度平衡二叉搜索树,控制每个结点的左右子树高度差不超过1。因为偶数个节点如2或4,都无法让左右高度差相等,所以退而求其次,让它接近完全二叉树,控制树的高度在 log 2 N \log_2{N} log2N之内( N N N是结点数)。
所以AVL的高度平衡是相对于每个根结点而言的,也就是平衡因子的绝对值不大于1。
这不是一棵AVL树,其中,值为76的结点左子树比右子树高度多3,即平衡因子是-3。
同一棵高度平衡的AVL树,每个结点的平衡因子都不大于1。
平衡因子是一种实现AVL树的限定方法,它不是必须的,只是一种叫法。
本文GIF动图皆使用ScreentoGif软件在网站https://visualgo.net录制。
2. 定义结点类
AVL树是改良以后的二叉搜索树,由于它有着平衡因子的限制,所以将AVL树的结点用三叉链表示,其中新增的是根结点的父结点的指针,以便稍后进行旋转后的链接操作。除此之外,也需要为每个结点增加一个新的属性:平衡因子。
这里使用了pair,它是储存一个键值对(key和value)的单位,分别用模板参数K和V表示。其中,构造函数分别把指针默认设置为nullptr,平衡因子默认设置为0,由于模板参数在传入时就必须指定类型,所以使用传入的模板参数组合构造pair。
// 定义结点类
template <class K, class V>
struct AVLTreeNode
{AVLTreeNode* _left;AVLTreeNode* _right;AVLTreeNode* _parent;pair<K, V> _kv;int _bf;AVLTreeNode(const pair<K, V>& kv):_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _bf(0){}
};
3. 插入函数
由于AVL树是优化的二叉搜索树,我们知道,在二叉搜索树中,插入是要按照规则在指定区域插入的,所以二叉搜索树的各种操作都是有查找功能的。
AVL树的查找步骤和二叉搜索树别无二致,只是多了限制条件:平衡因子的绝对值不大于1,所以在每次插入以后都要根据平衡因子的情况对树的形态进行调整,比如旋转。
插入可以分为两种情况:
- 空树:插入根结点;
- 非空:插入后根据平衡因子是否符合条件选择是否旋转。
二叉搜索树的插入规则:规则:
- 新结点的值小于根结点的值,将结点插入到左子树当中;
- 新结点的值大于根结点的值,将结点插入到右子树当中;
- 新结点的值等于根结点的值,插入结点失败。
那么如何决策是否要旋转呢?如何旋转?
3.1 更新平衡因子
当我们插入或删除某一个结点时,都会引起这个结点的所有祖先节点的平衡因子发生改变。
什么是祖先节点?
- 结点A->B之间如果存在(唯一)一条路径,那么称A为B的祖先,B称为A的子代。一个结点的祖先不止一个。
从新插入结点的第一个父结点开始往上更新平衡因子,最坏的情况可能要一直更新到根结点。除了root的父结点是nullptr
,其他所有结点都有父结点,所以可以用这个条件判断是否遍历完祖先节点。
规则
当前祖先节点,就是新插入结点的祖先节点中的某一个结点。
- 新插入结点在当前祖先节点(parent)的左边,平衡因子-1;
- 新插入结点在当前祖先节点(parent)的右边,平衡因子+1;
每次执行以上操作以后,还要继续判断当前祖先结点(parent)平衡因子的情况,以判断是否还要继续向上调整:
- parent的平衡因子等于-1或1,接近平衡,继续往上更新;
- 说明未插入结点之前parent是平衡的,插入后改变了parent这棵子树的平衡,它的子树一边比另一边高。而更高的一边虽然让parent这棵子树满足bf的绝对值小于等于1,但是它仍然有可能会影响parent往上的祖先节点。
- parent的平衡因子等于0,平衡,停止往上更新;
- 说明未插入结点之前parent是不平衡的,插入以后使得parent这棵子树平衡,它的两边子树一样高,不会影响所有祖先结点的平衡因子,停止更新。
- parent的平衡因子等于-2或2,不平衡,无法通过调整平衡因子达到平衡,需要旋转。
- 说明在未插入之前,parent这棵子树已经是一边高一边低(1层)了,插入以后一边比另一边高2层,通过调整平衡因子已经无法解决平衡问题了,所以需要进行旋转操作。
由于新插入的结点可能会影响所有祖先结点的平衡因子,所以最坏的情况要一直更新到根节点。因为从下到上迭代祖先节点,所以结点类的实现使用了三叉链结构,其中新增的是当前结点指向父结点的指针。
动图中的平衡因子 = 左子树高度 - 右子树高度,是一样的。
3.2 直接插入
AVL树插入新结点,如果最后新结点的所有祖先结点的平衡因子都符合AVL树的要求,就不用旋转。
通过动图可以知道,当插入新结点以后,比二叉搜索树多出的步骤就是检查新增结点的祖先节点的平衡因子是否符合条件。插入后的祖先结点的bf变化情况如下:
由于这部分的代码会被包含在下面的情况,而且插入的操作和二叉搜索树的逻辑并无二致,所以在此只给出插入以后向上检查祖先节点的平衡因子的逻辑:
while (parent)
{if (parent->_right == cur) // 插入在右边,bf+1{parent->_bf++;}else // 插入在左边,bf-1{parent->_bf--;}if (parent->_bf == 0) // 直到符合AVL树的规则停止{break;}else if (abs(parent->_bf) == 1) // 往上调整{parent = parent->_parent;cur = cur->_parent;}else if (abs(parent->_bf) == 2) // 不平衡{// 不平衡,要进行下面的旋转操作}
}
抽象图表示了无数种情况的集合,但具体采取哪种旋转方式,只取决于平衡因子不符合条件的那一小部分。
3.3 左单旋
示例
首先看动图中的示例:
下面是新结点的所有被影响的祖先节点的平衡因子的变化情况:
理解
当新结点插入以后,值为86的结点就不平衡了,这时需要对其进行旋转,怎么知道向哪边旋转的呢?
如果86结点就是根结点,那么这样旋转后整棵树就直接平衡了,而实际情况可能是它作为一棵子树旋转的,所以旋转以后还要重新链接到原树上。
上面的图示中是(左)单旋的最简情况,实际上AVL树的结点往往不止这些,所以为了理解上的方便,由于平衡因子是左右子树的高度差,所以以一个固定值h
表示高度,把这h
高度的结点看作一个整体,相对于它的偏移量就能表示许多情况了。
为了描述和代码上的实现方便,新插入结点叫做cur
,子树的根结点叫做parent
,根结点的右孩子叫做subR
,右孩子的左孩子叫做subRL
:
抽象图要从整体感受,上图中插入结点以后parent不平衡,bf=2,说明它的右子树比左子树高2层,可以感性地认为右子树更重。所以以parent结点为轴点(从图形看是从subR为轴点,但是函数中的参数是parent),逆时针旋转,让那个高的一边单独做parent的右子树,然后让subRL“离家出走”,做parent的右孩子(看图)。
步骤
- 让subR的左子树subRL作为parent的右子树;
- 让parent作为subR的左子树;
- 让subR作为子树的整棵子树的根结点;
- 向上按照规则更新平衡因子。
// 左单旋函数
void RotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;Node* pParent = parent->_parent; // 保存父结点的父结点parent->_right = subRL; // 重建subRL和parent联系if (subRL != nullptr){subRL->_parent = parent;}subR->_left = parent; // 重建subR和parent联系parent->_parent = subR;if (parent == _root) // 父结点为根结点,旋转后的subR作为根结点,无父结点{_root = subR;subR->_parent = nullptr;}else{if (pParent->_left == parent){pParent->_left = subR;}else{pParent->_right = subR;}subR->_parent = pParent;}subR->_bf = 0; // 更新平衡因子parent->_bf = 0;
}
注意
更新后的平衡因子是根据旋转以后的抽象图才能知道的,所以写树的代码时(数据结构)一定要画图,否则在脑子里是很乱的。
3.4 右单旋
示例
新插入结点的祖先结点的平衡因子变化情况如下:
理解
同样地,使用新插入结点叫做cur
,子树的根结点叫做parent
,根结点的左孩子叫做subL
,右孩子的左孩子叫做subLR
:
右单旋和左单旋是对称的,只是平衡因子的正负有所区别。依然可以形象地用“重量”理解它,就像扁担一样。
步骤
- 让subL的右子树subLR作为parent的左子树;
- 让parent作为subL的右子树;
- 让subL作为整棵子树的根结点;
- 向上按照规则更新平衡因子。
// 右单旋函数
void RotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;Node* pParent = parent->_parent; // 保存父结点的父结点parent->_left = subLR; // 重建subLR和parent联系if (subLR != nullptr){subLR->_parent = parent;}subL->_right = parent; // 重建subL和parent联系parent->_parent = subL;if (parent == _root) // 父结点为根结点,旋转后的subL作为根结点,无父结点{_root = subL;subL->_parent = nullptr;}else{if (pParent->_left == parent){pParent->_left = subL;}else{pParent->_right = subL;}subL->_parent = pParent;}subL->_bf = 0; // 更新平衡因子parent->_bf = 0;
}
注意
单旋的图示中的树都可能是子树,所以步骤的第三步中只是将旋转后的subR、subL作为子树的根结点。既然是子树,旋转以后的子树就必须链接到原树上,所以这也是AVL树的结点类使用三叉链结构的原因之一,其中新增的是parent指针。
3.5 左右双旋
示例
新插入结点的祖先结点的平衡因子变化情况如下:
理解
当在subLR的右子树中插入结点,会让parent的平衡因子变为-2,不平衡。从旋转之前的二叉树来看(第二个二叉树),对parent而言,它的左子树更“重”,而且就从子树的个数而言,parent的左孩子subL有3个子树,而右孩子只有一个子树。
这样讨论是合理的,因为AVL树严格限制了每个结点子树的高度,所以对于parent而言,这4个子树的高度是接近的。
这里的子树是相对于parent,就parent的左右孩子而言的,所以总共四个子树。
从结果来看,引发双旋的原因是parent某一边的子树有3个子树,而另一边只有1个子树。经过双旋以后将那3个中的一个子树分到另一边,这样就让整棵树满足AVL树的规则。
步骤
- 以subL为轴点左单旋;
- 以parent为轴点右单旋;
- 更新平衡因子。
// 左右双旋函数
void RotateLR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;RotateL(subL);RotateR(parent);if(bf == -1){subL->_bf = 0;subLR->_bf = 0;parent->_bf = 1;}else if(bf == 1){subL->_bf = -1;subLR->_bf = 0;parent->_bf = 0;}else if(bf == 0){subL->_bf = 0;subLR->_bf = 0;parent->_bf = 0;}elseassert(false);
}
注意
根据插入后未旋转之前subLR的平衡因子的情况(-1/0/1),双旋以后的平衡因子更新情况有三种:
当插入后subLR的平衡因子为-1:
更新后的平衡因子:parent:1;subL:0;subLR:0。
当插入后subLR的平衡因子为0:
当插入后subLR的平衡因子为1:
3.6 右左双旋
示例
新插入结点的祖先结点的平衡因子变化情况如下:
理解
双旋的过程是对称的。但是思想都是相同的,将子树多的那一部分放到少的那一边。
// 右左双旋函数
void RotateRL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;RotateR(subR);RotateL(parent);if(bf == -1){subR->_bf = 1;parent->_bf = 0;subRL->_bf = 0;}else if(bf == 1){subR->_bf = 0;parent->_bf = -1;subRL->_bf = 0;}else if(bf == 0){subR->_bf = 0;parent->_bf = 0;subRL->_bf = 0;}elseassert(false);
}
4. AVL树的验证
首先AVL树是二叉搜索树,所以首先可以验证它是否符合二叉搜索树的性质:
// 中序遍历子函数
void _InOrder(Node* root)
{if (root == nullptr){return;}_InOrder(root->_left);cout << root->_kv.first << ":" << root->_kv.second << endl;_InOrder(root->_right);
}
然而中序遍历只能验证它是一棵二叉搜索树,而AVL树限制条件更严格,所以可以遍历每个结点,让结点中的平衡因子和实际的平衡因子比较。
如何知道实际的平衡因子?
- 使用平衡因子公式:
平 衡 因 子 = 右 子 树 高 度 − 左 子 树 高 度 平衡因子 = 右子树高度 - 左子树高度 平衡因子=右子树高度−左子树高度
使用递归得到当前结点子树高度时,返回的是左右子树中更高的子树高度再加上自身结点高度(+1):
// 验证平衡因子 子函数
bool _IsBalance(Node* root)
{if(root == nullptr){return false;}int leftH = Height(root->_left);int rightH = Height(root->_right);int diff = rightH - leftH;if(diff != root->_bf){cout << root->_kv.first << "平衡因子异常" << endl;return false;}return abs(diff) < 2&& _IsBalance(root->_left)&& _IsBalance(root->_right);}
int Height(Node* root) // (子)树的高度等于结点高度+高的子树
{if(root == nullptr) // 空树高度为0return 0;return max(Height(root->_left), Height(root->_right)) + 1;
}
5. AVL树的性能
AVL树有着严格的高度限制,是高度平衡的二叉搜索树,可以保证二叉树在任何情况都是接近完全二叉树的,这个特性使得在查找时的效率稳定性比普通的二叉搜索树好,因为后者不是所有情况下查找的效率都是 O ( l o g 2 N ) O(log_2N) O(log2N)。AVL树即使在最坏的情况下,查找的时间复杂度也是 O ( l o g 2 N ) O(log_2N) O(log2N)。
但是,虽然它查找的效率很高,是严格的 O ( l o g 2 N ) O(log_2N) O(log2N),然而删除操作的时间复杂度在 l o g ( 2 l o g 2 N ) log(2log_2N) log(2log2N)左右。插入操作的时间复杂度在 O ( l o g 2 N ) O(log_2N) O(log2N)之上,因为AVL树的平衡因子的绝对值只要超过1,就必须旋转,由于频繁插入使得效率变低。
红黑树是AVL树的优化,它解决了AVL树频繁插入导致效率变低的问题。
AVL树的插入(C++实现)相关推荐
- c++《AVL树的概念》《AVL树的插入》《AVL树的旋转》《AVL树的验证》《AVL树的删除》《AVL树的性能》
4.1 AVL树 4.1.1 AVL树的概念 二叉搜索树虽可以缩短查找的效率,**但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当 于在顺序表中搜索元素,效率低下.**因此,两位俄罗斯的 ...
- AVL树的 插入 和 删除
AVL树的 插入 和 删除 文章目录 AVL树的 插入 和 删除 AVL的定义 AVL树节点的创建 AVL的插入 调整 插入的左旋 插入右旋 左右旋 右左旋 AVL的删除 前面写的 二叉搜索树(BST ...
- AVL树的插入与删除(详解)
AVL树的插入与删除(详解) 平衡二叉树的定义就不在这里赘述了,平衡二叉树的插入与删除都是基于平衡二叉树的查找进行的.平衡二叉树的查找和二叉树的查找又是一样的. 插入的话,我们从平衡二叉树的根结点出发 ...
- 【AVL树】AVL树的插入操作以及旋转
在讲解AVL树之前必须了解二叉搜索树, 可以看我之前的博客:二叉搜索树 AVL树是在二叉搜索树的基础上,在向二叉树排序树中插入新的结点,如果保证每个结点的左右子树的高度差的绝对值不超过1,即需要在插入 ...
- 08_Python算法+数据结构笔记-二叉搜索树查询/删除-AVL树旋转/插入/应用-贪心算法
b站视频:路飞IT学城 清华计算机博士带你学习Python算法+数据结构_哔哩哔哩_bilibili #71 二叉搜索树:查询 import randomclass BiTreeNode:def __ ...
- 数据结构——平衡二叉树(AVL树)之插入
文章目录 前言 一.定义 二.基本操作 1.查找, 2.插入(如何调整) 如何调整 代码实现插入 前言 首先我们来思考一下一个普通二叉树保存数据,如果想查找一个数据,由于普通二叉树保存数据是随机的,要 ...
- AVL树的插入_删除操作实现~
AVL的插入操作:找到所插位置之后,往上寻找可能发生不平衡之处x,若找到x则调整平衡.顶多调整平衡1次. AVL的删除操作:找到删除结点x,类似于二叉查找树一样找到其后继或前驱y,若为x->ri ...
- 数据结构-----AVL树的插入删除操作
对于AVL的插入和删除,主要利用的就是上篇文章所述的四种旋转操作,根据插入后不同的结构选用不同的方式复原平衡. 再次声明一下,http://www.cnblogs.com/QG-whz/p/51672 ...
- 第七章 二叉搜索树 (d2)AVL树:插入
转载于:https://www.cnblogs.com/ZHONGZHENHUA/p/10246808.html
最新文章
- 能做pc网页吗_梦幻西游网页版:如今还能抽金伙伴吗?玩家亲自验证,感觉还行...
- FCS编程之连接示例
- 剖析Elasticsearch集群系列第一篇 Elasticsearch的存储模型和读写操作
- word2vec如何得到词向量
- 安卓机更新系统会卡吗_iOS13.3系统值得更新吗?信号怎么样?续航改善了没有?...
- 计算机管理中添加用户属性,如何在计算机右键菜单栏中添加属性选项
- 拼多多回应被列入恶名市场:对此不能认同
- 浅入MFC之对话框及MFC程序的运行
- spark1.0和2.0的区别_Spark2.1.0——Spark初体验
- YOLOv3的环境配置
- 欧美古风格html网站模板
- 学生电影网页设计作品 影视主题学生网页制作 简单网页设计作业 静态影视介绍网页模板
- 深圳保障性住房【公租房、安居房、人才房】简单说明
- 什么叫断章取义,什么叫曲解——你被骗了多少年?
- 中国生物农药行业需求态势及“十四五”前景规划报告(2022-2027年)
- 俞伯牙摔琴谢知音的故事
- 把VBA转换成JAVA_请教,我用Excel中生成了Java代码,用VBA生成Jav
- Connext DDSQoS参考
- 半导体物理·笔记之④晶体中原子的电子状态——能带
- (翻译)链接需要鼠标悬停效果的原因