【C++笔记】AVL树的模拟实现

【C++笔记】AVL树的模拟实现

  • 一、AVL树的概念
  • 二、AVL树的模拟实现
    • 2.1、定义节点
    • 2.2、插入
    • 2.3、旋转
      • 2.3.1、左单旋
      • 2.3.2、右单旋
      • 2.3.3、左右双旋
      • 2.3.4、右左双旋
      • 2.3.5、插入接口的整体代码实现
  • 三、验证AVL树
    • 3.1、验证

一、AVL树的概念

二叉搜索树虽然在一般情况下可以提高查找的效率,但如果插入数据的顺序接近有序或有序,那二叉搜索树就会变成一个类似“单链表”的结构:
在这里插入图片描述
这样查找的效率就会变得和单链表一样是O(n)了,效率低下。
对此,两位俄罗斯的数学家G.M.Adelson-Velskii
和E.M.Landis在1962年
发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

一棵AVL树要么是空树,要么就是具有以下性质的一棵二叉搜索树:

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

比如下面这棵树就是一棵AVL树:
在这里插入图片描述

二、AVL树的模拟实现

2.1、定义节点

定义AVL树的节点除了传统的左右指针和值之外还需要定义两个成员——parent指针和平衡因子,因为我们后面在调整平衡的时候必须要用到它们:

// 定义AVL树节点
template <class K, class V>
struct AVLTreeNode {
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;
	pair<K, V> _val;
	int _bf; // 平衡因子
	// 构造
	AVLTreeNode(const pair<K, V>& key)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_val(key)
		,_bf(0)
	{}
};

然后同样的,我们只需要在AVLTree类里面封装一个根节点的指针即可:

// AVL树
template <class K, class V>
class AVLTree {
public:
	typedef AVLTreeNode<K, V> Node;
	// ……
private :
	Node* _root = nullptr;
};

2.2、插入

因为AVL树其实结构和普通二叉树没什么区别,只是规则不一样,所以我们这里就只需要实现与其他数不同的插入即可。

AVL树的插入也实现要找到插入位置,但是在插入成功之后还需要调整平衡因子,并判断是否需要进行调整,以确保满足AVL树的规则。

最容易的情况就是在插入新节点后,左右高度差并没有超出范围,这样就不需要调整了(调整到根节点为止,如果没有一个节点的平衡因子超出范围就不需要调整),例如在下面这AVL树中我们要插入一个值为9的节点:
在这里插入图片描述
当我们找到插入位置后就需要一直向上调整平衡因子:
在这里插入图片描述
当我们调整完后发现并么有平衡因子出现异常,因此也就不需要调整了,所执行的操作和二叉搜索树的插入一样了。

这里先给出不需要调整的代码实现:

// 插入
bool insert(const pair<K, V>& key) {
	if (nullptr == _root) {
		_root = new Node(key);
		return true;
	}
	Node* cur = _root;
	Node* parent = nullptr;
	
	// 先插入节点
	while (cur) {
		if (key.first > cur->_val.first) {
			parent = cur;
			cur = cur->_right;
		}
		else if (key.first < cur->_val.first) {
			parent = cur;
			cur = cur->_left;
		}
		else {
			return false;
		}
	}

	// 准备插入
	cur = new Node(key);
	if (key.first > parent->_val.first) {
		parent->_right = cur;
		cur->_parent = parent;
	}
	else {
		parent->_left = cur;
		cur->_parent = parent;

	}
	// 检查平衡因子,看看是否需要旋转调整
	while (parent) {
		//  先做第一步调整
		if (cur == parent->_left) {
			parent->_bf--;
		}
		else {
			parent->_bf++;
		}


		if (parent->_bf == 0) {
			return true;
		}
		else if (parent->_bf == 1 || parent->_bf == -1) { // 继续向上检查
			cur = parent;
			parent = parent->_parent;
		} else {
			assert(false);
		}
	}
	return true; // 插入成功
}

