C++学习记录——이십일 AVL树

news2024/11/16 19:48:44

文章目录

  • 1、了解AVL树
  • 2、模拟实现
    • 3、旋转
      • 1、左单旋
      • 2、右单旋
      • 3、双旋(先左后右)
      • 4、双旋(先右后左)
    • 4、检查平衡
    • 5、测试性能(随机数)
    • 6、删除


1、了解AVL树

如果数据有序或接近有序,二叉搜索树将退化为单支数,查找元素相当于在顺序表中搜索元素,效率低下。因为有两位俄罗斯数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的办法:当向二叉搜索树插入新节点后,如果能保证每个结点的左右子树高度差的绝对值不超过1,即可降低树的高度,减少平均搜索长度。

AVL树也叫高度平衡搜索树。左右子树高度差叫平衡因子。平衡因子只是AVL树实现的一种方式,还有别的方式。在平衡因子方式中,如果平衡因子是-1,0,1,那么就是AVL树。

2、模拟实现

博客末尾有整体代码的链接

结构

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)
	{}
};

Insert

template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	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->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}
		cur->_parent = parent;
		return true;
	}
private:
	Node* _root = nullptr;
};

这是搜索树的代码,在此基础上继续写,完成一个AVL树的版本。

如何写?加入这个新结点插入左子树最下边一个结点的后面了,它当然有三个可能,在最下结点的左右两边,或者和这个结点同一层。插入完成后,平衡因子要更新,更新完后就得查看bf(上图代码中代表因子的变量) 是否还是符号要求,如果绝对值大于1,平衡结构收到影响,就需要旋转处理。

插入结点后这个结点只会影响它的部分祖先,也就是那一个子树里的某些结点。这时候就可以利用parent来访问祖先。

如果新结点在父节点右边,父节点的因子++;如果在左边,结点–。更新完后父结点的平衡因子如果等于1或者-1,说明高度变了,要更新爷爷结点的因子。

从cur和parent现在的位置出发,cur新增结点当然是0,假如在parent右边,parent的因子变为1,cur来到parent位置,parent来到上面的结点位置,假如原parent所在结点在父结点的右侧,那么因为在右侧所以要++,现在parent的位置就会变成2,这就出来另一种情况了,更新完后parent的因子变为2,或者可能出现-2,这时候parent所在的子树不平衡,就需要处理一遍,也就是旋转。

如果插入新结点后,parent的因子是0,就不需要往上调整了。说明这时候parent原本是1或者-1,插入之后就高度不变,就可以插入结束了。

所以就是

parent的bf绝对值是1,那就需要往上更新
parent的bf是0,那就插入结束
parent的bf绝对值大于1,那就旋转

可能会出现这种情况,一路更新到根节点。

		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)
			{
				//需要旋转处理
			}
			else
			{
				//为了防止出现更多绝对值大于1的情况
				assert(false);
			}
		}

3、旋转

1、左单旋

在这里插入图片描述

abc都是高度为h的子树。比如h == 2,就可以有两个子树或一个左子树或一个右子树的3种情况。c一定是有两个子树的,而ab则是三种情况中任意一种。

h == 1

在这里插入图片描述

在最底层最右边,也就是c,无论插入在左还是右,c的因子都会变化,对于60这个结点来说,右子树插入新结点,它+1,而30变为2.

h == 0,abc位置没有结点,插入后往上走,各因子的变化如同上一段。

h == 2,c有四个位置可插入,无论哪一个,都会让30结点的因子变为2.如果c结点不是有两个子树,就不会让30这个结点变为2。因为只有一个子树的话,插入后高度不变,所以不会更新。在这种情况下,ab总共有9种组合,c可以有4个变化,所以有36个组合。

h == 0和1,各有2个组合,总共40个组合。无论怎么样,都是左单旋。最后都要变成第一个图第三个模样。

b变成30的右边,30变成60的左边,60变成整棵树的根。

旋转的原则是保持搜索树,并且左右均衡,降低整棵树的高度

2、右单旋

在这里插入图片描述

在这里插入图片描述

了解左单旋后,右单旋就会更容易理解。

b变成60的左边,60变为30的右边,30成为整棵树的根。

右单旋情况分析和左单旋一样。

