搜索二叉树进阶之AVL树

news2024/11/15 3:44:32

前言

二叉搜索树(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树需要在每个节点存储额外的高度信息,增加了空间开销。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C语言-输出菱形

题目要求&#xff1a; 输出以下图形 程序&#xff1a; #define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() {int i, j;for (i 0; i < 4; i){for (j i 1; j < 4; j)printf(" ");for (j 0; j < 2 * i 1; j)printf("*");…

虽迟但到:Midjourney推出网页端并限时免费!

大家好&#xff0c;我是木易&#xff0c;一个持续关注AI领域的互联网技术产品经理&#xff0c;国内Top2本科&#xff0c;美国Top10 CS研究生&#xff0c;MBA。我坚信AI是普通人变强的“外挂”&#xff0c;专注于分享AI全维度知识&#xff0c;包括但不限于AI科普&#xff0c;AI工…

【最长公共子序列】

题目 代码 #include <bits/stdc.h> using namespace std;const int N 1010; int f[N][N]; char A[N], B[N]; int main() {int n, m;cin >> n >> m;cin >> A1 >> B1;for(int i 1; i < n; i){for(int j 1; j < m; j){if(A[i] B[j]) f[…

Linux三剑客-sedawk

一、三剑客-sed命令 1、格式 sed 找谁干啥 文件 找谁:条件&#xff0c;匹配哪一行&#xff0c;哪些行. 干啥:动作&#xff0c;增删改查. #显示文件的第3行 sed -n 3p /etc/passwd选项说明-n取消默认输出-p查找-rsed支持扩展正则-i修改文件内容&#xff0c;这个选项放在最后…

VS2017编译osg3.6.0和osgearth2.10

osg3.6.0正常编译即可&#xff0c;osgearth2.10编译过程中会出现如下错误 1.osgEarth出错 1>HTTPClient.obj : error LNK2019: 无法解析的外部符号 curl_global_init&#xff0c;该符号在函数 "public: static void __cdecl osgEarth::HTTPClient::globalInit(void)&…

【日常记录-Docker】基于Alibaba Cloud Linux3安装nodejs18

Author&#xff1a;赵志乾 Date&#xff1a;2024-08-23 Declaration&#xff1a;All Right Reserved&#xff01;&#xff01;&#xff01; 1. 问题 Alibaba Cloud Linux3基础镜像中携带的nodejs安装包版本为v14&#xff0c;与项目开发中使用的v18版本不同&#xff0c;需要更新…

数据库 —>数据库编程

数据库&#xff0c;用来保存信息&#xff0c;和文件有同样的作用&#xff0c;但是却有别于文件&#xff1b; 文件掉电不会消失&#xff0c;一般用来存储软件配置&#xff0c;想要保存的东西&#xff0c;他在查找的时候是一行一行的去查找&#xff0c;效率不高&#xff1b; 数据…

虚谷数据库连接断开-常见问题的排查及解决方法

在日常的数据库管理工作中。虚谷数据库连接断开是一个常见的问题&#xff0c;这不仅会影响数据库的性能&#xff0c;还可能导致应用程序无法正常运行&#xff0c;本文将探讨Xugu数据库连接断开的原因&#xff0c;并提供相应的解决方法。 E50022 与服务器间的连接已经断开,可能…

如何用ACME.SH实现SSL证书自动化管理?

在上篇《免费SSL证书有效期缩短至90天&#xff0c;该如何应对&#xff1f;》中&#xff0c;想必大家都已经get到了——建站必备四件套之SSL证书的有效期不断缩短已成不可逆的趋势。 这一趋势下&#xff0c;如何有效管理SSL证书成了一道难题。有机智的小伙伴反馈&#xff0c;使用…

golang(go语言)打包成带图标的 exe 可执行文件

目录 1、准备 ico 图标 2、生成 syso 文件 3、打包 4、效果 1、准备 ico 图标 2、生成 syso 文件 创建 main.rc 文件&#xff0c;rc文件的名称main 与项目根目录下 main.go的 main 同名 IDI_ICON1 ICON "favicon.ico" cmd 窗口运行命令 windres -o main.syso main…

从零开始编程:Go语言真的适合新手吗?

Go语言自诞生以来&#xff0c;一直以其简洁、高效和面向工程的特性受到开发者的青睐&#xff0c;尤其是在后端开发和并发编程方面&#xff0c;Go表现出了独特的优势。然而&#xff0c;作为一门以简单著称的语言&#xff0c;它是否适合作为编程初学者的第一门语言呢&#xff1f;…

电脑换硬盘怎么全盘克隆?轻松实现数据迁移

随着科技的不断发展&#xff0c;电脑硬盘的存储容量和读写速度也在不断提升。为了获得更好的电脑使用体验&#xff0c;许多用户会选择更换更大容量、更高效的硬盘。然而&#xff0c;在更换硬盘的过程中&#xff0c;一个关键的问题摆在了我们面前&#xff1a;如何将旧硬盘中的所…

一文掌握 Go 内存对齐

往期精选文章推荐&#xff1a; 深入理解 go map go 常用关键字 深入理解 Go 数组、切片、字符串 深入理解channel 深入理解 go context 深入 go interface 底层原理 深入理解 go reflect 深入理解 go unsafe 前言 在前面的文章 《深入理解 go reflect》和 《深入理解…

还在拼接字符串生成XML?(Java)

FreeMarker是一个功能强大的Java模板引擎&#xff0c;广泛应用于生成动态内容&#xff0c;如HTML、XML和其他文本格式。本文将介绍FreeMarker的基本使用方法&#xff0c;并提供一个更丰富的XML模板示例&#xff0c;以及模板标签和标识的含义。 1. 引入依赖 <dependency>…

Redis持久化RDB/AOF

一、RDB RDB&#xff08;Redis DataBase&#xff09; &#xff1a;RDB 持久性以指定的时间间隔执行数据集的时间点快照&#xff0c;就是把某一刻的数据和状态以文件的形式写到磁盘上。这个快照文件称为RDB文件&#xff08;dump.rdb&#xff09;。 自动触发 Redis7版本&#xff…

Jira使用指南(高级搜索JQL/统计/面板设计)

1.Jira使用指南 Jira使用指南.pdf 上面的pdf比较详细的介绍了Jira的使用&#xff0c;目录如下&#xff1a; 或者从Jira的中文官网获得更多的使用指南 JIRA入门教程 1.1JIRA如何根据过滤出我关注的单子 https://www.cnblogs.com/wzxbro/p/17203914.html 登录JIRA平台&#…

利用子域的System权限通往父域

前言 最近翻阅笔记发现一篇文章提到通过子域的System权限可以突破获取到父域权限&#xff0c;本文将对此技术进行尝试复现研究。 利用分析 环境信息&#xff1a; 子域&#xff1a;187、sub.cs.org 父域&#xff1a;197、cs.org首先通过在子域的域控机器上打开mmc.exe->连…

Git 版本控制操作

1. 版本回退 Git 能够管理⽂件的历史版本&#xff0c;这是版本控制器重要的能⼒。如果有⼀天你发现之前前的⼯作做的出现了很⼤的问题&#xff0c;需要在某个特定的历史版本重新开始&#xff0c;这个时候&#xff0c;就需要版本回退的功能了。 执⾏ git reset 命令⽤于回退版…

Radiance Field Learners As UAV First-Person Viewers 翻译

作为无人机第一人称视角的辐射场学习者 引言。第一人称视角&#xff08;FPV&#xff09;在无人机飞行轨迹的革新方面具有巨大的潜力&#xff0c;为复杂建筑结构的导航提供了一条令人振奋的途径。然而&#xff0c;传统的神经辐射场&#xff08;NeRF&#xff09;方法面临着诸如每…

Python 爬虫入门(十二):正则表达式「详细介绍」

Python 爬虫入门&#xff08;十二&#xff09;&#xff1a;正则表达式 前言一、正则表达式的用途二、正则表达式的基本组成元素2.1 特殊字符2.2 量词2.3 位置锚点2.4 断言2.5 字符集2.6 字符类2.6.1 基本字符类2.6.2 常见字符类简写2.6.3 POSIX字符类2.6.4 组合使用 三、 正则表…