2.3、旋转

2.3.1、左单旋

而一旦我们的插入导致平衡因子超出了范围,这时候就需要调整了,比如我们想要在下面这棵AVL中在插入一个值为10的节点:
在这里插入图片描述
更新平衡因子的过程中,我们发现8这个节点的平衡因子超出了范围,这时候我们该怎样调整呢?

如果我们将8看做需要调整的这棵子树的根,那么对根为8的这棵子树进行 “左单旋” 就能解决问题。

像这样在右子树的右子树(右右)中插入新节点的情况,处理的方法称为左单旋,下面介绍具体步骤:
在这里插入图片描述
如上图所示,将节点8设置为parent,parent的parent设置成parent_parent,parent的右孩子设置成subRight,将subRight的左孩子设置成subRightL。
左单旋我们需要做的就是让subRight成为parent的右孩子,再让parent成为subRight的左孩子,然后让subRight成为这棵子树新的根,并让parent_parent连接上subRight。

相信大家看上面的图示都会感到眼花缭乱,所以我这里转化出了一个抽象图:
在这里插入图片描述
这里其实并不用关心子树的高度h具体是多高,因为h取任何值都是一样的。
从图中我们也可以观察的出,这里的本质其实是通过将subRight改成新的根节点,从而将整棵子树的高度降低了一个高度。
做完这些后不要忘了调整平衡因子,由图中我们可以很形象的看出直接把subRight和parent的平衡因子调整成0即可。
(图中省略了parent指针的处理,但是在实际实现中,parent指针是一定要处理的)

那这样做为什么正确呢?

通过之前对搜索二叉树规律的分析,我们可以得出另一个规律:对于一个根,它的左子树的所有节点一定是小于根节点的,右子树的所有节点一定是大于根的。
所以我们这里的subRightL也一定是大于parent的,所以它可以成为pareng的右子树,而subRight也一定是大于parent的,所以parent就可以成为subRight的根,所以经过上面的操作之后,这棵子树还是满足二叉搜索树的规律的。

那怎么判断什么情况下我们要进行左单旋呢?
我们可以通过平衡因子来判断,因为平衡因子的定义是右子树与左子树的高度差,所以如果一个根它的平衡因子是2并且它的右子树的平衡因子是1,则说明是在右子树的右子树中插入新节点导致的不平衡。

所以一棵子树要进行左单旋判断条件是:

root->_bf == 2 && root->_right->_bf == 1;

接下来就是左单旋的代码实现:

// 左单旋
void RotateL(Node* parent) {
	Node* subRight = parent->_right;
	Node* subRightL = subRight->_left;
	Node* parent_parent = parent->_parent;

	parent->_right = subRightL;
	if (subRightL) {
		subRightL->_parent = parent;
	}
	subRight->_left = parent;
	parent->_parent = subRight;

	if (parent == _root) { // 如果当前的parent是根
		_root = subRight;
		subRight->_parent = nullptr;
	}
	else {
		// 如果当前的parent还不是根
		if (parent == parent_parent->_left) {
			parent_parent->_left = subRight;
		}
		else {
			parent_parent->_right = subRight;
		}
		subRight->_parent = parent_parent;
	}


	// 调整平衡因子
	parent->_bf = 0;
	subRight->_bf = 0;
}

2.3.2、右单旋

右单旋和做单旋在逻辑上是一样的,基本就是左单旋改一下方向就可以了。
当我们在左子树的左子树(左左)中插入节点导致不平衡的时候,就需要用到右单旋:
在这里插入图片描述
处理过程的抽象图也和左单旋的差不多:
在这里插入图片描述
然后这是右单旋的代码实现:

	// 右单旋
	void RotateR(Node* parent) {
		Node* subLeft = parent->_left;
		Node* subLeftR = subLeft->_right;
		Node* parent_parent = parent->_parent;

		parent->_left = subLeftR;
		if (subLeftR) {
			subLeftR->_parent = parent;
		}
		subLeft->_right = parent;
		parent->_parent = subLeft;

		if (parent == _root) { // 如果当前的parent为根
			_root = subLeft;
			subLeft->_parent = nullptr;
		}
		else {
			// 如果当前的parent不为根
			if (parent == parent_parent->_left) {
				parent_parent->_left = subLeft;
			}
			else {
				parent_parent->_right = subLeft;
			}
			subLeft->_parent = parent_parent;
		}

		// 调整平衡因子
		subLeft->_bf = 0;
		parent->_bf = 0;
	}

