【搜索结构】AVL树的学习与实现

news2024/11/17 14:08:35

目录

什么是AVL树

AVL树的定义

插入函数的实现

左单旋和右单旋

左右双旋与右左双旋


什么是AVL树

        AVL树实际上就是二叉搜索树的一种变体,我们都知道二i叉搜索树可以将查找的时间复杂度提升到O(logn),极大提升搜索效率。但是在极端情况下,当按顺序向树插入节点时,二叉树严重不平衡,相当于退化成了链表,此时查找的时间复杂度就变为了O(n),这并不是我们希望看到的。

        那么有没有什么方式可以让二叉搜索树保持一定的平衡性从而不至于导致查找效率严重降低呢?AVL树也就是高度平衡二叉树给出的解决方案是:

1. 二叉树的每个节点都有一个平衡因子,平衡因子等于左子树高度减右子树高度的值

2. 平衡因子的绝对值不能超过1

3. 当插入或删除节点导致平衡因子绝对值超过1时,进行旋转

AVL树的定义

        让我们来思考一下,要实现前面描述的功能,AVL树的单个节点应该有哪些成员变量呢?

1. 首先肯定要有左右子树的节点

2. 然后为了旋转时能够找到父亲,我们还需要存父亲节点

3. 为了确保平衡,我们要将左右高度差作为平衡因子保存

4. 最后还有搜索要用到的键值对

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):
		_kv(kv), 
		left(nullptr),
		right(nullptr),
		parent(nullptr),
		_bf(0)
	{}
};

那么,AVL树的定义就应该是:

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

插入函数的实现

        让我们先明确一下AVL树插入一个节点要做的事:

1. 按照二叉搜索树的规则找到插入位置进行插入

2. 根据左右高度差得到平衡因子

3. 当平衡因子绝对值大于1时进行旋转处理

        首先二叉树搜索规则就是:当插入节点的键小于当前节点的键时,和当前节点的左子树进行比较;当插入节点的键大于当前节点的键时,和当前节点的右子树进行比较;否则说明插入节点的键已经存在,这不符合二叉搜索树的规则,直接报错。我们按照这个规则找到可以插入的位置然后将新节点插入。

        插入完成之后,我们开始更新平衡因子,当新节点位于父亲的左边时,bf减1;当位于父亲的右边时,bf加1。修改完父亲的平衡因子后,进行判断:

1. 如果当前父亲平衡因子值为0,说明高度差没有改变,不需要进行处理,直接break即可。

2. 如果当前父亲平衡因子值为1/-1,说明插入导致高度差改变了,这可能会导致祖先节点的平衡因子绝对值超过1,所以需要继续往上更新祖先的平衡因子

3. 如果更新后的祖先的平衡因子绝对值超过1,就需要进行旋转处理

为了更直观地理解这个过程,让我们来看一个简单的例子:

插入新节点导致祖先失去平衡:

通过旋转,使AVL树恢复平衡

我们可以看到,这棵树的根节点平衡因子在插入新节点后,由1变为2,而进行一次左旋之后,平衡因子更新为0,恢复了平衡。

由于旋转涉及到的情况比较多且有一些细节操作,而这只是其中最简单的一种情况,所以我们先写出AVL树插入的基本框架,之后再对各个需要旋转的情况分别进行处理。

	bool Insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}
		Node* parent = nullptr; // 用于记录插入位置的父亲节点
		Node* cur = _root; // 用于比较查找到插入位置
		while (cur)
		{
			parent = cur;
			if (kv.first < cur->_kv.first)
			{
				cur = cur->left;
			}
			else if (kv.first > cur->_kv.first)
			{
				cur = cur->right;
			}
			// 搜索二叉树不支持重复key值的情况
			else
			{
				return false;
			}
		}
		// 此时说明已经找到了插入位置,插入新节点
		cur = new Node(kv);
		if (kv.first < parent->_kv.first)
		{
			parent->left = cur;
		}
		else
		{
			parent->right = cur;
		}
		cur->parent = parent; // 记得保持三叉链结构

		// 更新平衡因子
		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)
			{
                // 旋转处理……
			}
			// 说明旋转有问题,不能在|bf|==2时恢复平衡
			else
			{
				assert(false);
			}
		}
		return true;
	}

左单旋和右单旋

        先来看左单旋,如下图所示,是左单旋最简单的情况:

