【C++】平衡二叉搜索(AVL)树的模拟实现

news2024/10/7 16:27:42

一、 AVL树的概念

map、multimap、set、multiset 在其文档介绍中可以发现,这几个容器有个共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成 O ( N ) O(N) O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。
在这里插入图片描述

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。

因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年
发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

  • 它的左右子树都是AVL树
  • 左右子树高度之差(简称平衡因子,balance factor)的绝对值不超过1(-1/0/1)

如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 O ( l o g 2 n ) O(log_2 n) O(log2n),搜索时间复杂度O( l o g 2 n log_2 n log2n)。

在这里插入图片描述

二、AVL树节点的定义

AVL树的节点是三叉链结构:即parent、left和right,它们分别指向当前节点的父节点、左子节点和右子节点。通过这种方式,可以在 O ( 1 ) O(1) O(1)的时间内找到一个节点的父节点、左子节点和右子节点。

在这里插入图片描述

namespace AVL
{
	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; // 平衡因子

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

	};

	template<class K, class V>
	class AVLTree
	{
		typedef AVLTreeNode<K, V> Node;
	public:

	private:
		Node* _root = nullptr;
	};
}

三、AVL树的插入

AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:

  1. 按照二叉搜索树的方式插入新节点
  2. 调整节点的平衡因子

插入在左平衡因子-1,插入在右平衡因子+1

是否继续更新的依据:parent所在子树的高度是否变化

  1. parent->_bf == 0说明之前parent->_bf1 或者 -1 说明之前parent一边高一边低,这次插入填上矮的那边,parent所在子树高度不变,不需要继续往上更新
    在这里插入图片描述

  2. parent->_bf == 1-1说明之前是parent->_bf = 0,两边一样高,现在插入一边更高了,parent所在子树高度变了,继续往上更新
    在这里插入图片描述

  3. parent->_bf == 2-2,说明之前parent->_bf == 1或者-1,现在插入严重不平衡,违反规则,就地处理–旋转

bool insert(const pair<K, V>& kv)
{
	// 1. 先按照二叉搜索树的规则将节点插入到AVL树中
	// 空树直接构建根
	if (_root == nullptr)
	{
		_root = new Node(kv);
		return true;
	}

	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
		if (kv.first > cur->_kv.first) // 大了往右边走
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (kv.first < cur->_kv.first) // 小了往左边走
		{
			parent = cur;
			cur = cur->_left;
		}
		else
		{
			return false;// 相等不插入
		}
	}

	//开始插入
	cur = new Node(kv);// 新插入的节点
	// 小的插入左,大的插入右
	if (kv.first < parent->_kv.first)
	{
		parent->_left = cur;
		cur->_parent = parent;// 三叉链,不要忘记更新父指针
	}
	else
	{
		parent->_right = cur;
		cur->_parent = parent;
	}
	// 2. 新节点插入后,AVL树的平衡性可能会遭到破坏
	// 此时需要更新平衡因子,并检测是否破坏了AVL树的平衡性

	while (parent) // parent为空,也就更新到根停止
	{
		// 更新平衡因子
		// 新增在左,parent->bf--;
		// 新增在右,parent->bf++;
		if (cur == parent->_left)
		{
			parent->_bf--;
		}
		else
		{
			parent->_bf++;
		}
		//检测平衡因子
		if (parent->_bf == 0)
		{
			break;// 无需继续更新
		}
		else if (parent->_bf == 1 || parent->_bf == -1)
		{
			// 插入前parent的平衡因子是0,插入后parent的平衡因为为1 或者 - 1 ,说明以parent为根的二叉树
			// 的高度增加了一层,因此需要继续向上调整
			cur = parent;
			parent = parent->_parent;
		}
		else if (parent->_bf == 2 || parent->_bf == -2)
		{
			// parent的平衡因子为-2/2,违反了AVL树的平衡性
			// 需要对以 parent 为根的树进行 旋转 处理

			// 旋转

			break; // 旋转完成后,原 parent 为根的子树高度降低,已经平衡,不需要再向上更新
		}
		else
		{
			assert(false); // 平衡因子异常:绝对值大于2
		}
	}
	return true;
}