代码

	void RotateL(Node* parent)//左单旋
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		//前置设定好后,开始旋转。要考虑清除,每个结点有三个指针的,三叉链
		//subRL可能为空
		//旋转的不一定是整棵树
		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;
		Node* ppnode = parent->_parent;
		subR->_left = parent;
		parent->_parent = subR;
		if (ppnode == nullptr)
		{
			_root = subR;
			_root->_parent = nullptr;
		}
		else
		{
			if (ppnode->_left == parent)
			{
				ppnode->_left = subR;
			}
			else
				ppnode->_right = subR;
			subR->_parent == ppnode;
		}
		parent->_bf = subR->_bf = 0;
	}

	void RotateR(Node* parent)//右单旋
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;
		Node* ppnode = parent->_parent;
		subL->_right = parent;
		parent->_parent = subL;
		if (ppnode == nullptr)
		{
			_root = subL;
			_root->_parent = nullptr;
		}
		else
		{
			if (ppnode->_left == parent)
			{
				ppnode->_left = subL;
			}
			else
				ppnode->_right = subL;
			subL->_parent = ppnode;
		}
		subL->_bf = parent->_bf = 0;
	}

调用的条件

				if (parent->_bf == 2 && cur->_bf == 1)
				{
					RotateL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == -1)
				{
					RotateR(parent);
				}

3、双旋(先左后右)

在这里插入图片描述

h == 0,abcd都不存在,60结点也不存在,60这个结点就相当于新增。此时30因子是1,90因子是-2。

在这里插入图片描述

上图为h == 1,60的左右子树插入结点后都需要旋转。

h == 2,画图比较麻烦。bc是1,都是1个结点,ad则是三种情况的一种,b或者c的任意一个孩子位置插入结点都会引发旋转,这样总共36种,加上上面的,总共40种。

这时候一个单旋不能解决问题,需要两个单旋。如果b位置插入结点,30和60左旋,60和90右旋;

	void RotateLR(Node* parent)
	{
		RotateL(parent->_left);
		RotateR(parent);
	}

双选控制因子比较麻烦。b位置插入的话,60原始因子是-1,最后30 60 90对应的因子分别是001;如果是c插入,60原始因子是1,就是-100;h == 0的时候,两种情况分别是10-2,000。

	void RotateLR(Node* parent)
	{
		//parent为90,subL是30,subLR是60
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;
		RotateL(parent->_left);
		RotateL(parent);
		if (bf == 1)
		{
			parent->_bf = 0;
			subLR->_bf = 0;
			subL->_bf = -1;
		}
		else if (bf == -1)
		{
			parent->_bf = 1;
			subLR->_bf = 0;
			subL->_bf = 0;
		}
		else if(bf == 0)
		{
			parent->_bf = 0;
			subLR->_bf = 0;
			subL->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

4、双旋(先右后左)

h == 0,60就是新增结点。

h == 1,60下面没有子树,30有一个左结点,90有一个右结点,那么在60左右新增结点都会引发旋转

h == 2,那么a和d就有三种情况,60有两个子结点bc,bc可以新增4个结点。

有三种情况会引发双旋,c插入,c高度变化为h;b插入,b高度变化为h;60本身是新增点。要判断是哪个情况就判断60的因子,也就是1,-1,0

	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;
		RotateR(parent->_right);
		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);  
		}
	}

进入旋转前的判断条件

		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
			{
				//为了防止出现更多绝对值大于1的情况
				assert(false);
			}
		}

测试代码:

void Test_AVLTree1()
{
	int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	AVLTree<int, int> t1;
	for (auto e : a)
	{
		t1.Insert(make_pair(e, e));
	}
	t1.InOrder();
}

4、检查平衡

这里检查高度即可,平衡因子因为是我们自己定的,用来判断是否是平衡二叉树有些不妥。

	int _High(Node* root)
	{
		if (root == NULL) return 0;
		int leftH = _High(root->_left);
		int rightH = _High(root->_lright);
		return leftH > rightH ? leftH + 1 : rightH + 1;
	}

	bool _IsBalance(Node* root)
	{
		if (root == NULL) return true;
		int leftH = _High(root->_left);
		int rightH = _High(root->_right);
		//所有子树都要检查,所以除了根结点的左右子树,还有剩下结点的
		return abs(leftH - rightH) < 2
			&& _IsBalance(root->_left)
			&& _IsBalance(root->_right);
	}
	
	bool IsBalance()
	{
		return _IsBalance(_root);
	}

两个测试用例
int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };

虽然树正常,但是平衡因子的数值可能不对劲。比如一个结点和它的右子树的因子都是0,这时候再次插入就会引发因子数值的错误。这样改

	bool _IsBalance(Node* root)
	{
		if (root == NULL) return true;
		int leftH = _High(root->_left);
		int rightH = _High(root->_right);
		if (rightH - leftH != root->_bf)
		{
			cout << root->_kv.first << "节点平衡因子异常" << endl;
			return false;
		}
		//所有子树都要检查,所以除了根结点的左右子树,还有剩下结点的
		return abs(leftH - rightH) < 2
			&& _IsBalance(root->_left)
			&& _IsBalance(root->_right);
	}