但显然需要左单旋的情况通常会更复杂些,所以我们实现左单旋时,需要考虑具有通用性的情况:

        可以发现,我们的左单旋操作似乎只需要让parent的右指向subRL,然后让subR的左指向parent。但大家可别忘了,我们定义AVL树节点时,为了方便向上更新,设计的是三叉链结构,所以还需要更新subRL和parent的父亲节点。除此之外,还需要考虑到parent可能是祖先节点的孩子,所以如果parent是AVL树的根节点:将根节点更新为subR,并将subR的父亲更新为nullptr;如果parent是祖先节点的孩子:则将祖先节点的孩子更新为subR,并将subR的父亲更新为父亲节点。

        指针朝向修改完后,我们还需要修改发生高度变化的节点的平衡因子,如上图所示,parent的平衡因子由2变为0,subR的平衡因子由1变为0。

void RotateL(Node* parent)
{
		// 在修改之前先保存祖父节点
		Node* grandpa = parent->parent;

		// 事实上,对于左旋这一情况,我们要修改的只有parent,subR,subRL最多再加个grandpa的指针朝向
		Node* subR = parent->right;
		Node* subRL = subR->left;

		// 进行左旋操作
		parent->right = subRL;
		subR->left = parent;

		// 之后还得把父指针也一起修改了
		parent->parent = subR;
		if(subRL)
			subRL->parent = parent;

		// 这棵树不是子树
		if (parent == _root)
		{
			_root = subR;
			_root->parent = nullptr;
		}
		// 这棵树是子树
		else
		{
			// 是祖父节点的左子树
			if (grandpa->left == parent)
			{
				grandpa->left = subR;
			}
			// 是祖父节点的右子树
			else
			{
				grandpa->right = subR;
			}
			subR->parent = grandpa;
		}
		parent->_bf = subR->_bf = 0;
	}

        接下来是右单旋,在局部子树左偏时,我们通过右旋来进行处理:

由于在左单旋部分我们已经详细讲解过了,右单旋其实就相当于反过来,所以就不再讲解一遍了。

	void RotateR(Node* parent)
	{
		// 在修改之前先保存祖父节点
		Node* grandpa = parent->parent;

		// 事实上,对于右旋这一情况,我们要修改的只有parent,subL,subLR最多再加个grandpa的指针朝向
		Node* subL = parent->left;
		Node* subLR = subL->right;

		// 进行右旋操作
		parent->left = subLR;
		subL->right = parent;

		// 之后还得把父指针也一起修改了
		parent->parent = subL;
		if (subLR)
			subLR->parent = parent;

		// 需要考虑现在调整的这棵树是子树的情况
		if (parent == _root)
		{
			_root = subL;
			_root->parent = nullptr;
		}
		else
		{
			// 是祖父节点的左子树
			if (grandpa->left == parent)
			{
				grandpa->left = subL;
			}
			// 是祖父节点的右子树
			else
			{
				grandpa->right = subL;
			}
			subL->parent = grandpa;
		}
		parent->_bf = subL->_bf = 0;
}

左右双旋与右左双旋

        我们还是先来看一个左右双旋的简单例子,可以看到,我们先通过一次左旋,把这棵子树修改为了单纯的左偏,而处理左偏,我们只需要进行一次右旋即可。

再来看更普遍的情况:

事实上,由于我们已经有了左旋和右旋的代码,所以进行双旋时,可以直接复用左旋和右旋函数,所以双旋主要考虑的是如何更新平衡因子。

一共有三种情况:

第一种:60就是新增节点,此时平衡因子全部更新为0

第二种:新增节点向b插入,此时parent的因子为1,其余因子为0

第三种:新增节点向c插入,此时subL的因子为-1,其余因子为0

大家可以自己分别画一下这三种情况,其实自己动手画一下就很好理解了,让我们看代码:

void RotateLR(Node* parent)
	{
		Node* subL = parent->left;
		Node* subLR = subL->right;
		// 啊啊啊,原来是这里错了,调试了一个晚上。。
		// int bf = parent->_bf;
		int bf = subLR->_bf;

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

		if (bf == 0)
		{
			parent->_bf = subL->_bf = subLR->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 1;
			subLR->_bf = 0;
			subL->_bf = 0;
		}
		else if (bf == 1)
		{
			parent->_bf = 0;
			subLR->_bf = 0;
			subL->_bf = -1;
		}
		//else
		//{
		//	assert(false);
		//}
	}

