数据结构——AVL树

2023-09-15 15:03:59

目录

1.什么是AVL树?

2.AVL树插入的模拟实现

①节点定义

②插入

③旋转

⑴右单旋

⑵左单旋

⑶双旋(右左旋)

⑷双旋(左右旋)

⑸完整的插入代码

3.AVL树的性能分析


1.什么是AVL树?

AVL树是一种自平衡二叉查找树,也被称为高度平衡树。它具有以下特点:

  1. 它本身是一棵二叉搜索树,即每个结点包含一个关键字和两个子结点,且满足左子树中所有关键字小于该结点的关键字,右子树中所有关键字大于该结点的关键字。
  2. AVL树带有平衡条件,即每个结点的左右子树的高度之差的绝对值(平衡因子)最多为1。为了保持这种平衡,可能需要通过一次或多次树旋转来重新平衡,这可能需要对树进行调整。

2.AVL树插入的模拟实现

①节点定义

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

	pair<K, V> _kv;
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;
	int _bf;                        //平衡因子
};

因为在使用过程中可能会频繁使用到父节点,因此我们将其设计为三叉链,且在这里我们设计一个平衡因子(注:在AVL树中可能没有平衡因子,在这里引入平衡因子只是为了方便我们理解),规定一个初始节点的平衡因子为0,当它的左子树中出现新节点时平衡因子就--,右子树中出现新节点时平衡因子就++,当平衡因子==2或者==-2时,此时我们就认为当前节点往下的树已经失衡,需要对其作出调整(各式各样的旋转)

②插入

