【C++练级之路】【Lv.15】AVL树(双子旋转,领略绝对平衡之美)



快乐的流畅:个人主页


个人专栏:《C语言》《数据结构世界》《进击的C++》

远方有一堆篝火,在为久候之人燃烧!

文章目录

  • 引言
  • 一、AVL树的概念
  • 二、AVL树的模拟实现
    • 2.1 结点
    • 2.2 成员变量
    • 2.3 插入
    • 2.4 旋转
      • 2.4.1 左单旋
      • 2.4.2 右单旋
      • 2.4.3 左右旋
      • 2.4.4 右左旋
  • 三、AVL树的验证
  • 四、AVL树的性能
    • 4.1 优势
    • 4.2 缺陷
    • 4.3 适用场景

引言

之前讲解了二叉搜索树,最优情况下它具有非常好的搜索性能,但是在极端场景下,它可能退化为单支树,可以形象地称为歪脖子树~


这样的话,它搜索的时间复杂度会从O(log2N)退化为O(N2),完全丧失了其优良的搜索性能。那么AVL树就可以登场了,它就是为解决这类问题而生的!

一、AVL树的概念

两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了AVL树,AVL树满足两条性质:

  • 它的左右子树都是AVL树
  • 任意结点的左右子树高度差的绝对值不超过1

这样通过控制子树高度差,让AVL树几乎完美接近于平衡,便不会出现单支树的情况,保证了优良的搜索性能,因此AVL树又称为高度平衡二叉搜索树

二、AVL树的模拟实现

2.1 结点

template<class K, class V>
struct AVLTreeNode
{
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;
	pair<K, V> _kv;
	int _bf;//balance factor

	AVLTreeNode(const pair<K, V>& kv)
		: _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _bf(0)
	{}
};

细节:

  1. 使用三叉链,增加了指向parent的指针
  2. 使用KV模型,数据存储键值对pair
  3. 结点存储平衡因子,用来记录左右子树高度差

注:平衡因子计算高度差,是 右子树高度 - 左子树高度

2.2 成员变量

template<class K, class V>
class AVLTree
{
protected:
	typedef AVLTreeNode<K, V> Node;
public:
protected:
	Node* _root = nullptr;
};

2.3 插入

因为AVL树也是二叉搜索树,所以默认成员函数和遍历与之前写的没什么不同,这里重点讲解AVL树的插入。

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;
	}
	else
	{
		parent->_left = cur;
	}
	cur->_parent = parent;

	while (parent)//讨论平衡因子
	{
		if (cur == parent->_right)
		{
			++parent->_bf;
		}
		else
		{
			--parent->_bf;
		}

		if (parent->_bf == 1 || parent->_bf == -1)
		{
			parent = parent->_parent;
			cur = cur->_parent;
		}
		else if (parent->_bf == 0)
		{
			break;
		}
		else if (parent->_bf == 2 || parent->_bf == -2)
		{
			//旋转
			if (parent->_bf == 2 && cur->_bf == 1)
			{
				RotateL(parent);
			}
			else if (parent->_bf == -2 && cur->_bf == -1)
			{
				RotateR(parent);
			}
			else if (parent->_bf == -2 && cur->_bf == 1)
			{
				RotateLR(parent);
			}
			else if (parent->_bf == 2 && cur->_bf == -1)
			{
				RotateRL(parent);
			}
			else
			{
				assert(false);
			}
			break;
		}
		else
		{
			assert(false);
		}
	}

	return true;
}

思路:

  1. 以二叉搜索树的方式正常插入
  2. 讨论平衡因子,以及调整结构

这里的重点在于如何讨论和调整平衡因子(bf)。

  1. 首先,插入cur结点,调整parent结点的bf,左减右加
  2. 讨论parent的bf
    • bf为0
    • bf为1或-1
    • bf为2或-2

bf为0时:

分析:此时没有增加高度,而是补上缺口,整棵树是平衡的,直接break即可


bf为1或-1时:

分析:此时增加了局部子树的高度,不确定有没有影响整体的高度差,所以要继续向上调整

parent = parent->_parent;
cur = cur->_parent;

bf为2或-2时:

此时bf已经超出平衡限制区间,需要进行结构调整,我们称之为旋转

2.4 旋转

