AVL 树

2023-09-20 15:00:00

一、AVL 树的概念

二叉搜索树 中,当我们连续插入有序的数据时,二叉搜索树可能会呈现单枝树的情况,此时二叉搜索树的查找效率为 O(N)

俄罗斯的两位数学家 G. M. Adelson-Velsky 和 E. M. Landis 发明了 AVL 树可以解决上述问题,AVL 树保证树中的每个结点的左右子树高度差不会超过 1,从而保证 AVL 树是一颗高度平衡的二叉搜索树,从而保证 AVL 树的搜索效率为 O(log N),AVL 树的名字就是取自于这两位科学家

一颗 AVL 树是 空树 或者满足如下条件:

  • 左右子树的高度差小于等于 1 的二叉搜索树
  • 左右子树均为 AVL 树

AVL 树是一颗在二叉搜索树并且满足所有结点的左右子树高度差不超过 1
在这里插入图片描述

二、AVL 树的实现

AVL 树有很多实现方式,这里采用三叉链和平衡因子,结点的平衡因子的值为右子树的高度减去左子树的高度,通过控制所有结点的平衡因子的绝对值小于等于 1,并且保证该树为二叉搜索树,即可实现 AVL 树

1. AVL 树的存储结构

// AVL 树的结点
template<class K, class V>
struct AVLTreeNode
{
	std::pair<K, V> _kv;
	AVLTreeNode<K, V>* _parent;
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	int _bf;	// 平衡因子: 右子树的高度前去左子树的高度

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

// AVL 树
template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	AVLTree<K, V>()
		: _root(nullptr)
	{}

private:
	Node* _root;
};

2. AVL 树的插入

首先按照二叉搜索树的方式插入结点,保证插入结点之后还是二叉搜索树,当插入结点完成之后,该结点的祖先结点的平衡因子可能会受到影响,如果插入结点在祖先结点的左子树中,则祖先结点的 _bf --,否则该结点的 _bf ++(平衡因子的值为右子树的高度减去左子树的高度)

祖先结点的 _bf 更新后,有三种情况 _bf == 0 和 _bf == -1 || _bf == 1 以及 _bf == -2 || _bf == 2

  • 当 _bf == 0 时:当前更新 _bf 的结点所在的子树高度没有变化,此时不用继续更新祖先结点的 _bf

如果插入结点在祖先结点的右子树,祖先结点的平衡因子从 -1 -> 0
如果插入结点在祖先节点的左子树,祖先结点的平衡因子从 1 -> 0

无论是这两种的那种情况,对于更新后 _bf == 0 的结点的祖先结点而言,子树的高度是没有变化的
在这里插入图片描述

  • 当 _bf == -1 || _bf == 1 时,当前更新 _bf 的结点所在的子树高度增加了,此时需要继续更新祖先结点的 _bf

如果插入结点在祖先结点的右子树,祖先结点的平衡因子从 0 -> 1
如果插入结点在祖先节点的左子树,祖先结点的平衡因子从 0 -> -1

无论是这两种的那种情况,对于更新后 _bf == -1 || _bf == 1 的结点的祖先结点而言,子树的高度都增加了 1
继续更新父结点

  • 当 _bf == -2 || _bf == 2 时,当前更新 _bf 的结点左右子树高度差超过 1 了,已经不平衡了,此时需要对该结点所在的子树进行旋转,旋转之后该结点的 _bf 会变成 0,此时也不用继续更新祖先结点的 _bf 了

旋转有四种情况:右单旋、左单旋、左单旋再右单旋、右单旋再左单旋
在这里插入图片描述

  • 右单旋:插入结点在较高左子树的左侧

在这里插入图片描述

  • 左单旋:插入结点在较高右子树的右侧,旋转方法类似于右单旋

  • 左单旋再右单旋:插入结点在较高左子树的右侧,旋转方法类似于右单旋再左单旋

  • 右单旋再左单旋:插入结点在较高右子树的左侧

在这里插入图片描述

// 右单旋
void RotateR(Node* parent)
{
	Node* pparent = 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 (pparent == nullptr) _root = subL;
	else
	{
		if (pparent->_kv.first > subL->_kv.first) pparent->_left = subL;
		else pparent->_right = subR;
	}
	subL->_parent = pparent;
}