bool Insert(const pair<K, V>& kv)
{
	if (_root == nullptr)
	{
		_root = new Node(kv);
		return true;
	}

	Node* cur = _root;
	Node* parent = nullptr;
	// 寻找要插入的位置
	while (cur)
	{
		if (kv.first < cur->_kv.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (kv.first > cur->_kv.first)
		{
			parent = cur;
			cur = cur->_right;
		}
		else
		{
			return false;
		}
	}

	// 到此处cur已经指向了应该插入的位置,
	// 然后判断应该插入到parent的哪边
	cur = new Node(kv);
	if (kv.first > cur->_kv.first)
	{
		parent->_right = cur;
	}
	else
	{
		parent->_left = cur;
	}
	cur->_parent = parent;

	// 插入完成后要更改平衡因子
	// 从父节点向上更新一直更新到平衡因子为0(已平衡)
	// 或者更新到平衡因子为2或-2(已失衡)
	// 或者更新到根节点的父节点为止
	while (parent)
	{
		if (cur == parent->_left)
			parent->_bf--;
		else
			parent->_bf++;

		if (parent->_bf == 0)
		{
			break;
		}
		else if (parent->_bf == -1 || parent->_bf == 1)
		{
			cur = parent;
			parent = parent->_parent;
		}
		else if (parent->_bf == 2 || parent->_bf == -2)
		{
			// 此时当前节点已失衡,需要通过旋转来调整

			// ......(在下方结合图片具体分析)
		}
		else
		{
			assert();
		}
	}

	return true;
}

③旋转

根据插入位置的不同,可以具体地将它们大致分为四类。它们分别有对应的旋转策略,在这里我们使用先特殊后推广到一般的方法来解释

⑴右单旋

此情况适用于新节点插入较高左子树的左侧时,抽象图如下

当h==0时,示例图如下

当h==1时,示例图如下

接下来推广到一般,示例图如下

不难看出,右单旋操作的关键是将当前节点(即5)的右边赋给父节点(即10)的左边,然后将当前节点(即5)的右边指向父节点,再增添一些细节后可以得到如下右单旋代码

void RotateR(Node* parent)
{
	Node* cur = parent->_left;    // 记录当前节点
	Node* curright = cur->_right; // 记录当前节点的右节点

	// 如果当前节点的右节点为空说明是h为0的情况
	// 不为空时就要进行节点间的连接
	if (curright) 
	{
		curright->_parent = parent;
	}

	parent->_left = curright;
	cur->_right = parent;

	// 此时需要确定parent是否属于子树
	if (parent == _root)
	{
		_root = cur;
		cur->_parent = nullptr;
	}
	else // 此时parent以下的节点属于子树
	{
		cur->_parent = parent->_parent;

		// 确认parent与其父节点间的关系
		// 然后将cur与parent的父节点连接起来
		if (parent->_parent->_left == parent)
		{
			parent->_parent->_left = cur;
		}
		else
		{
			parent->_parent->_right = cur;
		}
	}
	parent->_parent = cur;

	// 在进行右单旋后,当前节点与父节点的平衡因子均变为0
	cur->_bf = parent->_bf = 0;
}

⑵左单旋

此情况适用于新节点插入较高右子树的右侧,抽象图如下

其具体情况与右单旋类似,这里就不过多赘述,直接给出代码

void RotateL(Node* parent)
{
	Node* cur = parent->_right;    // 记录当前节点
	Node* curleft = cur->_left; // 记录当前节点的左节点

	// 如果当前节点的左节点为空说明是h为0的情况
	// 不为空时就要进行节点间的连接
	if (curleft)
	{
		curleft->_parent = parent;
	}

	parent->_right = curleft;
	cur->_left = parent;

	// 此时需要确定parent是否属于子树
	if (parent == _root)
	{
		_root = cur;
		cur->_parent = nullptr;
	}
	else // 此时parent以下的节点属于子树
	{
		cur->_parent = parent->_parent;

		// 确认parent与其父节点间的关系
		// 然后将cur与parent的父节点连接起来
		if (parent->_parent->_left == parent)
		{
			parent->_parent->_left = cur;
		}
		else
		{
			parent->_parent->_right = cur;
		}
	}
	parent->_parent = cur;

	// 在进行左单旋后,当前节点与父节点的平衡因子均变为0
	cur->_bf = parent->_bf = 0;
}

⑶双旋(右左旋)

此情况适用于新节点插入较高左子树的右侧,抽象图如下

在此我们先以左边的插入情况举例

当h==0时,示例图如下

当h==1时,示例图如下

接下来推广到一般,示例图如下

接下来再让我们看看右边的情况

当h==0时,示例图如下

当h==1时,示例图如下

接下来推广到一般,示例图如下

结合上述几幅图像来看,从结果上来看,最终的结果是15节点的右边赋给了20节点的左边,15节点的左边边赋给了10节点的右边;此外,对于平衡因子来说,当h==0时,三个节点的平衡因子均被更新为0,而h!=0时,三个节点的平衡因子分为2种情况,当插入在15节点的左边时,三个节点的平衡因子分别被更新为0,0,1,当插入在15节点的右边时,三个节点的平衡因子分别被更新为0,0,-1。通过复用左单旋与右单旋的代码可以得到如下代码

void RotateRL(Node* parent)
{
	Node* cur = parent->_right;
	Node* curleft = cur->_left;
	int bf = curleft->_bf;
	RotateR(cur);
	RotateL(parent);

	if (bf == 0) // h==0的情况
	{
		parent->_bf = 0;
		cur->_bf = 0;
		curleft->_bf = 0;
	}
	else if (bf == 1) //新节点插入到右侧的情况
	{
		parent->_bf = -1;
		cur->_bf = 0;
		curleft->_bf = 0;
	}
	else if (bf == -1)//新节点插入到左侧的情况
	{
		cur->_bf = 1;
		parent->_bf = 0;
		curleft->_bf = 0;
	}
	else // 出现其他情况时报错
	{
		assert();
	}
}

⑷双旋(左右旋)

此情况适用于新节点插入较高左子树的右侧,具体抽象图如下

这里与上面的右左旋大同小异,因此在这里只画出两种情况的一般示例图与h==0的示例图

当h==0时,示例图如下

当新节点插入在7的左侧时,一般示例图如下

当新节点插入在7的右侧时,一般示例图如下

根据结果我们发现它的结果与右左旋类似,因此我们只需对代码作一定的修改即可,代码如下

void RotateLR(Node* parent)
{
	Node* cur = parent->_left;
	Node* curright = cur->_right;
	int bf = curright->_bf;
	RotateL(cur);
	RotateR(parent);

	if (bf == 0) // h==0的情况
	{
		parent->_bf = 0;
		cur->_bf = 0;
		curright->_bf = 0;
	}
	else if (bf == 1) //新节点插入到右侧的情况
	{
		parent->_bf = 0;
		cur->_bf = -1;
		curleft->_bf = 0;
	}
	else if (bf == -1)//新节点插入到左侧的情况
	{
		cur->_bf = 0;
		parent->_bf = 1;
		curleft->_bf = 0;
	}
	else // 出现其他情况时报错
	{
		assert();
	}
}

⑸完整的插入代码

bool Insert(const pair<K, V>& kv)
{
	if (_root == nullptr)
	{
		_root = new Node(kv);
		return true;
	}

	Node* cur = _root;
	Node* parent = nullptr;
	// 寻找要插入的位置
	while (cur)
	{
		if (kv.first < cur->_kv.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (kv.first > cur->_kv.first)
		{
			parent = cur;
			cur = cur->_right;
		}
		else
		{
			return false;
		}
	}

	// 到此处cur已经指向了应该插入的位置,
	// 然后判断应该插入到parent的哪边
	cur = new Node(kv);
	if (kv.first > cur->_kv.first)
	{
		parent->_right = cur;
	}
	else
	{
		parent->_left = cur;
	}
	cur->_parent = parent;

	// 插入完成后要更改平衡因子
	// 从父节点向上更新一直更新到平衡因子为0(已平衡)
	// 或者更新到平衡因子为2或-2(已失衡)
	// 或者更新到根节点的父节点为止
	while (parent)
	{
		if (cur == parent->_left)
			parent->_bf--;
		else
			parent->_bf++;

		if (parent->_bf == 0)
		{
			break;
		}
		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);
			}
			else if (parent->_bf == -2 && cur->_bf == -1)
			{
				RotateR(parent);
			}
			else if (parent->_bf == 2 && cur->_bf == -1)
			{
				RotateRL(parent);
			}
			else if (parent->_bf == -2 && cur->_bf == 1)
			{
				RotateLR(parent);
			}
				
			// 插入完成后结束插入
			break;
		}
		else
		{
			assert();
		}
	}

	return true;
}