旋转分为两大类:单旋和双旋。而单旋分为左单旋和右单旋,双旋分为左右旋和右左旋。

2.4.1 左单旋

场景:右部外侧插入

过程:

  1. parent接过subR的左子树subRL
  2. subR左边再链接parent

void RotateL(Node* parent)//左单旋
{
	Node* grandparent = parent->_parent;
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	parent->_right = subRL;
	if (subRL)
	{
		subRL->_parent = parent;
	}

	subR->_left = parent;
	parent->_parent = subR;

	if (grandparent)
	{
		if (grandparent->_right == parent)
		{
			grandparent->_right = subR;
		}
		else
		{
			grandparent->_left = subR;
		}
	}
	else
	{
		_root = subR;
	}
	subR->_parent = grandparent;

	parent->_bf = subR->_bf = 0;
}

细节:

  1. 大体是三步链接,注意双向链接
  2. 注意判空(subRL,grandparent)
  3. 如果判空,注意_root的传递
  4. 最后调整平衡因子_bf

2.4.2 右单旋

场景:左部外侧插入

过程:

  1. parent接过subL的右子树subLR
  2. subL右边再链接parent

void RotateR(Node* parent)//右单旋
{
	Node* grandparent = parent->_parent;
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	parent->_left = subLR;
	if (subLR)
	{
		subLR->_parent = parent;
	}

	subL->_right = parent;
	parent->_parent = subL;

	if (grandparent)
	{
		if (grandparent->_right == parent)
		{
			grandparent->_right = subL;
		}
		else
		{
			grandparent->_left = subL;
		}
	}
	else
	{
		_root = subL;
	}
	subL->_parent = grandparent;

	parent->_bf = subL->_bf = 0;
}

细节:同左单旋

2.4.3 左右旋

场景:左部内侧插入