四、AVL树的旋转

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

旋转的目的:

  • 让这颗子树左右高度不超过1
  • 旋转过程中继续保持是搜索树
  • 更新调整孩子节点的平衡因子
  • 让这颗子树的高度跟插入前保持一致

根据节点插入位置的不同,AVL树的旋转分为四种:

  1. 新节点插入 较高 左 左 子树的 左 左 侧—左左:右单旋

在这里插入图片描述

在插入前,图中AVL树是平衡的,新节点插入到30的左子树(注意:此处不是左孩子,图中a/b/c是高度为 h 的AVL子树)中,30左子树增加了一层,导致以60为根的二叉树不平衡

要让60平衡,只能将60左子树的高度减少一层,右子树增加一层,即将左子树往上提,这样60转下来,因为60比30大,只能将其放在30的右子树,而如果30有右子树,右子树根的值一定大于30,小于60,只能将其放在60的左子树,旋转完成后,更新节点的平衡因子即可。

在旋转过程中,有以下几种情况需要考虑:

  1. 30节点的右孩子可能存在,也可能不存在
  2. 60可能是根节点,也可能是子树,如果是根节点,旋转完成后,要更新根节点,如果是子树,可能是某个节点的左子树,也可能是右子树

这里举一些详细的例子进行画图,考虑各种情况,加深旋转的理解

h == 0,则a/b/c是空树:

在这里插入图片描述

h == 1:

在这里插入图片描述

h == 2的情况已经有很多种了,随着h的增加情况会越来越复杂

在这里插入图片描述

看图写代码:

在这里插入图片描述


void RotateR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	// 30的右变成60的左
	parent->_left = subLR;

	if (subLR != nullptr) // 30的右不为空,更新_parent指针
	{
		subLR->_parent = parent;
	}

	Node* ppNode = parent->_parent;
	// 60变成30的右
	parent = subL->_right;
	parent->_parent = subL;//不要忘记更新parent的父指针

	if (_root == parent) // parent就是根
	//if (ppNode == nullptr) //也可以使用这个判断条件
	{
		_root = subL;
		_root->_parent = nullptr;
	}
	else // parent是左或右子树
	{
		// parent是左就把subL链接到左,是右就链接到右
		if (parent == ppNode->_left)
		{
			ppNode->_left = subL;
		}
		else
		{
			ppNode->_right = subL;
		}
		subL->_parent = ppNode;// 同样不要忘记更新subL的父指针
	}
	// 最后更新parent和subL的平衡因子
	parent->_bf = subL->_bf = 0;			
}

  1. 新节点插入较高 右 右 子树的 右 右 侧—右右:左单旋

在这里插入图片描述
左单旋实现及情况考虑可参考右单旋

h == 0的情况:

在这里插入图片描述

h == 1:

在这里插入图片描述

void RotateL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	// 60的左变成30的右
	parent->_right = subRL;
	// 更新subRL的父指针
	if (subRL)
	{
		subRL->_parent = parent;
	}
	Node* ppNode = parent->_parent;
	// 30变成60的左
	subR->_left = parent;
	parent->_parent = subR;

	//if (_root == parent)
	if (ppNode == nullptr)
	{
		_root = subR;
		_root->_parent = nullptr;
	}
	else
	{
		if (parent == ppNode->_left)
		{
			ppNode->_left = subR;
		}
		else
		{
			ppNode->_right = subR;
		}
		subR->_parent = ppNode;
	}
	parent->_bf = subR->_bf = 0;
}

像下图的情况简单的单旋已经不能正确调整平衡,需要使用双旋(不同轴点的单旋):

在这里插入图片描述

  1. 新节点插入较高 左 左 子树的 右 右 侧—左右:先左单旋再右单旋

a/d是高度为 h 的AVL树
b/c是高度为 h - 1 的AVL树
在这里插入图片描述

h == 0:

在这里插入图片描述

h == 1:

在这里插入图片描述

看图写代码:

