C++ AVLTree

devtools/2024/10/19 1:03:06/

目录

1. AVLTree的定义

2. 平衡因子

3. AVLTree的基础接口 

插入 

 旋转

左单旋:

右单旋:

双旋:

4. AVLTree的测试 

5. 小结


1. AVLTree的定义

        二叉搜索树(BST)虽可以缩短查找的效率,但 如果数据有序或接近有序二叉搜索树将退化为单支树,查 找元素相当于在顺序表中搜索元素,效率低下 。因此,两位俄罗斯的数学家 G.M.Adelson-Velskii和E.M.Landis 1962 年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右 子树高度之差的绝对值不超过 1( 需要对树中的结点进行调整 ) ,即可降低树的高度,从而减少平均搜索长度。

2. 平衡因子

平衡因子不是必须的,只是选择实现AVLTree的一种方式。

平衡因子的优势是可以直接观察,但是需要付出维护的代价。

在每一个节点中都加入其平衡因子的数值,一般习惯用右子树高度-左子树高度

template<typename k,typename v>
struct AVLTreeNode {typedef AVLTreeNode<k, v> Node;Node* _left;Node* _right;Node* _parent;int _bf;//balance factorpair<k, v> _kv;AVLTreeNode( const pair<k,v>& kv = make_pair(k(),v()) ):_kv(kv),_left(nullptr),_right(nullptr),_parent(nullptr),_bf(0){}
};

以下树为例进行分析。 

                       

将平衡因子当作一个风向标。

标出每一个节点现在的平衡因子:

                     

新插入的节点会影响从根到该节点路径上所有节点的平衡因子,每插入一个平衡因子就需要调整其祖先的平衡因子(这也是为什么每个节点的成员需要设计一个parent)。

                        

对于叶子结点,每插入一个节点,对于其双亲(原来的叶子)的变化:

插入在parent右边, 平衡因子++

插入在parent左边,平衡因子--

   

下面来讨论在普遍情况下插入节点后平衡因子的变化。

首先,任意树的平衡因子只会是-1 0 1

(否则不满足AVL树的条件)

1. 如果在插入节点之后,parent的平衡因子是0(就像上图的节点8),说明原本是-1或者1,然后新节点加入到了矮的那一边。

2. 如果插入后parentr的平衡因子是1或则-1,说明原本parent的平衡因子本来是0,在右或左插入一个。那此时parent的子树的高度就变化了,需要继续向上更新。

3. 如果插入后parent的平衡因子是2或者-2,说明原本parent的平衡因子本来是-1或者1,然后新节点加入到了长的那一边。此时parent的子树的高度也变化了,需要继续向上更新。

并且已经违反了AVL树的规则,需要进行旋转。


3. AVLTree的基础接口 

基本结构都和二叉搜索树类似。

先实现结点:

template<typename k,typename v>
struct AVLTreeNode {typedef AVLTreeNode<k, v> Node;Node* _left;Node* _right;Node* _parent;int _bf;//balance factorpair<k, v> _kv;AVLTreeNode(const pair<k,v>& kv = make_pair(k(),v()):_kv(kv),_left(nullptr),_right(nullptr),_parent(nullptr),_bf(0){}
};

再写树:

构造我们现在只强调一下拷贝构造。

                

不能直接将根_root拷贝过来,这样是两颗重复的树,我们需要自己实现深拷贝。

而树的拷贝需要递归,构造函数里直接递归当然不合适,所以需要再封装一层。

插入 

插入的大逻辑如下:

            

bool insert(const pair<k, v>& kv) {Node* parent = nullptr;Node* cur = _root;if (cur == nullptr) {_root = new Node(kv);return true;}while (cur) {if (cur->_kv.first > kv.first) {parent = cur;cur = cur->_left;}else if (cur->_kv.first < kv.first) {parent = cur;cur = cur->_right;}else {return false;}}cur = new Node(kv);if (parent->_kv.first > kv.first) parent->_left = cur;if (parent->_kv.first < kv.first) parent->_right = cur;cur->_parent = parent;//开始调整parent的balance factor,因为是三叉链,所以可以通过parent一路遍历向上while (parent) {//先计算平衡因子if (parent->_left == cur) {parent->_bf--;}else if (parent->_right == cur) {parent->_bf++;}//再决定是否向上调整if (parent->_bf == 0) {break;}else if (parent->_bf == 1 || parent->_bf == -1) {//adjust upcur = parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2) {//旋转}else {//如果还有其他情况,说明原节点的bf有问题,不满足avl树的规范,直接断掉assert(0);}}return true;
}