过程:

  1. 先对subL进行左单旋
  2. 再对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 = -1;
		subLR->_bf = 0;
		parent->_bf = 0;
	}
	else if (bf == -1)
	{
		subL->_bf = 0;
		subLR->_bf = 0;
		parent->_bf = 1;
	}
	else if (bf == 0)
	{
		subL->_bf = 0;
		subLR->_bf = 0;
		parent->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

细节:

  1. 这里旋转直接复用前面单旋的代码
  2. 主要的重点,在于平衡因子bf的讨论
    • bf为1,在subLR的右侧插入
    • bf为-1,在subLR的左侧插入
    • bf为0,插入subLR(之前为空)

2.4.4 右左旋

场景:右部内侧插入

过程:

  1. 先对subR进行右单旋
  2. 再对parent进行左单旋

void RotateRL(Node* parent)//右左旋
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	int bf = subRL->_bf;

	RotateR(subR);
	RotateL(parent);

	if (bf == 1)
	{
		parent->_bf = -1;
		subRL->_bf = 0;
		subR->_bf = 0;
	}
	else if (bf == -1)
	{
		parent->_bf = 0;
		subRL->_bf = 0;
		subR->_bf = 1;
	}
	else if (bf == 0)
	{
		parent->_bf = 0;
		subRL->_bf = 0;
		subR->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

细节:同左右旋


综上所述,旋转的目的保证平衡,同时降低树的高度

三、AVL树的验证

我们主要验证左右子树高度是否平衡,即高度差是否小于等于1

bool IsBalance()
{
	return _IsBalance(_root);
}

int Height(Node* root)
{
	if (root == nullptr)
	{
		return 0;
	}

	int leftH = Height(root->_left);
	int rightH = Height(root->_right);

	return leftH > rightH ? leftH + 1 : rightH + 1;
}

bool _IsBalance(Node* root)
{
	if (root == nullptr)
	{
		return true;
	}

	int leftH = Height(root->_left);
	int rightH = Height(root->_right);

	if (rightH - leftH != root->_bf)
	{
		cout << root->_kv.first << "节点平衡因子异常" << endl;
		return false;
	}

	return abs(rightH - leftH) <= 1
		&& _IsBalance(root->_left)
		&& _IsBalance(root->_right);
}

细节:

  1. 为了方便计算高度,写一个Height函数
  2. 在子函数递归中,计算高度差是否小于等于1
  3. 与此同时,还要检查平衡因子是否正常

四、AVL树的性能

4.1 优势

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即 l o g 2 ( N ) log_2 (N) log2(N)

4.2 缺陷

但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。

4.3 适用场景

因此,如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。


真诚点赞,手有余香

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【聚类】K-Means聚类(优缺点、手肘法、轮廓系数法、检测异常点、图像压缩,含代码实战)

写在前面&#xff1a; 首先感谢兄弟们的关注和订阅&#xff0c;让我有创作的动力&#xff0c;请一键三连&#xff0c;在创作过程我会尽最大能力&#xff0c;保证作品的质量&#xff0c;如果有问题&#xff0c;可以私信我&#xff0c;让我们携手共进&#xff0c;共创辉煌。 1、…

FPGA - AXI4_Lite(实现用户端与axi4_lite之间的交互逻辑)

在之前的博客中对AXI4总线进行了介绍&#xff08;FPGA-AXI4接口协议概述&#xff09;&#xff0c;在这篇博客中&#xff0c;实现用户端与axi4_lite之间的交互逻辑。 一&#xff0c; AXI4 1.1 AXI4 介绍 对AXI4总线简单介绍&#xff08;具体可见FPGA-AXI4接口协议概述&#…

【3D reconstruction 学习笔记】

三维重建 3D reconstruction 1. 相机几何针孔相机摄像机几何 2. 相机标定线性方程组的解齐次线性方程组的解非线性方程组的最小二乘解透镜相机标定带畸变的相机标定 3. 单视图重建2D平面上的变换3D空间上的变换单视测量无穷远点 无穷远线 无穷远平面影消点 影消线单视重构 4. 三…

AJAX踩坑指南(知识点补充)

JWT JSON Web Token是目前最为流行的跨域认证解决方案 如何获取&#xff1a;在使用JWT身份验证中&#xff0c;当用户使用其凭据成功登录时&#xff0c;将返回JSON Web Token(令牌&#xff09; Token本质就是一个包含了信息的字符串 如何获取Token:登录成功之后&#xff0c;服务…

K8s+Nacos实现应用的优雅上下线【生产实践】

文章目录 前言一、环境描述二、模拟请求报错三、配置优雅上下线1.修改nacos配置2.修改depolyment配置3.重新apply deployment后测试4.整体(下单)测试流程验证是否生效 四、期间遇到的问题 前言 我们在使用k8s部署应用的时候&#xff0c;虽然k8s是使用滚动升级的&#xff0c;先…

处理登录失效后提示多个错误

问题: 我的场景是后端规定&#xff0c;即使登录失效返回的code仍是200&#xff0c;然后data的code是999什么的&#xff1b; 原本代码&#xff1a; 修改版代码&#xff1a; 通过节 const NotLoginEvent () > {router.replace("/login");localStorage.clear();M…

队列——数据结构——day5

队列 队列(queue)是只允许在一端进行插入操作&#xff0c;而在另一端进行删除操作的线性表。 队列是一种先进先出(First In First Out)的线性表&#xff0c;简称FIFO。允许插入的一端称为队尾&#xff0c;允许删除的一端称为队头。假设队列是q(a1,a2,……,an),那么a1就是队头元…

银行数字人民币系统应用架构设计

2019年10月&#xff0c;01区块链联合数字资产研究院发布了《人民币3.0&#xff1a;中国央行数字货币运行框架与技术解析》&#xff0c;从数字货币界定和人民币发展历程出发&#xff0c;区分了央行数字货币与比特币、移动支付等的区别&#xff0c;全面介绍了央行数字货币的发展历…

C#,图论与图算法,有向图(Direct Graph)广度优先遍历(BFS,Breadth First Search)算法与源程序

1 图的广度优先遍历 图的广度优先遍历(或搜索)类似于树的广度优先遍历(参见本文的方法2)。这里唯一需要注意的是,与树不同,图可能包含循环,因此我们可能再次来到同一个节点。为了避免多次处理节点,我们使用布尔访问数组。为简单起见,假设所有顶点都可以从起始顶点到达…

ChatGPT智能聊天系统源码v2.7.6全开源Vue前后端+后端PHP

测试环境:Linux系统CentOS7.6、宝塔、PHP7.4、MySQL5.6,根目录public,伪静态thinkPHP,开启ssl证书 具有文章改写、广告营销文案、编程助手、办公达人、知心好友、家庭助手、出行助手、社交平台内容、视频脚本创作、AI绘画、思维导图等功能 ai通道:文心一言、MiniMax、智…

GraalVM详细安装及打包springboot、java、javafx使用教程(打包springboot2篇)

前言 在当前多元化开发环境下&#xff0c;Java作为一种广泛应用的编程语言&#xff0c;其应用部署效率与灵活性的重要性日益凸显。Spring Boot框架以其简洁的配置和强大的功能深受开发者喜爱&#xff0c;而JavaFX则为开发者提供了构建丰富桌面客户端应用的能力。然而&#xff…

Linux设备驱动开发 - 三色LED呼吸灯分析

By: fulinux E-mail: fulinux@sina.com Blog: https://blog.csdn.net/fulinus 喜欢的盆友欢迎点赞和订阅! 你的喜欢就是我写作的动力! 目录 展锐UIS7885呼吸灯介绍呼吸灯调试方法亮蓝灯亮红灯亮绿灯展锐UIS7885呼吸灯DTS配置ump9620 PMIC驱动ump9620中的LED呼吸灯驱动LED的tr…

python--容器、列表

1.python官方内置的容器 list: set: tuple: dict: 弱数据类语言通通没有数组&#xff0c;因为数组指的是 类型固定、大小固定、连续的内存空间。 2.链表&#xff1a; 非连续内存空间 python用的是双向链表 单向链表&#xff1a;优点&#xff1a;不浪费内存&#xf…

代码随想录day28(1)二叉树:二叉搜索树中的插入操作(leetcode701)

题目要求&#xff1a;给定二叉搜索树&#xff08;BST&#xff09;的根节点和要插入树中的值&#xff0c;将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据保证&#xff0c;新值和原始二叉搜索树中的任意节点值都不同。 思路&#xff1a;对于二叉搜索树来说&…

如何使用ospf (enps) 简单实践ospf协议

1. OSPF的基本概念 OSPF&#xff08;Open Shortest Path First&#xff0c;开放式最短路径优先&#xff09;是一种广泛应用于TCP/IP网络中的内部网关协议&#xff08;Interior Gateway Protocol, IGP&#xff09;&#xff0c;主要用于在同一自治系统&#xff08;Autonomous Sys…

SpringBoot项目集成XXL-job

文章目录 首先引入依赖配置信息配置类定义定时任务执行方法配置任务执行器配置任务执行计划 在集成 XXL-job 前,首先确保部署了 XXL-job 的 admin 服务, 如果还没有部署的话请参照 Docker安装部署XXL-Job 将 XXL-job 部署起来. 此时, XXL-job 已经部署好了, 下来一步一步的来集…

【Python 滑块不同的操作】对滑块进行处理,列如切割、还原、去除、无脑识别距离等等

文章日期&#xff1a;2024.03.23 使用工具&#xff1a;Python 类型&#xff1a;图片滑块验证的处理&#xff08;不限于识别距离&#xff09; 使用场景&#xff1a;&#xff1f; 文章全程已做去敏处理&#xff01;&#xff01;&#xff01; 【需要做的可联系我】 AES解密处理&a…

BGP4+简介

定义 BGP是一种用于自治系统AS&#xff08;Autonomous System&#xff09;之间的动态路由协议&#xff0c;常用版本是BGP-4&#xff0c;BGP-4只能传递IPv4路由。针对IPv6的BGP4扩展&#xff0c;通常称为BGP4。 目的 BGP4用于在AS之间传递路由信息&#xff0c;并不是所有情况…

Python爬取歌曲宝音乐:轻松下载Jay的歌

歌曲宝是一个不用付费就能听jay的歌曲&#xff0c;但是每次都只能播放一首不方便&#xff0c;于是今天想把它下载下来&#xff0c;本地循环播放&#xff0c;它所用到的接口是某我的还不错哈 获取搜索接口 分析html请求接口&#xff0c;获取到的数据是直接渲染好的HTML内容&…

lvs+keepalived+nginx实现高可用

主机&#xff1a;192.168.199.132 备机&#xff1a;192.168.199.133 真实服务器1&#xff1a;192.168.199.134 真实服务器2&#xff1a;192.168.199.135 问题&#xff1a; 防火墙没关 132配置ipvsadm进行dr模式 132配置keepalived.conf 133配置ipvsadm进行dr模式 133配置ke…