2.3.3、左右双旋

然后还有一些情况是单纯的左单旋和右单旋不能解决的,因为它们并不是单纯的“左左”或“右右”,比如在左子树的右子树中插入一个节点导致不平衡:
在这里插入图片描述
如果这时候只是单纯的对90这个根进行右单旋的话就会变成下面这样子:
在这里插入图片描述
我们会返现调整后30节点的平衡因子还是超出了范围,所以这样是不能解决问题的。

其实这里的主要问题是插入的位置变了,我们简单的分析一下单纯的右单旋就能的出问题的出处:
在这里插入图片描述
通过观察我们发现,右边单旋我们是通过把高的那颗子树(a)“往上层移动”来达到减少整体高度的,而矮的那颗子树(b)所在的层数不变。
而如果是在b这棵子树中插入:
在这里插入图片描述
其实就变成了,减少左子树的一个高度,去增加右子树的一个高度,但是还是不平衡,而且转化后也不是一个单纯的左单旋。

那该怎样解决呢?
其实我们可以先通过对30这棵子树进行左单旋,从而使旋转后的整棵子树变成左高右低的形式:
在这里插入图片描述
这样就可以把90这棵子树变成单纯的右单旋了。
然后再对90进行右单旋就变平衡了:
在这里插入图片描述

但是平衡因子又该怎么更新呢?
对于平衡因子更新的处理,我们可以先来看看双旋后的对比:
在这里插入图片描述

从中我们可以看得出,双旋其实还可以这样处理:
让subLeft成为新的根,然后subLeft成为subLeftR的左子树,parent成为subLeftR的右子树,子树b成为subLeft的右子树,子树c成为parent的左子树。

所以平衡因子的更新,的关键点就在于subLeftR的平衡因子,或者说是看subLeftR这棵子树的左子树高还是右子树高,即子树b高还是子树c高。

如果subLeftR的平衡因子为0,则说明subLeft本身就是新加入的节点,此时a,b,c,d这四棵子树都是空,所以subLeftR、subLeft和parent的平衡因子都为0。

如果subLeftR的平衡因子为1,则表示子树c要比子树b高1,所以更新后parent的平衡因子为0,subLeft的平衡因子为-1。

如果subLeftR的平衡因子为1,则表示子树b要比子树c高1,所以更新后subLeft的平衡因子为0,parent的平衡因子为1。

然后这是左右双旋的代码实现:

// 左右双旋
void RotateLR(Node* parent) {
	Node* subLeft = parent->_left;
	Node* subLeftR = subLeft->_right;
	int bf = subLeftR->_bf;
	RotateL(parent->_left);
	RotateR(parent);

	// 调整平衡因子
	if (0 == bf) {
		parent->_bf = 0;
		subLeft->_bf = 0;
		subLeftR->_bf = 0;
	}
	else if (-1 == bf) {
		subLeft->_bf = 0;
		parent->_bf = 1;
		subLeftR->_bf = 0;
	}
	else if (1 == bf) {
		subLeft->_bf = 0;
		parent->_bf = 0;
		subLeftR->_bf = -1;
	}
	else {
		assert(false);
	}
}

2.3.4、右左双旋

右左双旋的分析逻辑其实和左右双旋一样,只是方向不同罢了。
在这里插入图片描述
然后这是右左双旋的代码实现,基本和左右双旋的一样:

// 右左双旋
void RotateRL(Node* parent) {
	Node* subRight = parent->_right;
	Node* subRightL = subRight->_left;
	int bf = subRightL->_bf;
	RotateR(parent->_right);
	RotateL(parent);
	
	// 调整平衡因子
	if (0 == bf) {
		// subRightL自己就是新增节点
		parent->_bf = 0;
		subRightL->_bf = 0;
		subRight->_bf = 0;
	}
	else if (1 == bf) {
		// subRight的右边新增
		parent->_bf = -1;
		subRight->_bf = 0;
		subRight->_bf = 0;
	}
	else if (-1 == bf) {
		// subRight的左边新增
		parent->_bf = 0;
		subRightL->_bf = 0;
		subRight->_bf = 1;
	}
	else {
		assert(false);
	}
}

2.3.5、插入接口的整体代码实现

// 插入
bool insert(const pair<K, V>& key) {
	if (nullptr == _root) {
		_root = new Node(key);
		return true;
	}
	Node* cur = _root;
	Node* parent = nullptr;
	
	// 先插入节点
	while (cur) {
		if (key.first > cur->_val.first) {
			parent = cur;
			cur = cur->_right;
		}
		else if (key.first < cur->_val.first) {
			parent = cur;
			cur = cur->_left;
		}
		else {
			return false;
		}
	}

	// 准备插入
	cur = new Node(key);
	if (key.first > parent->_val.first) {
		parent->_right = cur;
		cur->_parent = parent;
	}
	else {
		parent->_left = cur;
		cur->_parent = parent;

	}
	// 检查平衡因子,看看是否需要旋转调整
	while (parent) {
		//  先做第一步调整
		if (cur == parent->_left) {
			parent->_bf--;
		}
		else {
			parent->_bf++;
		}


		if (parent->_bf == 0) {
			return true;
		}
		else if (parent->_bf == 1 || parent->_bf == -1) { // 继续向上检查
			cur = parent;
			parent = parent->_parent;
		}
		else if (parent->_bf == 2 || parent->_bf == -2) {
			if (parent->_bf == 2 && cur->_bf == 1) { // 左单旋
				RotateL(parent);
				return true;
			}
			else if (parent->_bf == -2 && cur->_bf == -1) { // 右单旋
				RotateR(parent);
				return true;
			}
			else if (parent->_bf == 2 && cur->_bf == -1) { // 右左双旋
				RotateRL(parent);
				return true;

			}
			else if (parent->_bf == -2 && cur->_bf == 1) { // 左右双旋
				RotateLR(parent);
				return true;
			}
		}
		else {
			assert(false);
		}
	}
	return true; // 插入成功
}

三、验证AVL树

3.1、验证

验证AVL树我们需要验证两个方面,一个是左右高度差,另一个是平衡因子,因为有时候就算左右高度差就算正常,平衡因子在更新的时候也有可能会出错。而平衡因子一旦出错,就很有可能会带出更多的错误。

所以我们可以写一个判断是否平衡的函数:

// 检查是否平衡
bool isBalance() {
	return _isBalance(_root);
}

bool _isBalance(Node* root) {
	if (nullptr == root) {
		return true;
	}
	int leftH = _height(root->_left);
	int rightH = _height(root->_right);
	// 顺便检查一些平衡因子是否异常
	if (rightH - leftH != root->_bf) {
		cout << root->_val.first << " 平衡因子异常" << endl;
		return false;
	}
	return abs(rightH - leftH) < 2 && _isBalance(root->_left) && _isBalance(root->_right);
}

// 计算高度
int _height(Node* root) {
	if (nullptr == root) {
		return 0;
	}
	return max(_height(root->_left), _height(root->_right)) + 1;
}

// 中序遍历
void Inorder() {
	_Inorder(_root);
	cout << endl;
}

// 中序遍历子函数
void _Inorder(Node* root) {
	if (nullptr == root) {
		return;
	}
	_Inorder(root->_left);
	cout << "(" << root->_val.first << ", " << root->_bf << ") ";
	_Inorder(root->_right);
}