在搜索二叉树的基础上,插入变的更加复杂。 

首先比较的都是pair中的first,也就是key值

并且依然需要使用双指针,否则不好接入

最后就是需要插入完成之后调整_bf


 旋转

如果在一棵原本是平衡的 AVL 树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。

直接通过上文中的例子来进入“旋转”的主题 

左单旋:

新节点插入较高右子树的右侧,开始向上遍历调整平衡因子,调整到8的时候发现平衡因子是2,需要旋转,然后就将8这个节点传进我们的调节函数RotateL

旋转方法:9的左子树变成8的右子树,8变成9的左子树 

这样旋转之后高度和加入节点之前没有变化。


抽象图:

所有的分析中都要注意,节点之间的高度差距不能超过1

假设30为传入函数的需要调整的节点(命名为parent)

60命名为subR a/b/c是三个等高的树

(如果a/b/c不等高就不能构成我们想调整的情况。)

总结一下就是:

                                                    

左单旋需要将subRL连接到parent的右边,因为60的左子树一定是大于30的,然后将30连接到60的左边。


可以先通过列举h==0 h==1 h==2 h==3等情况观察:

 h==0时最简单。也是上文引例中的情况。


h==1时的情况


h==2时,情况的种类就很多了。

a/b可能是xyz中的任意一种情况。c只能是x,否则直接在该子树中就可以调节了。

注意为什么是3*3*4 

因为左单旋对应的模型是在60的右树(C)下进行增加节点,所以只有4个可以选择的位置。


 h==3时,先分析高度为3的子树的情况,可能为x(全满)或者y(非全满)

其中y共有14种情况。所以不讨论加节点的C树,a/b树就已经有(14+1)*(14+1)种可能性

因此,解决这个问题还需要从抽象图入手,将a、b、c当作高度为h的抽象树。

代码实现:

旋转点:平衡因子出现问题的点。

左单旋其实只需要改变subR suRL parent三者的链接关系。

parent可以是整棵树的根,也可以是任意一个子树的根 

                         

依然有逻辑bug,subRL和parent的_parent都还要修改。

先只修改subRL的_parent(parent是需要调整的节点的变量名,_parent是节点内的成员变量)

                           

subRL可能是空(h==0的时候就是空)。但是parent和subR是一定存在的(否则怎么会出现一个_bf是2一个_bf是1呢)

                          

那如果这棵树本来自身只是一颗子树呢?

所以在改动parent->_parent的值之前,要记录一下原来的parent->_parent

对于parent->_parent的分析:

如果是空,表明30(原来的parent)本来就是整棵树的根,现在整棵树的根调整了,

我们需要调整下_root

如果不是空,就像之前的insert一样,得从parentParent再去找我们左旋的这棵树是在左边还是右边。但是不用调整_root

                     

至此,单旋的大逻辑就已经完成。

节点链接好了,但是我们现在还没有处理平衡因子。

经过观察不难发现,parent和subR的平衡因子都变成0了

再加一句:parent->_bf = subR->_bf = 0;

void RotateL(Node* parent) {Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if (subRL) {subRL->_parent = parent;}Node* parentParent = parent->_parent;subR->_left = parent;parent->_parent = subR;if (parentParent) {if (parentParent->_left == parent) {parentParent->_left = subR;}else {parentParent->_right = subR;}}else {subR->_parent = nullptr;this->_root = subR;}subR->_parent = parentParent;//调整平衡因子parent->_bf = subR->_bf = 0;
}

为了便于记忆:取名字的三个节点呈现一个“ > ”形状,要将凹陷的部分拉出来,所以出现这种形状叫左旋。

                                                     


右单旋:

逻辑几乎和左旋是一样的,只是交换了right和left

读者可以自行尝试先写一写这个代码。

void RotateR(Node* parent) {assert(parent);Node* subL = parent->_left;Node* subLR = subL->right;parent->_left = subLR;if (subLR) {subLR->_parent = parent;}//解决subL和parent的父节点问题Node* parentParent = parent->_parent;subL->_right = parent;parent->_parent = subL;if (parentParent == nullptr) {_root = subL;}else {if (parentParent->_left == parent) {parentParent->_left = subL;}else {parentParent->_right = subL;}}subL->_parent = parentParent;//调整平衡因子parent->_bf = subL->_bf = 0;
}