// 左单旋
void RotateL(Node* parent)
{
	Node* pparent = 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 (pparent == nullptr) _root = subR;
	else
	{
		if (pparent->_kv.first > subR->_kv.first) pparent->_left = subR;
		else pparent->_right = subR;
	}
	subR->_parent = pparent;
}


// 插入
bool Insert(const std::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->_left;
		}
		else if (cur->_kv.first < kv.first)
		{
			parent = cur;
			cur = cur->_right;
		}
		else return false;
	}

	cur = new Node(kv);
	if (parent->_kv > kv.first) parent->_left = cur;
	else parent->_right = cur;

	cur->_parent = parent;

	// 更新平衡因子
	while (parent)
	{
		// 如果插入结点在祖先结点的左子树,_bf--
		// 如果插入结点在祖先结点的右子树,_bf++
		if (parent->_left == cur) parent->_bf--;
		else parent->_bf++;

		// 当 _bf == 0 时,结点所在的子树高度没有变化,不用继续更新祖先结点的 _bf
		// 当 _bf == -1 || _bf == 1 时,结点所在的子树高度增加 1,需要继续更新祖先结点的 _bf,最多更新到根结点
		// 当 _bf == -2 || _bf == 2 时,结点所在的子树不平衡了,需要对子树进行旋转,旋转之后 _bf 变为 0,也不用继续更新祖先结点的 _bf 了
		// 当 _bf 为其他值时,说明出大问题了
		if (parent->_bf == 0) break;
		else if (parent->_bf == -1 || parent->_bf == 1)
		{
			// 继续更新
			parent = parent->_parent;
			cur = cur->_parent;
		}
		else if (parent->_bf == -2 || parent->_bf == 2)
		{
			// 旋转
			// parent->_bf == -2 && cur->_bf == -1 右单旋
			// parent->_bf ==  2 && cur->_bf ==  1 左单旋
			// parent->_bf == -2 && cur->_bf ==  1 左单旋再右单旋
			// parent->_bf ==  2 && cur->_bf == -1 右单旋再左单旋
			// 当 _bf 为其他值时,说明出大问题了
			if (parent->_bf == -2 && cur->_bf == -1)
			{
				RotateR(parent);
				parent->_bf = 0;
				cur->_bf = 0;
			}
			else if (parent->_bf == 2 && cur->_bf == 1)
			{
				RotateL(parent);	
				parent->_bf = 0;
				cur->_bf = 0;
			}
			else if (parent->_bf == -2 && cur->_bf == 1)
			{
				Node* sub = cur->_right;
				int bf = sub->_bf;

				RotateL(cur);
				RotateR(parent);

				// bf ==  0 sub 就是新增
				// bf == -1 sub 左边新增
				// bf ==  1 sub 右边新增
				sub->_bf = 0;
				if (bf == 0)
				{
					parent->_bf = 0;
					cur->_bf = 0;
				}
				else if (bf == -1)
				{
					parent->_bf = 1;
					cur->_bf = 0;
				}
				else if (_bf == 1)
				{
					parent->_bf = 0;
					cur->_bf = -1;
				}
				else assert(false);
			}
			else if (parent->_bf == 2 && cur->_bf == -1)
			{
				Node* sub = cur->_left;
				int bf = sub->_bf;

				RotateR(cur);
				RotateL(parent);

				// bf ==  0 sub 就是新增
				// bf == -1 sub 左边新增
				// bf ==  1 sub 右边新增
				sub->_bf = 0;
				if (bf == 0)
				{
					parent->_bf = 0;
					cur->_bf = 0;
				}
				else if (bf == -1)
				{
					parent->_bf = 0;
					cur->_bf = 1;
				}
				else if (bf == 1)
				{
					parent->_bf = -1;
					cur->_bf = 0;
				}
				else assert(false);
			}
			else assert(false);

			break;
		}
		else assert(false);
	}

	return true;
}
更多推荐

基于中文金融知识的 LLaMA 系微调模型的智能问答系统:LLaMA大模型训练微调推理等详细教学