右左双旋和左右双旋完全类似,也是三种情况,其实理解了左右双旋,右左双旋就很好写了!

	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 = subR->_bf = subRL->_bf = 0;
		}
		else if (bf == -1)
		{
			// 新增节点位于subRL的左子树
			parent->_bf = 0;
			subRL->_bf = 0;
			subR->_bf = 1;
		}
		else if (bf == 1)
		{
			// 新增节点位于subRL的右子树
			parent->_bf = -1;
			subRL->_bf = 0;
			subR->_bf = 0;
		}
		//else
		//{
		//	assert(false);
		//}
	}

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

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

相关文章

IPTV智慧云桌面,后台服务器搭建笔记

环境CentOs7.9 &#xff0c;安装宝塔yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh 访问宝塔&#xff0c;修改服务器端口安全组端口 26029 注意&#xff01;&#xff01;&#xff01;&#xff01…

IDEA leetcode插件代码模板配置,登录闪退解决

前言 最近换电脑&#xff0c;配置idea时和原来的模板格式不一样有点难受&#xff0c;记录一下自己用的模板&#xff0c;后期换电脑使用&#xff0c;大家也可以使用&#xff0c;有更好的地方可以分享给我~ IDEA leetcode插件代码模板配置,登录闪退解决 前言1 下载IDEA leetcode…

Django基础用法+Demo演示

Django快速上手 参考: Django快速上手 再写几个页面 编辑demo1/urls.py, 添加URL和视图函数映射 urlpatterns [path(index/, views.index),path(user/list/, views.user_list),path(user/add/, views.user_add), ]编辑app01/views.py&#xff0c;添加几个函数 from djang…

蓝桥杯-洛谷刷题-day3(C++)

目录 1.忽略回车的字符串输入 i.getline() ii.逐个字符的识别再输入 2.获取绝对值abs() 3.做题时的误区 4.多个变量的某一个到达判断条件 i.max() 5.[NOIP2016 提高组] 玩具谜题 i.代码 6.逻辑上的圆圈 i.有限个数n的数组 7.数组的定义 i.动态数组 1.忽略回车的字符串输…

Redis在高性能缓存中的应用

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 Redis在高性能缓存中的应用 Redis在高性能缓存中的应用 Redis在高性能缓存中的应用 引言 Redis 概述 定义与原理 发展历程 Redi…

AOP实现上下游泳道隔离RPC调用

在平时代码项目代码测试的过程中&#xff0c;“隔离”思想就经常被用上&#xff0c;比方说多个并行开发的需求都需要用到服务 A 的能力&#xff0c;但是又需要同时部署 A 不同的代码分支&#xff0c;这个时候“泳道隔离”机制就显得尤为重要了。“泳道隔离”即将相同代码仓库的…

TCP/IP--Socket套接字--JAVA

一、概念 Socket套接字&#xff0c;是由系统提供⽤于⽹络通信的技术&#xff0c;是基于TCP/IP协议的⽹络通信的基本操作单元。 基于Socket套接字的⽹络程序开发就是⽹络编程。 二、分类 1.流套接字 使用传输层TCP协议。TCP协议特点&#xff1a;有链接、可靠传输、面向字节流…

号卡分销系统,号卡系统,物联网卡系统源码安装教程

号卡分销系统&#xff0c;号卡系统&#xff0c;物联网卡系统&#xff0c;&#xff0c;实现的高性能(PHP协程、PHP微服务)、高灵活性、前后端分离(后台)&#xff0c;PHP 持久化框架&#xff0c;助力管理系统敏捷开发&#xff0c;长期持续更新中。 主要特性 基于Auth验证的权限…

平衡二叉搜索树之 红黑 树的模拟实现【C++】

文章目录 红黑树的简单介绍定义红黑树的特性红黑树的应用 全部的实现代码放在了文章末尾准备工作包含头文件类的成员变量和红黑树节点的定义 构造函数和拷贝构造swap和赋值运算符重载析构函数findinsert【重要】第一步&#xff1a;按照二叉搜索树的方式插入新节点第二步&#x…

线性数据结构

数组 数组&#xff08;Array&#xff09; 是一种很常见的数据结构。它由相同类型的元素&#xff08;element&#xff09;组成&#xff0c;并且是使用一块连续的内存来存储。 我们直接可以利用元素的索引&#xff08;index&#xff09;可以计算出该元素对应的存储地址。 数组…