3.AVL树的性能分析

        AVL树是一种绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1。这样的平衡条件使得AVL树在查询时的性能高效且稳定。在AVL树中,任何节点的两个子树的高度差都不超过1,因此,在执行查询操作时,最坏的情况就是需要遍历log(N)层节点,其中N是树中节点的数量,因此查询效率非常高。

        然而,如果需要对AVL树进行结构修改操作,比如插入或删除节点,维持其绝对平衡性的同时会导致性能降低。在插入节点时,需要维护其平衡性,这可能会导致旋转的次数增加。在最差的情况下,旋转次数可能达到O(log(N)),这会显著影响到插入操作的性能。

        在删除节点时,可能需要执行多次旋转操作来重新平衡树。在最差的情况下,删除操作可能需要O(log(N))的时间复杂度,因为需要一直旋转到根节点。因此,如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树。然而,如果数据经常需要修改,那么AVL树可能就不是最佳选择了。

        总的来说,AVL树在查询性能上表现出色,但如果需要经常进行结构修改操作,其性能就可能会变得较差。

更多推荐

Java多线程(一)

文章目录一、程序、进程、线程基本概念1.程序(program)2.进程(process)3.线程(thread)二、单核CPU和多核CPU的理解三、并行和并发1.并行2.并发四、创建多线程的方式一(继承Thread类)1.创建两个分线程,其中一个线程遍历100以内的偶数,另外一个线程遍历100以内的奇数2.售票案例五、

wait函数与waitpid函数