项目设计集合(人工智能方向):助力新人快速实战掌握技能、自主完成项目设计升级,提升自身的硬实力(不仅限NLP、知识图谱、计算机视觉等领域):汇总有意义的项目设计集合,助力新人快速实战掌握技能,助力用户更好利用CSDN平台,自主完成项目设计升级,提升自身的硬实力。专栏订阅:项目大全提升自身的硬实力[专栏详细介绍:项目设计

malloc与free

目录前提须知:malloc:大意:头文件:申请空间:判断是否申请成功:使用空间:结果:整体代码:malloc申请的空间怎么回收呢?注意事项:free:前提须知:为什么要有动态内存分配?我们已经掌握的内存开辟⽅式有:intval=20;//在栈空间上开辟四个字节chararr[10]={0};//在栈空间上开辟10个字节

postgresql-索引与优化

postgresql-索引与优化索引简介索引类型B-树索引哈希索引GiST索引SP-GiST索引GIN索引BRIN索引创建索引唯一索引多列索引函数索引部分索引覆盖索引查看索引维护索引删除索引索引简介索引(Index)可以用于提高数据库的查询性能;但是索引也需要进行读写,同时还会占用更多的存储空间;因此了解并适当利用索引

Web 3.0 发展到什么水平了?

最初,有互联网:电线和服务器的物理基础设施,让计算机和它们前面的人相互交谈。美国政府的阿帕网在1969年发出了第一条消息,但我们今天所知道的网络直到1991年才出现,当时HTML和URL使用户可以在静态页面之间导航。将此视为只读Web或Web1。在2000年代初期,情况开始发生变化。首先,互联网的互动性越来越强;这是一

金融投资公司如何实现创新, 盛创汇凭借人工智能站上硬科技C位

作为硬科技产业的重要组成部分,近年人工智能受到了国家政策的高度重视。在《“十四五”规划和2035年远景目标纲要》中,人工智能被摆放在科技前沿领域攻关方面的首要位置,先后八次被提及。《规划》指出,对新一代人工智能,要在前沿基础理论突破,专用芯片研发,深度学习框架等开源算法平台构建,学习推理与决策、图像图形、语音视频、自然

深入思考redis面经

1redission分布式锁1.1为了保证数据一致性,引入了redission的锁,你是为了抗住高并发而去为了引入这个锁还是说为了保证数据一致性去引入的答:主要是为了抗住高并发问题,解决redis的缓存击穿问题,但是也能解决一定的数据一致性问题。是的,当我们谈到“击穿”问题时,通常指的是缓存击穿,即当某个热点缓存失效时

C++ PrimerPlus 复习 第一章 命令编译链接文件 make文件

第一章命令编译链接文件C++有什么呢?C++源代码文件后缀运行C++过程可执行代码:编译语法:makeMakefile基础语法编写完make只要和将要编译的文件放一起就行然后在该目录使用make命令,就将自动运行;基础的Makefile版本现在的缺点也大\^-\^更加健全的Makefile但还是有不小的缺点常用版本C+

C++设计模式_04_Strategy 策略模式

接上篇,本篇将会介绍C++设计模式中的Strategy策略模式,和上篇模板方法TemplateMethod一样,仍属于“组件协作”模式,它与TemplateMethod有着异曲同工之妙。文章目录1.动机(Motivation)2.代码演示Strategy策略模式2.1传统方法处理2.2怎么用扩展的方式来支持未来的变化呢

PROB: Probabilistic Objectness for Open World Object Detection(论文解析)

PROB:ProbabilisticObjectnessforOpenWorldObjectDetection摘要2相关工作摘要开放世界目标检测(OWOD)是一个新的、具有挑战性的计算机视觉任务,它弥合了传统的目标检测(OD)基准和现实世界中的目标检测之间的差距。除了检测和分类已知/标记的对象外,OWOD算法还应该能够

【论文阅读】Untargeted Backdoor Attack Against Object Detection(针对目标检测的无目标后门攻击)

文章目录一.论文信息二.论文内容0.摘要1.论文概述2.背景介绍3.作者贡献4.重点图表一.论文信息论文题目:UntargetedBackdoorAttackAgainstObjectDetection(针对目标检测的无目标后门攻击)发表年份:2023-ICASSP(CCF-B)作者信息:ChengxiaoLuo(清华

为什么JWT要结合Redis使用

JWT解决了什么问题存储token的情况:校验token需要重复调用数据库耗时的问题JWT本身缺陷alg不要指定为none算法不要指定数组,只使用一种算法令牌长度可能会超过允许的URL长度,和cookie长度如果需要跟踪用于速率限制和IP白名单的API这些功能,那么使用无状态是不现实的以上都是容易避免的问题,JWT最大

热文推荐