void Test_AVLTree1()
{
	//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	AVLTree<int, int> t1;
	for (auto e : a)
	{
		t1.Insert(make_pair(e, e));
		cout << e << "" << t1.IsBalance() << endl;//每插入一次就打印看看谁插入出了问题
	}
	t1.InOrder();
	cout << t1.IsBalance() << endl;
}

把LR的函数中调整因子的代码屏蔽掉,第二个测试用例就会出现6节点异常。

5、测试性能(随机数)

void Test_AVLTree2()
{
	srand(time(0));
	const size_t N = 100000;
	AVLTree<int, int> t;
	for (size_t i = 0; i < N; ++i)
	{
		size_t x = rand();
		t.Insert(make_pair(x, x));
	}
	cout << t.IsBalance() << endl;
}

AVL树的性能和满二叉树差不多,高度是logn,增删查改是logn,插入10亿个结点需要最多查找30次。

6、删除

整体和插入呈现相反的思路。比如删除右边子节点,父节点因子-1,删除后如果父节点是-1就不需要调整,如果是0,那么就需要调整。

下一篇是红黑树。

链接: https://gitee.com/kongqizyd/start-some-c-codes-for-learning.c/blob/master/AVL%E6%A0%91/AVLTree.h

结束。

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

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

相关文章

Java+Python+Paddle提取长文本文章中词频,用于Echart词云图数据

公司有个需求&#xff0c;就是需要提供给echart词云图的数据&#xff0c;放在以前我们的数据来源都是从产品那直接要&#xff0c;产品也是跑的别的接口&#xff0c;那怎么行呢&#xff0c;当然有自己的一套可以随便搞了&#xff0c;那么操作来了 Java package cn.iocoder.yud…

推荐几款2023年还在用的IDE工具

近期有不少刚学编程的小伙伴来问我&#xff0c;市面上那么多IDE工具&#xff0c;该怎么选&#xff1f;今天在这里跟大家分享几款个人比较钟爱的IDE工具&#xff0c;供大家参考。 Visual Studio 优点&#xff1a;支持多种语言&#xff0c;包括C#, C, Visual Basic等&#xff0c…

【Linux】进程信号“疑问?坤叫算信号吗?“

鸡叫当然也算信号啦~ 文章目录 前言一、认识信号量二、信号的产生 1.调用系统函数向进程发信号2.由软件条件产生信号3.硬件异常产生信号总结 前言 信号在我们生活中很常见&#xff0c;下面我们举一举生活中信号的例子&#xff1a; 你在网上买了很多件商品&#xff0c;再等待不…

【跟着陈七一起学C语言】今天总结:函数、数组、指针之间的关系

友情链接&#xff1a;专栏地址 知识总结顺序参考C Primer Plus&#xff08;第六版&#xff09;和谭浩强老师的C程序设计&#xff08;第五版&#xff09;等&#xff0c;内容以书中为标准&#xff0c;同时参考其它各类书籍以及优质文章&#xff0c;以至减少知识点上的错误&#x…

深度学习实战29-AIGC项目:利用GPT-2(CPU环境)进行文本续写与生成歌词任务

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下深度学习实战29-AIGC项目&#xff1a;利用GPT-2(CPU环境)进行文本续写与生成歌词任务。在大家没有GPU算力的情况&#xff0c;大模型可能玩不动&#xff0c;推理速度慢&#xff0c;那么我们怎么才能跑去生成式的模型…

14 KVM虚拟机配置-配置虚拟设备(其它常用设备)

文章目录 14 KVM虚拟机配置-配置虚拟设备&#xff08;其它常用设备&#xff09;14.1 概述14.2 元素介绍14.3 配置示例 14 KVM虚拟机配置-配置虚拟设备&#xff08;其它常用设备&#xff09; 14.1 概述 除存储设备、网络设备外&#xff0c;XML配置文件中还需要指定一些其他外部…

Python+selenium,轻松搭建 Web 自动化测试框架

在程序员的世界中&#xff0c;一切重复性的工作&#xff0c;都应该通过程序自动执行。「自动化测试」就是一个最好的例子。 随着互联网应用开发周期越来越短&#xff0c;迭代速度越来越快&#xff0c;只会点点点&#xff0c;不懂开发的手工测试&#xff0c;已经无法满足如今的…

云渲染靠谱吗,使用云渲染会不会被盗作品?

云渲染靠谱吗、安全吗&#xff1f;如果使用 云渲染会不会被盗作品......Renderbus瑞云渲染作为一个正经的云渲染平台&#xff0c;也时不时会收到这类疑问&#xff0c;首先&#xff0c;瑞云渲染是肯定靠谱的,各位可以放心使用。另外小编也将在本篇教你如何辨别云渲染平台是否安全…

通达信W底形态选股公式,也称双底形态

