数据结构——AVL树

2023-10-31

目录

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树在查询性能上表现出色,但如果需要经常进行结构修改操作,其性能就可能会变得较差。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

数据结构——AVL树 的相关文章

  • 音乐服务器制作教程,让NAS做音乐服务器

    最终的目的是让手机APP可以随时播放家里NAS上下载的音乐 经过以前的尝试和最近百度谷歌 有了一些成果 分享出来 一 NAS自带服务和手机APP 优点是布署简单 都是直接用 我只用过黑群的软件 只能说可以用 除了特简单再没有什么特点 需要有

随机推荐

  • 用Charles来模拟弱网测试环境

    在我们平时的测试过程中 需要模拟很多的测试场景 比如常见的弱网测试 你不可能说去地铁 停车场实地去测试 那么我们就需要模拟弱网环境 今天就讲一下如何通过charles来模拟弱网环境 1 首先打开Charles 点击Proxy 选择Throt
  • Latex插入参考文献的两种方法—自动与手动

    先忍不住吐槽一下 为啥都21世纪了还有期刊要求参考文献要放在 tex文件里面 使用 bib文件多简洁优美啊 现在我们就来看下latex中插入参考文献的两种方法 第一种 自动方法 使用 bib文件 在主文件 tex的同级目录下创建exampl
  • 6、服务数据的定义和使用

    一 服务数据模型 二 具体实现步骤 1 首先现在功能包中创建一个srv的文件夹 然后在改文件夹下新建一个以 srv为后缀的文件 所举例的该文件的具体内容如下 string name uint8 age uint8 sex uint8 unk
  • 2023最新版Anaconda下载安装教程(非常详细)从零基础入门到精通,看完这一篇就够了

    1 前言 小编的电脑是win10系统的 这里以win10系统安装Anaconda为例 其他的系统安装过程类似 可以照猫画虎 下面请看具体的安装过程 2 下载软件 1 首先去官网上进行下载软件 下载地址 https docs anaconda
  • 专访雅虎刷题狂人曹鹏:10年理论与实践结合的程序员之路

    采访联络员 SophyJ 作者 ly行云流水 所属机构 CSDN高校俱乐部 高校发布地址 http student csdn net mcd topic 163587 941331 摘要 在曹鹏博士的采访过程中 他最长提起的便是感恩 感谢良
  • Floyd算法的原理和实现代码

    原理 假设有向图G V E 采用邻接矩阵存储 设置一个二维数组A用于存放当前顶点之间的最短路径长度 分量A i j 表示当前顶点i gt j的最短路径长度 然后 每次添加一个顶点 同时对A的数组进行筛选优化 期间会产生k个A数组 Ak i
  • 第一个vue程序

    div message h2 school name school moblie h2 div
  • 程序、进程、线程联系以及进程和线程的区别和联系

    程序和进程的区别与联系 程序是一组有序的指令集合是一个静态的概念 一个程序由一组指令组成 以二进制方式存在存储器中 进程是程序及其数据在计算机上的一次运行活动 是一个动态的概念 进程的运行实体是程序 离开的程序的进程没有意义 进程是由程序
  • 交互原型设计工具

    1 axure RP 适合 快速创建应用软件或Web线框图 流程图 原型和规格说明文档 优点 支持交互设计 并可生成规格说明文档和输出HTML原型 Axure RP 集 UX 原型 规范和图表于一身 2 Sketch 适合 为视觉设计师打造
  • 图数据库——大数据时代的高铁

    作者 董小珊 姚臻 责编 仲培艺 zhongpy csdn net 本文为 程序员 原创文章 未经允许不得转载 更多精彩文章请订阅 程序员 如果把传统关系型数据库比做火车的话 那么到现在大数据时代 图数据库可比做高铁 它已成为NoSQL中关
  • IDEA鼠标右击new没有class和interface的解决办法

    IDEA点击new没有class和interface 问题如下图 解决办法 1 File gt Project Structure 如下图所示 2 选择Modules gt 右边Sources中选择所需目录 然后点击 Sources gt
  • 云平台的技术

    约束记录表 简朴 勤劳 谦虚 诚恳 禁止浪费 珍惜时间 虚心学习 纯心做人 1 0 1 1 节制 静默 条理 决断 不恋吃睡 开口有益 规整事务 坚持 迅捷 0 1 1 1 正直 中庸 整洁 宁静 贞洁 敬业负责 不倚势凌人 外表整洁 不纠
  • 【解决】windows安装pycrypto出错问题。error C2061: 语法错误: 标识符“intmax_t”

    1 执行命令报错 pip install pycrypto Installing collected packages pycrypto Running setup py install for pycrypto error ERROR C
  • easyUI Tree树动态刷新子节点

    tree tree url xxx 默认是post请求 checkbox false animate true lines true loadFilter function rows 返回要显示的过滤数据 返回数据时以标准树格式返回的 也就
  • MongodbTemplate 批量更新或者修改

    批量更新或者修改 public void saveOnlineStatusList List
  • 线性反馈移位寄存器 LFSR

    参考连接 添加链接描述 运算基础 模2运算 线性反馈移位寄存器用于产生可重复的伪随机序列PRBS 该电路由n级除法器和异或门组成 在k阶段 寄存器存在初值 Rn 1 R1 R0 称为seed 在k 1阶段 寄存器的值变为 k 1阶段 Rn
  • word2010或以上版本编号变成黑块的正确处理方

    打开编号显示为黑块的文档 把光标放置在黑块的后面 然后在键盘上按左方向键 则黑块变灰色 为选中状态 2 然后按下ctrl shift s 出现应用样式窗口点击 重新应用 黑块显示成正常的编号 3 然后点击 多级列表 按钮 选择 定义新的多级
  • 一次数据库的选型,FireBird胜出

    做了n多年的J2EE应用以后 如何做客户端的BI确实让我一下子摸不到门路 近期的一个客户要求我们给他做基于客户端的BI分析 客户是对外提供重要数据的单位 有很多的客户每年购买他的数据 可以说人家的数据库 每行每列都是钱 在这种情况下 他们非
  • css实现文字环绕图片布局

    前言 css实现文字环绕图片的效果 实现效果 实现代码 通过图片属性 align div style width 400px img src d303 paixin com thumbs 3548553 231637502 staff 10
  • 数据结构——AVL树

    目录 1 什么是AVL树 2 AVL树插入的模拟实现 节点定义 插入 旋转 右单旋 左单旋 双旋 右左旋 双旋 左右旋 完整的插入代码 3 AVL树的性能分析 1 什么是AVL树 AVL树是一种自平衡二叉查找树 也被称为高度平衡树 它具有以