搜索二叉树进阶之AVL树

2024-08-24 07:12
文章标签 二叉树 进阶 搜索 avl

本文主要是介绍搜索二叉树进阶之AVL树,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

二叉搜索树(BST)是一种基础的数据结构,能够高效地进行搜索、插入和删除操作。然而,在最坏的情况下,普通的BST可能会退化成一条链表,导致操作效率降低。为了避免这种情况,出现了自平衡二叉搜索树,AVL树就是其中的一种。

一、什么是AVL树?

AVL树是Adelson-Velsky和Landis在1962年发明的一种自平衡二叉搜索树。它的特点是通过对树进行旋转操作来保持平衡,以确保在最坏情况下,树的高度仍然是O(log n),从而保证插入、删除和查找操作的时间复杂度都是O(log n)。

1.1 AVL树的平衡因子

AVL树的核心概念是平衡因子(Balance Factor)。对于树中的任意节点,平衡因子定义为其右子树高度减去左子树高度的值(其实左右都可以,只要保证左右子树的高度差的绝对值小于等于1就行)。即:

  • 平衡因子 = 右子树高度 - 左子树高度

为了保证AVL树的平衡,平衡因子的取值必须是-1、0或1。一旦某个节点的平衡因子超出这个范围,就需要进行旋转操作来恢复平衡。(旋转操作后续讲解)

2.2 AVL树平衡因子的更新

我们规定,当一个节点的右子树高度增加时,此时平衡因子就++,当他的左子树节点增加,该节点的平衡因子就--。

于是我们就会遇见以下三种情况:

1、平衡因子更新为0:

这说明之前的平衡因子为-1或者1,都有过高度差值,但此次插入导致差值为0,两边子树高度相同。所以不需要再继续向父节点检查。

2、平衡因子更新为-1或者1:

这说明之前的平衡因子为0(不可能为-2或者2,因为说明插入前就不是AVL树了),此次插入将之前平衡的高度差再次拉上差距,我们需要继续向上检查当前节点的父节点,是否会出现平衡因子异常。并且,若该节点为父节点的左子节点,就让父节点平衡因子--,否则让其++。

3、平衡因子为-2或者2:

这后面插入之后已经不是一个AVL树了,我们需要对该异常节点进行旋转操作。

二、AVL树的旋转

1、左单旋:

以这个抽象图为例,Parent的左子树高度为h,我们命名为a,subR为Parent的右子树,subR左右子树的高度一开始都是h,我们分别命名为b,c。

如果要对Parent进行左旋,那么此时a,b,c的高度都必须为h,并且c子树必须为满二叉树(如果不是,那么插入到空缺的叶子结点,高度不变,高度差仍然为1),否则Parent节点不能满足两边子树高度差绝对值大于1的条件。

此时只要在c子树上插入任意一个结点,都会使c的高度变为h+1,导致subR的高度差为1,根据上文,这会导致继续向上检查父节点,(父节点原本平衡因子为1),更新后为2,由于两个节点的平衡因子分别为2,1,同号单旋,异号双旋,所以需要对Parent节点进行左单旋。

具体方法就是将subR的左子树赋给Parent的右子树,让Parent的父节点指向subR节点。

我们以一个比较理解的例子为例:

在这个例子中,h为0,我们插入一个C节点到B节点的右子树,就会导致A节点的平衡因子出现问题,需要进行左旋操作。

随后我们将b的左子树给A的右子树(因为这里B的左子树为空,所以就没体现出来),随后让A的父节点变为B的父节点(在这里要判断A为他父节点的什么子树,左还是右,随后给B相应的身份)。

最后不要忘记更新平衡因子为0.

代码实现:

(我们以这样的定义为背景(后面的代码一样))

template<class T, class K>
struct AVLTreeNode
{AVLTreeNode(const pair<T, K>& kv):_kv(kv), parent(nullptr), left(nullptr), right(nullptr), _bf(0){}AVLTreeNode<T, K>* right;AVLTreeNode<T, K>* left;AVLTreeNode<T, K>* parent;pair<T, K> _kv;int _bf;
};template<class T, class K>
class  AVLTree
{typedef  AVLTreeNode<T, K> Node;
private:Node* _root;
};

parent指向父节点,_bf为平衡因子,_kv为存储的数据