实现好了左单旋和右单旋的代码

回到原来的插入代码,进行联系:

左旋的时候,parent的bf==2 cur的bf == 1

右旋的时候,parent的bf==-2 cur 的bf==-1

根据上述的两个条件,要讨论哪些旋转方案也逐渐明朗

cur->_bf==1&& parent->bf==2(左单旋)

cur->_bf==-1&& parent->bf==-2(右单旋)

cur->_bf==-1&& parent->bf==2

cur->_bf==1&& parent->bf==-2

   

来看后面两种情况:

双旋:

上面为单旋能解决的情况(parent和subR的bf符号相同),下面为符号相反

                                                                  (h==0)

按照之前的旋转思路,此时进行左单旋,能保证搜索树的特征,但是没有完成旋转的初衷:使高度差消失。

这样旋只是让原来的右边高变成现在的左边高

                                                                         (h==1)

如果此时再右旋,又会旋回去。

也就是说,左右旋面对这种情况只会原地打转。


解决方法:双旋

先看h==1或h==0

再看抽象图:

左双旋分为:右单旋+左单旋

右双旋分为:左单旋+右单旋

需要左双旋时,第一次右单旋就将本来需要左双旋的变成上文最基本的模型:

                                                  

因为此时涉及到对subR进行一次单旋,需要使用到subRL的右子树,所以需要单独再拆开一层树来分析。单旋的时候subRL不一定存在,但这次都是在subRL的位置上加的数据,所以subRL一定不为空

还可以直接观察双旋结果: 

直接看起始和终点状态,将subRL这个节点变成根,parent在左、subR在右

然后subRL的左右子树分别分配给parent的右和subR的左

总结:subRL或者subLR会成为新的根,他左边的子树分配给在两个节点中更左边的节点的右边,他右边的子树分配给在两个节点中更右边的节点(subR或subL / parent中的任意一个)的左边

       


双旋的代码实现的大逻辑:

                            

当然是不正确的,

虽然每一次单旋都能保证父节点和子节点的转接,但是没有考虑平衡因子的维护。

                                          

问题整理:

双旋并不像单旋,单纯的全部更新为0

并且新节点加到b和c,最后的结果还不一样。

h为0的时候还是一种单独的情况,更新完之后是全0

以左双旋为例: 

如果subRL的平衡因子是1 就是在C插入

如果subRL的平衡因子是-1 就是在B插入

如果是0,则subRL本身是新加入的节点,也就是上文所说的h是0的时候的特殊情况。  

RotateRL的意思就是先右单旋再左单旋 


左双旋和右双旋实现:

void RotateRL(Node* parent) {Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;//采用直接观察法,记录最初的subRL的_bfRotateR(subR);RotateR(parent);if (bf == 0) {//说明subRL就是新加入的元素subRL->_bf = 0;parent->_bf = 0;subR->_bf = 0;}else if (bf == -1) {parent->_bf = 0;subRL->_bf = 0;subR = 1;}else if (bf == 1) {parent->_bf = 1;subR->_bf = 0;subRL->_bf = 0;}else {assert(0);}
}void RotateLR(Node* parent) {Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;RotateL(subL);Rotate(parent);if (bf == 0) {parent->_bf = 0;subL->_bf = 0;subLR->_bf = 0;}else if (bf == 1) {parent->_bf = 0;subL->_bf = -1;subLR->_bf = 0;}else if (bf == -1) {parent->_bf = 1;subL->_bf = 0;subLR->_bf = 0;}else {assert(0);}
}

这个时候就能根据parent和cur的_bf值判断怎么旋了。

旋完之后可以直接break跳出平衡因子的调节


4. AVLTree的测试 

完成了以上代码后,使用{16, 3, 7, 11, 9, 26, 18, 14, 15} {4, 2, 6, 1, 3, 5, 15, 7, 16, 14}

两组测试用例来测试代码。

先实现中序走一遍。

        

              

但是中序只能保证是一个二叉搜索树。

还要判断其是不是平衡搜索树。


再实现一个找高度的函数

或者

均可,但是切忌写成:

 递归里套了递归,时间复杂度会大很多。这一点在之前的博客中有过详细讲解。


利用_Height函数实现IsBalanceTree函数