然后我们就可以来跑几个测试看一下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

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

相关文章

生成式AI以及当前趋势

ChatGPT 激发了人们的想象力和好奇心。自 2022 年 11 月推出后&#xff0c;短短两个月内其月活用户便达到 1 亿&#xff0c;成为有史以来增长速度最快的消费类应用和第一个杀手级的生成式 AI 应用。随着创新节奏的加快&#xff0c;想要紧跟生成式 AI 的发展速度&#xff0c;难度…

web前端-Gulp入门

web前端-Gulp入门 gulp的概述使用gulp准备工作gulp的常用APIgulp的常用插件gulpfile.js的初体验打包css文件打包scss文件打包js打包html打包images创建一个默认任务创建一个删除任务gulp启动服务创建一个监控任务 gulp的概述 gulp&#xff1a; 前端自动化打包固件工具&#xf…

Ansible playbook详解

playbook是ansible用于配置&#xff0c;部署&#xff0c;和被管理被控节点的剧本 playbook常用的YMAL格式&#xff1a;&#xff08;文件名称以 .yml结尾&#xff09; 1、文件的第一行应该以 "---" (三个连字符)开始&#xff0c;表明YMAL文件的开始。    2、在同一…

IIC子系统测温湿度

采用stm32MP157AAA芯片&#xff0c;温度传感器 si7006 0x40 1、在内核空间不支持浮点数进行打印&#xff0c;所以需要将读取到的数据拷贝到用户空间&#xff0c;执行用户程序打印 2、在probe函数中 分步注册字符设备驱动自动创建设备节点 3、在i2c驱动代码中&#xff0c;需要自…

通用的链栈实现(C++)

template<class T> class MyStack//链栈 { private:struct StackNode{T data;StackNode* next;StackNode(const T& val T(), StackNode* p nullptr) :data(val), next(p) {}//};StackNode* top;int cursize;void clone(const MyStack& s){Clear();cursize s.c…

cgo与调用c的回调函数指针

cgo直接调用函数&#xff0c;使用基本数据类型非常简单&#xff0c;包括一些结构体也比较简单&#xff0c;嵌套的稍微复杂些&#xff0c;但也可以&#xff0c;但有的时候&#xff0c;cgo调用c函数&#xff0c;会需要传递一个回调函数的指针&#xff0c;这时候就比较复杂了&…

office365 outlook邮件无法删除

是否遇到过&#xff0c;office365邮件存储满了&#xff0c;删除邮件无法删除&#xff0c;即便用web方式登录到outlook&#xff0c;删除邮件当时是成功的&#xff0c;但一会儿就回滚回来了&#xff0c;已删除的邮件&#xff0c;你想清空&#xff0c;最后清理后还是回到原样。 请…

YTM32的循环冗余校验CRC外设模块详解

YTM32的循环冗余校验CRC外设模块详解 文章目录 YTM32的循环冗余校验CRC外设模块详解引言原理与机制CRC算法简介从CRC算法到CRC硬件外设 应用要点&#xff08;软件&#xff09;CRC16 用例CRC32 用例 总结参考文献 引言 在串行通信帧中&#xff0c;为了保证数据在传输过程中的完…

基于Python优化图片亮度与噪点

支持添加噪点类型包括&#xff1a;添加高斯噪点、添加椒盐噪点、添加波动噪点、添加泊松噪点、添加周期性噪点、添加斑点噪点、添加相位噪点&#xff0c;还提供清除噪点的功能。 我们先看一下实测效果&#xff1a;&#xff08;test.jpg为原图&#xff0c;new.jpg为添加后的图片…

自动化测试的成本高效果差,那么自动化测试的意义在哪呢?