本文为旋转的介绍,所以AVL树的删除插入一律跳过,想看的朋友可以在评论区留言,我也许会单独发一篇AVL树的模拟实现讲解。

void RotateL(Node* Parent)
{Node* pParnet = Parent->parent;//指向Parent的父节点Node* subR = Parent->right;//subR节点Node* subRL = subR->left;//subRL节点,指向等会交给Parent的右子树的节点//先把subRL给Parent的右子树Parent->right = subRL;if (subRL)//判断subRL是否为空{//不为空时要更新subRL的父节点subRL->parent = Parent;}//随后判断Parent的父节点是否为空,为空就说明Parent为当前树的根节点。if (pParnet == nullptr){_root = subR;subR->parent = nullptr;//进行更新,根节点替换为subR}else{//不为空if (pParnet->left = Parent){//Parent为pParent的左子树节点pParnet->left = subR;}else{ppParent->right = subR;}subR->parent = pParnet;//更新subR的父节点}//旋转结束后,更新平衡因子Parent->_bf = subR->_bf = 0;//进行左旋的条件是,Parent的平衡因子一开始为2,subR平衡因子为1,二者同号且为正,进行左单旋
}

按照一开始讲解的逻辑按部就班的书写代码就行,注意的是一开始传递的参数只有一个Parent节点,我们需要提前创建指针指向subR,subRL,pParent。

2、右单旋

与左单旋相对应的就是右单旋,他就像是左单旋的轴对称一样。

此时只要对a进行插入(a必须为满二叉树),就会触发连续向上的平衡因子更新检查,一直更新到subL为-1,随后向上导致Parent平衡因子为-2 。

同号单旋,异号双旋,由于都是负数,就对Parent进行右单旋。

一样的逻辑,先让Parent的左子树指向subL的右子树节点,随后让Parent的父节点成为subL的父节点。