bool _IsBalanceTree(Node* root) {if (root == nullptr) return true;//空树也算AVLTree//计算节点高度差int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);int diff = rightHeight - leftHeight;if (abs(diff) >= 2) {cout << "结构错误,有高度差大于1的节点" << endl;return false;}if (diff != root->_bf) {cout << "平衡因子调节错误" << endl;return false;}//并且保证所有的子树都满足这个条件。return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}

调试技巧:

如果发现有错,用编译器cout来找报出错

打断点不一定要用编译器的条件断点,可以自己直接写。

第一组:

通过了。

第二组:

利用上述调试技巧打断点。 

 发现是6的平衡因子有问题

那么就在插入6时打下断点,开始观察是如何出错的。

(空语句不能打断点,所以我们定义一个变量语句来方便打断点) 

然后顺利找到是RotateRL时调节平衡因子的问题。(bf==1时parent的值应该是-1) 


AVLTree test2:

由于C语言库函数的设计,只能有32468个随机数。

所以希望产生N个随机数时最好加一个其他数字。


5. 小结

      AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即$log_2 (N)$。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。
 比如:插入时最多旋转两次(双旋),但是erase时可能需要旋转很多次。

http://www.ppmy.cn/devtools/125118.html

相关文章

Leetcode 在排序数组中查找元素的第一个和最后一个位置

这段代码的目的是在一个有序的数组中查找目标元素的第一个和最后一个位置。如果目标元素不存在&#xff0c;返回 [-1, -1]。算法要求时间复杂度为 O(log n)&#xff0c;所以使用了二分查找的思想。 主要思路&#xff1a; 使用两次二分查找&#xff1a; 第一次二分查找用于找到…

解决雪花ID在前端精度丢失问题

解决雪花ID在前端精度丢失问题 在现代分布式系统中&#xff0c;雪花算法&#xff08;Snowflake&#xff09;被广泛用于生成唯一的ID。这些ID通常是Long类型的整数。然而&#xff0c;当这些ID从后端传递到前端时&#xff0c;JavaScript的精度限制可能会导致精度丢失&#xff0c…

计算机等级考试——二级MSOffice高级应用考试常用函数

二级MSOffice高级应用考试常用函数 使用说明&#xff1a;本文共介绍了在二级 MSOffice 高级应用考试过程中考到的 6 类共 51 个函数&#xff0c;在学习过程建议打开Excel 工作表【公式】-【函数库】&#xff0c;边操作边学习&#xff0c;更易于理解中每个函数参数意义。 一、 …

教程:宏基因组数据分析教程

Orchestrating Microbiome Analysis Orchestrating Microbiome Analysis是一套包含宏基因组各种数据分析的教程&#xff0c;非常安利大家学习。 16S-analysis 16S-analysis是一本用于扩增子16s等微生物数据分析的教程&#xff0c;很适合新手入门学习。 Introduction to micro…

vue跨标签页通信(或跨窗口)详细教程

在 Vue 应用中,跨标签页(或跨窗口)的通信通常涉及到两个或多个浏览器标签页之间的信息共享。由于每个标签页或窗口都是独立的 JavaScript 执行环境,它们不能直接通过 Vue 或其他 JavaScript 库来直接相互通信。但是,有一些方法可以实现这种跨标签页的通信,主要依靠浏览器…

QD1-P5 HTML 段落标签(p)换行标签(br)

本节视频 www.bilibili.com/video/BV1n64y1U7oj?p5 ‍ 本节学习 HTML 标签&#xff1a; p标签 段落br标签 换行 ‍ 一、p 标签-段落 1.1 使用 p 标签划分段落 <p>段落文本</p>示例 <!DOCTYPE html> <html><head><meta charset"…

Facebook脸书投放目录guanggao(更适合独立站)操作步骤教学

Facebook guanggao是企业进行品牌推广、产品销售和营销转化的有效工具。在Facebook guanggao中创建目录可以帮助企业更好地展示产品&#xff0c;提高guanggao效果。以下是创建目录的详细步骤&#xff1a; 登录Facebook Business Manager&#xff08;BM业务管理器&#xff09;&a…

医疗病历交互系统:Spring Boot技术解析

第4章 系统设计 4.1 系统总体设计 系统不仅要求功能完善&#xff0c;而且还要界面友好&#xff0c;因此&#xff0c;对于一个成功的系统设计&#xff0c;功能模块的设计是关键。由于本系统可执行的是一般性质的学习信息管理工作&#xff0c;本系统具有一般适用性&#xff0c;其…