GoFly框架使用vue flow流程图组件说明

Vue Flow组件库是个高度可定制化的流程图组件&#xff0c;可用于工作流设计、流程图及图表编辑器、系统架构展示。可以根据自己的需求&#xff0c;设计独特的节点和边&#xff0c;实现个性化的流程图展示。这不仅增强了应用的视觉效果&#xff0c;也使得用户交互更为直观和流畅…

MySQL数据库:SQL语言入门 【2】(学习笔记)

目录 2&#xff0c;DML —— 数据操作语言&#xff08;Data Manipulation Language&#xff09; &#xff08;1&#xff09;insert 增加 数据 &#xff08;2&#xff09;delete 删除 数据 truncate 删除表和数据&#xff0c;再创建一个新表 &#xff08;3&#xf…

“南海明珠”-黄岩岛(民主礁)领海基线WebGIS绘制实战

目录 前言 一、关于岛屿的基点位置 1、领海基点 二、基点坐标的转换 1、最底层的左边转换 2、单个经纬度坐标点转换 3、完整的转换 三、基于天地图进行WebGIS展示 1、领海基点的可视化 2、重要城市距离计算 四、总结 前言 南海明珠黄岩岛&#xff0c;这座位于南海的…

19.UE5道具掉落

2-21 道具掉落&#xff0c;回血、回蓝、升级提升伤害_哔哩哔哩_bilibili 目录 1.道具的创建&#xff0c;道具功能的实现 2.随机掉落 1.道具的创建&#xff0c;道具功能的实现 新建Actor蓝图&#xff0c;并命名为道具总类&#xff0c;添加一个Niagara粒子组件和一个碰撞箱bo…

Cartographer激光雷达slam -20241116

Cartographer Cartographer代码结构 cartographer&#xff1a;负责处理来自雷达、IMU和里程计的数据并基于这些数据进行地图的构建&#xff0c;是cartographer理论的底层实现cartographer_ros&#xff1a;基于ros的通信机制获取传感器的数据并将它们转换成cartographer中定义…

node.js学习笔记-Window下MongoDB数据库安装(二)

一、介绍 MongoDB 是一个基于分布式文件存储的开源数据库系统&#xff0c;在当前的软件开发和数据存储领域中应用广泛&#xff0c;以下是对 MongoDB 的详细介绍&#xff1a; 文档型数据库&#xff1a;MongoDB 以 BSON&#xff08;Binary JSON&#xff09;格式存储数据&#x…

STM32G4的数模转换器(DAC)的应用

目录 概述 1 DAC模块介绍 2 STM32Cube配置参数 2.1 参数配置 2.2 项目架构 3 代码实现 3.1 接口函数 3.2 功能函数 3.3 波形源代码 4 DAC功能测试 4.1 测试方法介绍 4.2 波形测试 概述 本文主要介绍如何使用STM32G4的DAC模块功能&#xff0c;笔者使用STM32Cube工具…

【论文复现】轻松利用自适应特征融合实现去雾

&#x1f4dd;个人主页&#x1f339;&#xff1a;Eternity._ &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; ❀ 智慧医疗 介绍创新点网络结构特征提取阶段自适应融合阶段图像重建阶段上下文增强模块CEM特征融合模块AFM 结果分析 提示 论文题目&#xff1…

常用在汽车PKE无钥匙进入系统的高度集成SOC芯片:CSM2433

CSM2433是一款集成2.4GHz频段发射器、125KHz接收器和8位RISC&#xff08;精简指令集&#xff09;MCU的SOC芯片&#xff0c;用在汽车PKE无钥匙进入系统里。 什么是汽车PKE无钥匙进入系统&#xff1f; 无钥匙进入系统具有无钥匙进入并且启动的功能&#xff0c;英文名称是PKE&…

《TCP/IP网络编程》学习笔记 | Chapter 11:进程间通信

《TCP/IP网络编程》学习笔记 | Chapter 11&#xff1a;进程间通信 《TCP/IP网络编程》学习笔记 | Chapter 11&#xff1a;进程间通信进程间通信的基本概念通过管道实现进程间通信通过管道进行进程间双向通信 运用进程间通信习题&#xff08;1&#xff09;什么是进程间通信&…