代码演示:

	void RotateR(Node* Parent){Node* pParent = Parent->parent;Node* subL = Parent->left;Node* subLR = subL->right;Parent->left = subLR;if (subLR)//subLR是否为空{subLR->parent = Parent;}if (pParent == nullptr)//pParent是否为空{_root = subL;subL->parent = nullptr;}else{if (pParent->left = Parent){pParent->left = subL;}else{pParent->right = subL;}subL->parent = pParent;}subL->_bf = Parent->_bf = 0;}

3、右左双旋

我们之前只分析了二者平衡因子同号的情况,倘若Parent平衡因子为2,子树平衡因子为-1,或者Parent平衡因子为-2,子树平衡因子为1的时候,又该怎么办呢?

我们发现倘若在进行单项旋转的话,avl树仍然不会保持平衡。这时候就就需要进行双旋了。

由于Parent为2,子树为-1,异号进行左右双旋,先对子树进行左旋,再对Parent进行右旋。

注意,此时要更新旋转后的平衡因子,结果与subRL的平衡因子有关系,倘若subRL为-1,说明Parent的最后平衡因子为0,两个子树高度都为h,而subR的平衡因子为1,因为右子树高度为h,左子树高度为h-1。

代码演示如下:

void RotateRL(Node* Parent)
{Node* subR = Parent->right;Node* subRL = subR->left;int bf = subRL->_bf;//此时的subRL不可能为空,因为a的高度为h,bc高度为h-1,d高度为h,要想旋转,subRL必须存在。//或者说,subRL至少也是那个新插入的节点即:h为0,h-1代表subRL就是新插入的节点//我们这里可以复用之前写的单旋代码:RotateR(subR);RotateL(Parent);if (bf == 0){//说明subRL就是新插入的节点subR->_bf = subRL->_bf = Parent->_bf = 0;}else if (bf == -1){//插入在subRL的左树上Parent->_bf = subRL->_bf = 0;subR->_bf = 1;}else if (bf == 1){Parent->_bf = -1;subR->_bf = subRL->_bf = 0;}else{assert(false);//说明出现了其他情况,报错就行了}
}

注意,在复用之前单旋代码前,我们必须先保存当前subR,subRL的节点指针,并且知道subRL的平衡因子大小,这对我们更新最后的平衡因子有帮助。

4、左右双旋

如图,左右双旋也是右左双旋的翻版,思路只能说是差不了多少。

	void RotateLR(Node* Parent){Node* subL = Parent->left;Node* subLR = subL->right;int bf = subLR->_bf;RotateL(subL);RotateR(Parent);if (bf == 0){subL->_bf = subLR->_bf = Parent->_bf = 0;}else if (bf == 1){Parent->_bf = -1;subL->_bf = subLR->_bf = 0;}else if (bf == -1){subL->_bf = 1;subLR->_bf = Parent->_bf = 0;}else{assert(false);}}

 总结:

AVL树作为自平衡二叉搜索树的经典实现,通过对树的高度进行严格控制,确保了高效的查找、插入和删除操作。尽管其操作复杂度较高,但在需要频繁查找和维护较大数据集的场景中,AVL树无疑是一种值得选择的数据结构。

 优点

  • 平衡性好:通过自动调整树的高度,确保在最坏情况下,操作的时间复杂度保持在O(log n)。
  • 查找性能稳定:在大量数据插入或删除操作后,AVL树能够依然保持较好的查找性能。

缺点

  • 插入与删除操作复杂度较高:每次插入或删除节点后,可能需要进行多次旋转操作来恢复平衡,增加了操作的复杂性和耗时。
  • 空间开销大:为了维护平衡因子,AVL树需要在每个节点存储额外的高度信息,增加了空间开销。

这篇关于搜索二叉树进阶之AVL树的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1101798

相关文章

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

认识、理解、分类——acm之搜索

普通搜索方法有两种:1、广度优先搜索;2、深度优先搜索; 更多搜索方法: 3、双向广度优先搜索; 4、启发式搜索(包括A*算法等); 搜索通常会用到的知识点:状态压缩(位压缩,利用hash思想压缩)。

hdu1240、hdu1253(三维搜索题)

1、从后往前输入,(x,y,z); 2、从下往上输入,(y , z, x); 3、从左往右输入,(z,x,y); hdu1240代码如下: #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#inc

[MySQL表的增删改查-进阶]

🌈个人主页:努力学编程’ ⛅个人推荐: c语言从初阶到进阶 JavaEE详解 数据结构 ⚡学好数据结构,刷题刻不容缓:点击一起刷题 🌙心灵鸡汤:总有人要赢,为什么不能是我呢 💻💻💻数据库约束 🔭🔭🔭约束类型 not null: 指示某列不能存储 NULL 值unique: 保证某列的每行必须有唯一的值default: 规定没有给列赋值时的默认值.primary key:

hdu 4517 floyd+记忆化搜索

题意: 有n(100)个景点,m(1000)条路,时间限制为t(300),起点s,终点e。 访问每个景点需要时间cost_i,每个景点的访问价值为value_i。 点与点之间行走需要花费的时间为g[ i ] [ j ] 。注意点间可能有多条边。 走到一个点时可以选择访问或者不访问,并且当前点的访问价值应该严格大于前一个访问的点。 现在求,从起点出发,到达终点,在时间限制内,能得到的最大

【Linux 从基础到进阶】Ansible自动化运维工具使用

Ansible自动化运维工具使用 Ansible 是一款开源的自动化运维工具,采用无代理架构(agentless),基于 SSH 连接进行管理,具有简单易用、灵活强大、可扩展性高等特点。它广泛用于服务器管理、应用部署、配置管理等任务。本文将介绍 Ansible 的安装、基本使用方法及一些实际运维场景中的应用,旨在帮助运维人员快速上手并熟练运用 Ansible。 1. Ansible的核心概念

Flutter 进阶:绘制加载动画

绘制加载动画:由小圆组成的大圆 1. 定义 LoadingScreen 类2. 实现 _LoadingScreenState 类3. 定义 LoadingPainter 类4. 总结 实现加载动画 我们需要定义两个类:LoadingScreen 和 LoadingPainter。LoadingScreen 负责控制动画的状态,而 LoadingPainter 则负责绘制动画。

AI基础 L9 Local Search II 局部搜索

Local Beam search 对于当前的所有k个状态,生成它们的所有可能后继状态。 检查生成的后继状态中是否有任何状态是解决方案。 如果所有后继状态都不是解决方案,则从所有后继状态中选择k个最佳状态。 当达到预设的迭代次数或满足某个终止条件时,算法停止。 — Choose k successors randomly, biased towards good ones — Close

hdu4277搜索

给你n个有长度的线段,问如果用上所有的线段来拼1个三角形,最多能拼出多少种不同的? import java.io.BufferedInputStream;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;