1.函数介绍2.wait函数#include<sys/types.h>#include<sys/wait.h>pid_twait(int*wstatus);功能:等待任意一个子进程结束,如果该子进程结束了,此函数会回收子进程的资源参数:-int*wstatus:进程退出时的状态信息,传入的是一个int类型的地址,传出参

MySQL学习笔记

目录注释1、启动和关闭MYSQL服务2、库的增删改查3、表的增删改查3.1创建表3.2修改表3.3删除4、数据类型4.1字符串:char(num)与varchar(num)的区别4.2整型4.3浮点型4.4日期型4.5枚举型注释单行注释:#注释文字(没空格)单行注释:--注释文字(有空格)多行注释:/*注释文字*/,注

高效畅通的iOS平台S5配置指南

在iOS平台上,使用S5代理ip访问互联网是一种非常有用的技巧。无论是为了保证隐私安全,还是解决网络限制问题,S5代理ip都能为您提供更快、更稳定的互联网访问体验。本文将为您详细介绍如何在iOS平台上配置和使用S5代理ip,让您的网络畅通无阻!一、了解S5代理ip的工作原理S5代理ip是一种网络代理协议,可以让您的设备

仿网易云-360度混响

一直在用网易云音乐听歌,感觉他的这个动效还是挺不错的,最近也是想试试canvas绘图相关的。尝试了几次之后感觉效果还不错,不过距离网易云的还是有些差距。本期准备仿照制作如下效果:偷偷使用最近比较流行的罗刹海市的音乐来展示这个效果。效果展示如下:效果展示网站参考文档具体的流程大体上就是获取音频数据,然后根据音频数据绘制在

zookeeper集群

一,zookeeper定义Zookeeper是一个开源的分布式的,为分布式框架提供协调服务的Apache项目。二,zookeeper工作机制Zookeeper从设计模式角度来理解:是一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,Zooke

【Linux】nohub指令--终端退出后命令仍旧执行

文章目录0、背景1、作用2、语法3、用法演示4、关于2>&10、背景Shell中,执行一个持续进行的指令,会"霸屏",即你想再执行其他指令,要么重开个shell终端,要么退出这个执行。1、作用nohub,即nohangup(不挂起),用于在系统后台不挂断地运行命令,Ctrl+C退出终端后命令依旧执行。2、语法nohup

JavaScript策略模式

JavaScript策略模式1什么是策略模式2实现一个基础的策略模式3Javascript中策略模式4使用策略模式实现缓动动画5使用策略模式实现表单校验1什么是策略模式策略模式(StrategyPattern)是一种行为型设计模式,它定义了一系列算法,将每个算法都封装起来,并且使它们可以相互替换。策略模式让算法独立于使

Java的Socket通信的断网重连的正确写法

Java的Socket通信的断网重连的正确写法Socket通信的断网重连介绍客户端与服务端源码演示截图本地演示服务器演示演示截图总结Socket通信的断网重连介绍针对于已经建立通信的客户端与服务器,当客户端与服务器因为网络问题导致网络不通而断开连接了或者由于服务器端的服务被突然停掉,而客户端进行的一种尝试重新建立连接的

通用商城项目(下)

记录一些踩坑的地方,以及理顺一些思路。通过管理系统页面,完成商品属性分组和商品属性(基本属性)关联维护属性表与属性组表的功能完善:显示属性组与属性表的一对多关系前端1.引入组件,是否显示使用v-if,但是还要注意引入的组件本身,是否自己也有:visible.sync="visible"这样的属性。只有当两层是否显示的变

通过内网穿透,在Windows 10系统下搭建个人《我的世界》服务器公网联机

文章目录1.Java环境搭建2.安装我的世界Minecraft服务3.启动我的世界服务4.局域网测试连接我的世界服务器5.安装cpolar内网穿透6.创建隧道映射内网端口7.测试公网远程联机8.配置固定TCP端口地址8.1保留一个固定tcp地址8.2配置固定tcp地址9.使用固定公网地址远程联机今天和大家分享一下只需简

热文推荐