一、自动化测试的成本高效果差&#xff0c;那么自动化测试的意义在哪呢&#xff1f; 我觉得这个问题带有很强的误导性&#xff0c;是典型的逻辑陷阱之一。“自动化测试的成本高效果差”是真的吗&#xff1f;当然不是。而且我始终相信&#xff0c;回答问题的最好方式是把问题本身…

达索系统3DEXPERIENCE WORKS 2024流体仿真功能增强

设计工作中&#xff0c;网格划分和设计验证十分重要&#xff0c;它可以方便我们把复杂组件简单化处理&#xff0c;从而提升工作效率。 轻松应对&#xff0c;精准划分 在优化设计以获得更好的空气动力学性能时&#xff0c;需要了解空气在其周围产生的流动方式。达索系统3DEXPE…

(论文阅读29/100 人体姿态估计)

29.文献阅读笔记 简介 题目 DeepCut: Joint Subset Partition and Labeling for Multi Person Pose Estimation 作者 Leonid Pishchulin, Eldar Insafutdinov, Siyu Tang, Bjoern Andres, Mykhaylo Andriluka, Peter Gehler, and Bernt Schiele, CVPR, 2016. 原文链接 h…

STM32 X-CUBE-AI:Pytorch模型部署全流程

文章目录 概要版本&#xff1a;参考资料STM32CUBEAI安装CUBEAI模型支持LSTM模型转换注意事项模型转换模型应用1 错误类型及代码2 模型创建和初始化3 获取输入输出数据变量4 获取模型前馈输出模型应用小结 小结 概要 STM32 CUBE MX扩展包&#xff1a;X-CUBE-AI部署流程&#xf…

Accelerate 0.24.0文档 一:两万字极速入门

文章目录 一、概述1.1 PyTorch DDP1.2 Accelerate 分布式训练简介1.2.1 实例化Accelerator类1.2.2 将所有训练相关 PyTorch 对象传递给 prepare()方法1.2.3 启用 accelerator.backward(loss) 1.3 Accelerate 分布式评估1.4 accelerate launch1.4.1 使用accelerate launch启动训…

k8s集群搭建(ubuntu 20.04 + k8s 1.28.3 + calico + containerd1.7.8)

环境&需求 服务器&#xff1a; 10.235.165.21 k8s-master 10.235.165.22 k8s-slave1 10.235.165.23 k8s-slave2OS版本&#xff1a; rootvms131:~# lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 20.04.5 LTS Release: …

Java自学第11课:电商项目(4)重新建立项目

经过前几节的学习&#xff0c;我们已经找到之前碰到的问题的原因了。那么下面接着做项目学习。 1 新建dynamic web project 建立时把web.xml也生成下&#xff0c;省的右面再添加。 会询问是否改为java ee环境&#xff1f;no就行&#xff0c;其实改过来也是可以的。这个不重要。…

css3 初步了解

1、css3的含义及简介 简而言之&#xff0c;css3 就是 css的最新标准&#xff0c;使用css3都要遵循这个标准&#xff0c;CSS3 已完全向后兼容&#xff0c;所以你就不必改变现有的设计&#xff0c; 2、一些比较重要的css3 模块 选择器 1、标签选择器&#xff0c;也称为元素选择…

滚珠螺杆在注塑机械手中起什么作用?

注塑机械手的配件中滚珠螺杆是重要的一环&#xff0c;在注塑机械手中起着重要的作用。注塑机械手是一种自动化设备&#xff0c;可以在注塑生产中实现自动化操作&#xff0c;而滚珠螺杆则是实现这一操作的关键部件之一。 滚珠螺杆是一种将旋转运动转化为直线运动的精密传动部件&…

在Linux系统下微调Llama2(MetaAI)大模型教程—Qlora

Llama2是Meta最新开源的语言大模型&#xff0c;训练数据集2万亿token&#xff0c;上下文长度是由Llama的2048扩展到4096&#xff0c;可以理解和生成更长的文本&#xff0c;包括7B、13B和70B三个模型&#xff0c;在各种基准集的测试上表现突出&#xff0c;最重要的是&#xff0c…