void RotateLR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	int bf = subLR->_bf;

	// 对30左单旋,对90右单旋
	RotateL(parent->_left);
	RotateR(parent);

	// 最后更新平衡因子
	if (bf == 0) // subLR自己是新增
	{
		parent->_bf = 0;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
	else if (bf == -1) // 在subLR的左新增
	{
		parent->_bf = 1;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
	else if (bf == 1) // 在subLR的右新增
	{
		parent->_bf = 0;
		subL->_bf = -1;
		subLR->_bf = 0;
	}
	else
	{
		assert(false);// 异常处理
	}
}
  1. 新节点插入较高 右 右 子树的 左 左 侧—右左:先右单旋再左单旋

在这里插入图片描述

h == 0:

在这里插入图片描述

h == 1:

在这里插入图片描述

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

	RotateR(parent->_right);
	RotateL(parent);

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

总结:
假如以 parent 为根的子树不平衡,即 parent 的平衡因子为 2 或者 -2 ,分以下情况考虑:

  1. parent 的平衡因子为 2,说明 parent 的右子树高,设 parent 的右子树的根为 subR
    • 当 subR 的平衡因子为 1 时,执行左单旋
    • 当 subR 的平衡因子为 -1 时,执行右左双旋
  2. parent 的平衡因子为 -2 ,说明 parent 的左子树高,设 parent的左子树的根为 subL
    • 当 subL 的平衡因子为 -1 是,执行右单旋
    • 当 subL 的平衡因子为 1 时,执行左右双旋

旋转完成后,原 parent 为根的子树高度降低,已经平衡,不需要再向上更新。

insert 时平衡因子检测的整体代码:

while (parent) // parent为空,也就更新到根停止
{
	// 更新平衡因子
	// 新增在左,parent->bf--;
	// 新增在右,parent->bf++;
	if (cur == parent->_left)
	{
		parent->_bf--;
	}
	else
	{
		parent->_bf++;
	}
	//检测
	if (parent->_bf == 0)
	{
		break;// 无需继续更新
	}
	else if (parent->_bf == 1 || parent->_bf == -1)
	{
		// 插入前parent的平衡因子是0,插入后parent的平衡因为为1 或者 - 1 ,说明以parent为根的二叉树
		// 的高度增加了一层,因此需要继续向上调整
		cur = parent;
		parent = parent->_parent;
	}
	else if (parent->_bf == 2 || parent->_bf == -2)
	{
		// parent的平衡因子为-2/2,违反了AVL树的平衡性
		// 需要对以 parent 为根的树进行 旋转 处理

		if (parent->_bf == -2 && cur->_bf == -1) // 右单旋
		{
			RotateR(parent);
		}
		else if (parent->_bf == 2 && cur->_bf == 1) // 左单旋
		{
			RotateL(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; // 旋转完成后,原 parent 为根的子树高度降低,已经平衡,不需要再向上更新
	}
	else
	{
		assert(false); // 平衡因子异常:绝对值大于2
	}
}

AVL树的整体代码:AVL树的简单模拟实现


五、AVL树的验证

AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:

  1. 验证其为二叉搜索树
    如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
  2. 验证其为平衡树
    每个节点子树高度差的绝对值不超过1(注意节点中如果有平衡因子,还需验证节点的平衡因子是否计算正确
int Height(Node* root)
{
	if (root == nullptr)
		return 0;

	int lh = Height(root->_left);
	int rh = Height(root->_right);

	return lh > rh ? lh + 1 : rh + 1;
}

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

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

	int leftHeight = Height(root->_left);
	int rightHeight = Height(root->_right);

	if (rightHeight - leftHeight != root->_bf)
	{
		std::cout << root->_kv.first << " 平衡因子异常" << std::endl;
		return false;
	}

	return abs(rightHeight - leftHeight) < 2
		&& IsBalance(root->_left)
		&& IsBalance(root->_right);
}

六、AVL树的性能

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

但是如果要对AVL树做一些结构修改的操 作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。


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

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

相关文章

Java文档搜索引擎总结

Java文档搜索引擎总结项目介绍项目使用的技术栈前端页面展示后端逻辑部分索引部分搜索模块部分Web模块部分项目介绍 Java文档搜索引擎项目是一个SSM项目&#xff0c;该项目的前端界面部分是由搜索页面和展示页面组成&#xff0c;后端部分索引模块&#xff08;ScanAnalysis、in…

UNET 对 CARVANA 数据集的分割

目录 1. 介绍 2. UNET 网络 3. dataset 数据加载 4. utils 工具模块 4.1 get_loaders 函数 4.2 check_accuracy 函数 4.3 save_predictions_as_imgs 函数 4.4 完整代码 5. train 函数 5.1 关于导入的库文件 5.2 设置超参数 5.3 train_fn 训练一个epoch函数 5.4 m…

Redis是单线程还是多线程?Redis的10种数据类型,有哪些应用场景?

目录专栏导读一、同样是缓存&#xff0c;用map不行吗&#xff1f;二、Redis为什么是单线程的&#xff1f;三、Redis真的是单线程的吗&#xff1f;四、Redis优缺点1、优点2、缺点五、Redis常见业务场景六、Redis常见数据类型1、String2、List3、Hash4、Set5、Zset6、BitMap7、Bi…

【Java基础】30分钟Git 从入门到精通

一、 版本控制工具1、什么是版本控制系统&#xff1f;版本控制系统&#xff08;Version Control System&#xff09;:是一种记录一个或若干文件内容变化&#xff0c;以便将来查阅特定版本修订情况的系统。版本控制系统不仅可以应用于软件源代码的文本文件&#xff0c;而且可以对…

主成分分析(PCA)方法 和协方差 相关系数

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录前言1.主成分分析&#xff08;PCA&#xff09;方法2.算法步骤前言 主成分分析&#xff08;Principal Components Analysis&#xff0c;PCA&#xff09;是一种数据降…

Android工厂模式

工厂模式分为三种 :简单工厂模式 、工厂方法模式 、抽象工厂模式 。 目录 简单工厂模式 UML图 实现 使用场景&#xff1a; 优点 &#xff1a; 缺点&#xff1a; 工厂方法模式 UML图 实现 使用场景&#xff1a; 优点&#xff1a; 缺点&#xff1a; 抽象工厂模式 UM…

SpringCloud-负载均衡-Ribbon

文章目录1. 作用&#xff1a;负载均衡2. 应用实战2.1 provider-a代码2.2 provider-b代码2.3 consumer代码2.4 api工具向consumer发送请求查看对provider的调用情况3. ribbon总结3.1 Ribbon 源码核心3.2 如何实现负载均衡的呢&#xff1f;1. 作用&#xff1a;负载均衡 2. 应用实…

演唱会总是抢不到票?教你用Python制作一个自动抢票脚本

人生苦短 我用python 这个大家应该都知道吧&#xff1f; 是中国综合类现场娱乐票务营销平台&#xff0c; 业务覆盖演唱会、 话剧、音乐剧、体育赛事等领域。 如何快速抢票&#xff1f; 那么&#xff0c; 今天带大家用Python来制作一个自动抢票的脚本小程序 本文源码python安…

使用Java对稀疏数组的压缩与还原

稀疏矩阵的压缩与还原 稀疏数组中元素个数很少或者有大量的重复值&#xff0c;如果直接保存保存&#xff0c;会浪费很多空间&#xff0c;这时&#xff0c;就可以考虑对数组进行压缩存储。 先定义一个稀疏数组 //创建一个二维数组 11 * 11 int[][] array1 new int[11][11]; /…

Window 编辑、删除、新增右键菜单

关于 Window 右键菜单 右键菜单可以在注册表编辑器中新增和修改 建议先下载 registry-finder&#xff0c;查找速度更快&#xff01; 使用管理员模式打开 registry-finder 后&#xff0c;点击 HKEY_CLASSES_ROOT &#xff0c;修改注册表右键菜单的子路径如下表所示 类型路径…

49.在ROS中实现local planner(2)- 实现Purepersuit(纯跟踪)算法

48.在ROS中实现local planner&#xff08;1&#xff09;- 实现一个可以用的模板实现了一个模板&#xff0c;接下来我们将实现一个简单的纯跟踪控制&#xff0c;也就是沿着固定的路径运动&#xff0c;全局规划已经规划出路径点&#xff0c;基于该路径输出相应的控制速度 1. Pur…

Linux系列学习(三) - 进程和库文件

目录 引言&#xff1a; 学习&#xff1a; 基本命令补充&#xff1a; wc命令&#xff1a; more命令&#xff1a; less命令&#xff1a; cat ps命令&#xff1a; kill命令&#xff1a; bg命令&#xff1a; fg命令&#xff1a; 查看系统运行级别&#xff1a; 库文件&a…

unity UGUI系统梳理 - 常用可视化控件

作为一名合格的UI仔>.<&#xff0c;我发现很多UI很久没有使用了&#xff0c;所以我决定做一个UGUI系列博客重新梳理一下 1、Image 在没有放入图片下&#xff0c;image控件长这样 注意 我一般没交互需求的情况下都会把RaycastTarget给点掉&#xff0c;这个不单单是从提…

CAPL脚本DBLookup函数动态访问CAN 报文的属性

&#x1f345; 我是蚂蚁小兵&#xff0c;专注于车载诊断领域&#xff0c;尤其擅长于对CANoe工具的使用&#x1f345; 寻找组织 &#xff0c;答疑解惑&#xff0c;摸鱼聊天&#xff0c;博客源码&#xff0c;点击加入&#x1f449;【相亲相爱一家人】&#x1f345; 玩转CANoe&…

学习周报3.5

文章目录前言文献阅读摘要介绍方法总结相关性总结前言 本周阅读文献《Multi-step ahead probabilistic forecasting of multiple hydrological》&#xff0c;文献主要提出一种基于三维卷积神经网络、卷积最小门记忆神经网络和变分贝叶斯神经网络的混合深度学习模型&#xff08…

【博学谷学习记录】超强总结,用心分享|狂野大数据课程【Spark SQL函数定义】的总结分析

5.1 如何使用窗口函数 回顾: 窗口函数格式:分析函数 over(partition by xxx order by xxx [asc|desc] [rows between xxx and xxx])学习的相关分析函数有那些? 第一类: row_number() rank() dense_rank() ntile()第二类: 和聚合函数组合使用 sum() avg() max() min() count…

西电软件体系结构核心考点汇总(期末真题+核心考点)

文章目录前言一、历年真题二、核心考点汇总2.1 什么是软件体系架构?(软件体系结构的定义)2.2 架构风格优缺点2.3 质量属性2.4 质量评估前言 主要针对西安电子科技大学《软件体系结构》的核心考点进行汇总。 【期末期间总结资料如下】 针对西电计科院软件工程专业大三《软件体…

【QT】使用QML构建一个简易的计算器界面(三)

前面两篇对计算器界面的布局和显示以及实现功能做了相关优化&#xff0c;但是对输入显示那一块还没有具体的处理步骤&#xff0c;包括对输入表达式的合法性检查&#xff0c;显示框的多行历史显示等功能还需要添加&#xff0c;接下来将从这几个方面对这些功能进行添加。 1、对输…

概率论 1.3 古典概型与几何概型

1.3.1 排列与组合排列从n个不同元素任取r(r<n)个元素排成一列(考虑元素出现的先后次序)&#xff0c;称此为一个排列&#xff0c;此种排列的总数为n(n-1)....(n-r1)n!/(n-r)&#xff01;&#xff0c;若rn,则称为全排列&#xff0c;2.重复排列从n个不同元素中每次取出一个,放回…

GPIO输入和输出以及八种工作模式

一.GPIO的简介 GPIO &#xff08;general purpose input output&#xff09;是通用输入输出端口的简称&#xff0c;简单来说就是软件可控制的引脚&#xff0c;STM32芯片的GPIO引脚与外部传感器连接起来&#xff0c;从而实现与外部通讯、控制以及数据采集的功能。 1.引脚全是GP…