W底形态&#xff0c;也称双底形态&#xff0c;是一种经典的技术分析形态&#xff0c;代表了跌势的逆转。看起来像字母 "W"&#xff0c;描述了一波下跌&#xff0c;反弹&#xff0c;再次下跌到与上一波下跌相同或相近的位置&#xff0c;最后是另一波反弹。W底形态两次…

【细读Spring Boot源码】@ComponentScan是如何生效的?

前言 在使用SpringBoot使用过程中 RestController、Service、Repository这几个注解类上都标有Component注解 启动类上标有的SpringBootApplication注解类上有个ComponentScan注解。那么ComponentScan如何把相关的对象注册到BeanFactory的&#xff1f; 找到处理ComponentScan注…

【Qt 从入门到入土】下篇

【Qt 从入门到入土】上篇 一个非常好的学习 Qt 的视频 本文目录 6. 对话框QDialog6.1 基本概念6.2 标准对话框6.3 自定义消息框6.4 消息对话框6.5 标准文件对话框 7. 布局管理器7.1 系统提供的布局控件7.2 利用widget做布局 8. 常用控件8.1 QLabel 控件使用8.2 QLineEdit8.3 其…

1_5 pytorch操作

一、torch 算子 1、torch.nn.functional.affine_grid(theta, size) 给定一组仿射矩阵(theta)&#xff0c;生成一个2d的采样位置(流场)&#xff0c;通常与 grid_sample() 结合使用,用于空间仿射变换网络&#xff0c;用于对2D或3D数据进行仿射变换。 输入&#xff1a;theta(Te…

5.20 牛奶咖啡·仙羽「重生」巡回演唱会 武汉站 告白夜浪漫收官

牛奶咖啡主唱仙羽「重生」主题巡回演唱会将于2023年5月20日浪漫收官。经历多次延期&#xff0c;「重生」巡回演唱会武汉站 终于与大家见面&#xff0c;届时会为大家带来多首传唱度极高的歌曲&#xff0c;与歌迷朋友共度一个难忘的告白夜。 关于仙羽仙羽&#xff08;kiki&#…

微服务架构演变

微服务架构演变 认识微服务 服务架构演变 单体架构&#xff1a;将业务的所有功能集中在一个项目种开发&#xff0c;打成一个包部署 优点&#xff1a; 架构简单部署成本低 缺点&#xff1a; 耦合度高 分布式架构&#xff1a;根据业务功能对系统进行拆分&#xff0c;每个业…

python4delphi之初体验

最近需要做一个excel导入的工具, excel表格是python的强项,于是想delphi结合python实现 delphi环境xe2 python 3.6.6 上图可以看到,真的很爽,我在vscode写python脚本,然后给delphi调用 读取了一个excel文件,打印出了列头。 最爽的是,我在没wps,office环境的机…

【计算机视觉 | 目标检测】Grounding DINO 深度学习环境的配置(含案例)

“Grounding DINO&#xff1a;Marrying DINO with Grounded Pre-Training for Open-Set Object Detection”的官方 PyTorch 实现&#xff1a;SoTA 开放集对象检测器。 文章目录 一、Helpful Tutorial二、相关的论文工作2.1 相关的论文整理2.2 论文的亮点2.3 论文介绍2.4 Marryi…

shell脚本之数组与冒泡排序

目录 一. 数组1.1 数组定义&#xff1a;1.2 数组包括的数据类型&#xff1a;1.3 向函数传入数组的值 二. 冒泡排序算法 一. 数组 1.1 数组定义&#xff1a; 方法一&#xff1a; 数组名&#xff08; 1 2 3 4 5 &#xff09; 方法二&#xff1a; 数组名&#xff08; [0]1 [1]2 […

Nginx 概述和缓存能力

官方说明 理解说明 Nginx是一个高性能的Web服务器和反向代理服务器&#xff0c;其设计思想和执行机制可以概括如下&#xff1a; 异步事件驱动&#xff1a;Nginx采用基于事件驱动的异步I/O模型&#xff0c;这意味着它能够处理大量并发连接而不会消耗太多的系统资源。 单线程模…

(六)Kubernetes - 手动部署(二进制方式安装)

Kubernetes - 手动部署 [ 5 ] 1 部署NginxKeepalived高可用负载均衡器1.1 安装软件包(Master1/Master2)1.2 Nginx配置文件(主备相同)1.3 keepalived配置文件(Master1)1.4 keepalived配置(master2)1.5 Nginx增加Steam模块1.5.1 查看Nginx版本模块1.5.2 下载同一个版本的nginx1.5…

如何提高执行力

在最近的工作中很多事情容易拖延的&#xff0c;导致部分工作和自己的生活容易因为拖延导致混乱&#xff0c;比如没有提前预订好抢票&#xff0c;结果导致放假买不到票等等的事情出现。 所以很多事懒的原因是执行力差&#xff0c;为什么执行力差&#xff0c;说明事情不重要。那就…