数据结构:探秘AVL树

news2025/4/11 11:48:03

本节重点

  • 理解AVL树的概念
  • 掌握AVL树正确的插入方法
  • 利用_parent指针正确更新平衡因子
  • 掌握并理解四种旋转方式:左单旋,右单旋,左右双旋,右左双旋

一、AVL树的概念

AVL树得名于它的发明者G. M. Adelson-Velsky和E. M. Landis,他们在1962年的论文《An algorithm for the organization of information》中发表了它。

AVL树最先发明自平衡二叉搜索树,AVL是一颗空树,或者具备下列性质的二叉搜索树:它的左右子树都是AVL树,且左右子树的高度差的绝对值不超过1。AVL树是一颗高度平衡搜索二叉树,通过控制高度差去控制平衡。

AVL树的实现引入一个平衡因子的概念(balance factor)的概念,每个节点都有一个平衡因子,任何节点的平衡因子等于右子树高度减去左子树高度,也就是说任何节点的平衡因子等于0/1/-1,AVL树并不是必须要平衡因子,但是有了平衡因子可以更方便我们去进行观察和控制树是否平衡,就像一个风向标一样。

AVL树整体的节点数量和分布和完全二叉树类似,但是AVL树高度可以控制在logN,所以增删查改的效率也可以控制在O(logN),相比二叉搜索树有了本质的提升。

如图,每个节点上方的小数字表示该节点的平衡因子,平衡因子只能为-1/1/0当为2/-2时我们要通过旋转将左右子树重新达到平衡状态。 

二、AVL树的实现

2.1 AVL树的结构

AVL树我们分成两部分来实现,一部分是单个节点的定义,一部分是AVL树。并且用两个类进行封装:

template<class K>
struct AVLTNode//AVL树节点的定义
{
	K _key;
	struct AVLTNode<K>* _left;
	struct AVLTNode<K>* _right;

	struct AVLTNode<K>* _parent;//引入parent方便我们快速向上更新平衡因子

	int _bf;//平衡因子(balance factor)

	//构造函数:
	AVLTNode(K key)
		:_key(key)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_bf(0)
	{}
};


//AVL树的定义
template<class K>
class AVLTree
{
	typedef struct AVLTNode<K> Node;
public:
private:
	Node* root = nullptr;
};

2.2 AVL树的插入

AVL树插入的步骤:

  1. 插入一个值按照二叉搜索树的规则插入
  2. 新增节点后,只会影响祖先节点的高度,也就是可能会影响部分祖先节点的平衡因子,所以更新从新增节点->根节点路径上的平衡因子,实际最坏情况下更新到根,有些情况更新到中间就停止了。
  3. 更新平衡因子过程中没有出现问题,则插入结束
  4. 更新平衡因子的过程中出现不平衡,对不平衡子树旋转,旋转后降低了子树的高度,不会再影响上一层,所以插入结束

2.2.1 先按照搜索二叉树规则插入

在插入之前,我们需要判断该AVL树是否为空,若为空直接在_root 新增节点并返回,若不为空说明AVL树中已经存在节点,这时我们需要从根节点开始依次按照搜索树的规则寻找插入位置,找到之后创建新节点并链入到AVL树中。

代码示例:

bool Insert(const K& key)
	{
		if (_root == nullptr)
		{
			//AVL是一颗空树
			_root=new Node(key)
            return 1;
		}
		else
		{
			Node* cur = _root;
			Node* parent = nullptr;
			while (cur)
			{
				if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else
				{
					assert(false);
				}
			}
			//链接新节点:
			cur = new Node(key);
			if (cur->_key > parent->_key)
			{
				parent->_right = cur;
			}
			else
			{
				parent->_left = cur;
			}
			cur->_parent = parent;

		}

2.2.2 判断并更新平衡因子

更新规则:

平衡因子=右子树高度-左子树高度

插入节点会增加子树高度,影响当前节点平衡因子因为一次只能插入一个节点所以平衡因子要么++要么--:

插入在右子树平衡因子++;插入在左子树平衡因子--。

平衡因子的三种情况:(0,-1/1,-2/2)

1、更新后parent节点平衡因子为0

说明更新之前parent节点平衡因子为1或-1也就是左右子树一边高一边低,节点插入在低的一边,插入后左右平衡不会影响上一级节点的平衡因子。

2、更新后parent节点平衡因子为1/-1

说明更新之前parent节点平衡因子为0,插入后左右子树一边高一边低会影响上一级节点的平衡因子,所以要继续向上更新。

3、更新后parent节点平衡因子为2/-2

说明更新之前parent节点平衡因子为1/-1也就是左右子树一边高一边低,节点插入在高的一边

代码示例: 

while (parent)
{
	if (parent->_right == cur)
	{
		parent->_bf++;
	}
	else
	{
		parent->_bf--;
	}
	if (parent->_bf == 0)
	{
		//说明新增节点在低的一边,插入后左右子树平衡
		//不会影响祖先节点的 _bf直接break
		break;
	}
	else if(parent->_bf==1||parent->_bf==-1)
	{
		//新增节点之后为1或-1说明之前为0(左右子树平衡)
		//此时需要更新依次更新祖先节点的 _bf直到更新到根节点或某一祖先节点_bf==0
		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);
		}
		else
		{
			assert(false);
		}

		break;
	}
    else
    {
        assert(false);
    }
}

2.2.3 (不平衡)旋转子树

旋转的原则:

  • 保持搜索树的规则
  • 让旋转的树从不满足变为平衡,其次降低树的高度

首先我们需要明白的是旋转操作分为两部分,一是调整节点之间的链接关系,二是更新平衡因子     _bf

左单旋(RotateL)

当parent的平衡因子为2,且cur的平衡因子为1时AVL树会呈现右子树一边高的形式,这时我们需要进行左旋操作,需要注意的是满足 parent->_bf==2 && cur->_bf==1 条件的AVL树的形式可能有多种:

为了便于理解,我们可以选择一种简单的情况进行分析并写出相应代码:

代码示例:

void RotateL(Node* parent)
	{
		Node* SubR = parent->_right;
		Node* SubRL = SubR->_left;
		Node* pparent = parent->_parent;
		if (SubRL)
		{
			SubRL->_parent = parent;
		}
		parent->_right = SubRL;
		SubR->_left = parent;
		parent->_parent = SubR;
		if (parent == _root)
		{
			_root = SubR;
			SubR->_parent = nullptr;
		}
		else
		{
			if (pparent->_right == parent)
			{
				pparent->_right = SubR;
			}
			else
			{
				pparent->_left = SubR;
			}
			SubR->_parent = pparent;
		}
		//节点链接关系调整完成后更新平衡因子:
		SubR->_bf = 0;
		parent->_bf = 0;
	}
右单旋(RotateR)

类似的是,满足 parent->_bf==-2 && cur->_bf==-1 条件的AVL树的形式也可能有多种

我们也选择其中一种简单的情况进行分析和编写代码:

void RotateR(Node* parent)
	{
		Node* SubL = parent->_left;
		Node* SubLR = SubL->_right;
		Node* pparent = parent->_parent;
		if (SubLR)
		{
			SubLR->_parent = parent;
		}
		parent->_left = SubLR;
		SubL->_right = parent;
		parent->_parent = SubL;
		if (parent == _root)
		{
			_root = SubL;
			SubL->_parent = nullptr;
		}
		else
		{
			if (pparent->_right == parent)
			{
				pparent->_right = SubL;
			}
			else
			{
				pparent->_left = SubL;
			}
			SubR->_parent = pparent;
		}
		//节点链接关系调整完成后更新平衡因子:
		SubL->_bf = 0;
		parent->_bf = 0;
	}
左右双旋(RotateLR)

与单旋不同的是,双旋对应的AVL树的结构不再是单纯一边高,我们由条件判断很容易就可以看出来(parent->_bf == -2 && cur->_bf == 1 或 parent->_bf == 2 && cur->_bf == -1),此时我们发现AVL树节点类似“折线形”排列,这时单纯的单旋无法使二叉树再次平衡,我们需要进行两次单旋来解决。

类似的是满足双旋触发条件时,AVL树的结构要是拓展开来也有非常多种情况,我们可以选择其中一种较为简单的情况来分析并编写相应代码:

需要注意的是左右双旋的时候对SubLR的_bf也要进行考虑,目的是确定SubLR是否存在单个的子树,因为最终SubLR的子树会链入SubL或者parent影响两个节点的平衡因子

void RotateLR(Node* parent)
	{
		Node* SubL = parent->_left;
		Node* SubLR = SubL->_right;
		int bf = SubLR->_bf;
		RotateL(parent->_left);
		RotateR(parent);
		if (bf == 1)
		{
			SubLR->_bf = 0;
			parent->_bf = 0;
			SubL->_bf = -1;
		}
		else if (bf == 0)
		{
			parent->_bf = 0;
			SubLR->_bf = 0;
			SubL->_bf = 0;
		}
		else
		{
			SubLR->_bf = 0;
			parent->_bf = 1;
			SubL->_bf = 0;
		}
	}
右左双旋(RotateRL)

 

void RotateRL(Node* parent)
	{
		Node* SubR = parent->_right;
		Node* SubRL = SubR->_left;
		int bf = SubRL->_bf;
		RotateR(parent->_right);
		RotateL(parent);
		if (bf == 1)
		{
			SubRL->_bf = 0;
			parent->_bf = -1;
			SubR->_bf = 0;
		}
		else if (bf == 0)
		{
			parent->_bf = 0;
			SubLR->_bf = 0;
			SubL->_bf = 0;
		}
		else
		{
			SubRL->_bf = 0;
			parent->_bf = 0;
			SubR->_bf = 1;
		}
	}

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

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

相关文章

Linux 入门:基础开发工具(上)vim,gcc/g++,make/makefile

目录 一.软件包管理器 一&#xff09;.软件包 二&#xff09;.安装软件 三&#xff09;.删除软件 二.编辑器vim 一&#xff09;.vim的基本介绍 1.正常/普通/命令模式(Normal mode) 2.插入模式(Insert mode) 3.底行模式(last line mode) 二&#xff09;.vim的基本操作 …

5、无线通信基站的FPGA实现架构

基站&#xff08;Base Station&#xff0c;BS&#xff09;&#xff0c;也称为公用移动通信基站&#xff0c;是无线电台站的一种形式&#xff0c;具体则指在一定的无线电覆盖区中&#xff0c;通过移动通信交换中心&#xff0c;与移动电话终端之间的信息传递的无线电收发信电台。…

Linux2 CD LL hostnamectl type mkdir dudo

查看主机名信息 设置静态主机名 同时配置静态、瞬时主机名 下载Vmware tools https://blog.csdn.net/qq_34638161/article/details/102779721 mkdir创建目录 问题&#xff1a;为什么在root目录下 看不到 /var /usr那些文件夹

Docker容器部署Java项目的自动化脚本(Shell编写)

&#x1f91f;致敬读者 &#x1f7e9;感谢阅读&#x1f7e6;笑口常开&#x1f7ea;生日快乐⬛早点睡觉 &#x1f4d8;博主相关 &#x1f7e7;博主信息&#x1f7e8;博客首页&#x1f7eb;专栏推荐&#x1f7e5;活动信息 文章目录 Docker容器部署Java项目的自动化脚本&#x…

STM32F103C8T6单片机硬核原理篇:讨论GPIO的基本原理篇章1——只讨论我们的GPIO简单输入和输出

目录 前言 输出时的GPIO控制部分 标准库是如何操作寄存器完成GPIO驱动的初始化的&#xff1f; 问题1&#xff1a;如何掌握GPIO的编程细节——跟寄存器如何打交道 问题2&#xff1a;哪些寄存器&#xff0c;去哪里找呢&#xff1f; 问题三&#xff0c;寄存器的含义&#xff…

UniApp集成极光推送详细教程

最近项目要集成推送服务&#xff0c;选型极光推送&#xff0c;记录一下开发过程。 1、极光官网注册登录 1.1选择极光推送产品&#xff0c;新建应用 1.2在下一步中选择Android/IOS的消息推送服务 1.3产品设置中输入应用包名&#xff08;一经输入后不可更改&#xff0c;一定要正…

AI战略群与星际之门:软银AI投资版图计划深度解析

一、星际之门:万亿美元级 AI 基础设施革命 1.1 项目背景与战略定位 在 AI 技术迅猛发展的今天,算力已成为推动其前进的核心动力。软银联合 OpenAI、甲骨文、英伟达、微软、arm推出的 “星际之门”(Stargate)计划,无疑是 AI 领域的一颗重磅炸弹。作为 AI 领域史上最大单笔…

系统思考与时间管理

时间管理的真正秘诀&#xff1a;主动浪费时间&#xff1f; 巴菲特的私人飞机驾驶员觉得自己不够成功&#xff0c;于是向巴菲特请教应该怎么做。巴菲特让他列出了自己人生中最想实现的25个目标&#xff0c;并按重要程度排序&#xff0c;接着安排时间专注做前五件最重要的事情。…

mac air m系列arm架构芯片安装虚拟机 UTM+debian 浏览器firefox和chrome

成果展示&#xff1a;debian虚拟机&#xff0c;你值得拥有&#xff01; 预期结果 1、mac的m系列芯片&#xff0c;arm 架构且内存小&#xff0c;安装虚拟机。 考虑到mac m系列芯片8g内存&#xff0c;arm架构想安装一个轻量的虚拟机&#xff0c;偶然之间发现了debian&#xff0c…

Keepalived+LVS+nginx高可用架构

注明&#xff1a;所有软件已经下载好&#xff0c;防火墙和SELinux已经全部关闭 一.搭建NFS 1.服务端 1.创建文件 [rootnfs ~]# mkdir -p /nfs/data 2、修改权限 [rootnfs ~]# chmod orw /nfs/data 3、写配置文件 [rootnfs ~]# cat /etc/exports /nfs/data 192.168.111.118(r…

【力扣hot100题】(034)LRU缓存

做完这题已经没有任何力气写链表题了。 思路很简单&#xff0c;就是调试特别的痛苦。 老是频频报错&#xff0c;唉。 class LRUCache { public:struct ListNode{int key,val;ListNode* next; ListNode* prev;ListNode() : key(0), val(0), next(nullptr), prev(nullptr) {}L…

【redis】缓存 更新策略(定期、实时生存),缓存预热、穿透、雪崩、击穿详解

什么是缓存 redis 最常用的场景 核心思路就是把一些常用的数据&#xff0c;放到触手可及&#xff08;访问速度更快&#xff09;的地方 ⽐如我需要去⾼铁站坐⾼铁. 我们知道坐⾼铁是需要反复刷⾝份证的 (进⼊⾼铁站, 检票, 上⻋,乘⻋过程中, 出站…)正常来说, 我的⾝份证是放在…

使用STM32CubeMX和Keil在STM32上创建并运行一个简单的FreeRTOS多任务程序

目标 利用FreeRTOS运行两个任务&#xff0c;分别为点灯和OLED屏的显示。 利用STM32CubeMX生成Keil工程和相关初始化代码 知识回顾 之前已经利用STM32CubeMX生成过Keil工程和相关初始化代码了&#xff0c;可以去回顾一下&#xff0c;详情见&#xff1a;https://blog.csdn.ne…

从查重报告入手的精准论文降重秘籍

每个同学在使用论文查重时&#xff0c;为何同一篇文章&#xff0c;可能重复率从10%—30%不等&#xff1f;归根结底还是使用了不同查重系统。其实不同的论文查重与论文AIGC检测系统的算法、数据及模型都不一样&#xff0c;那如何针对这些系统的“个性”精准降重&#xff0c;这篇…

Uubuntu20.04复现SA-ConvONet步骤

项目地址&#xff1a; tangjiapeng/SA-ConvONet: ICCV2021 Oral SA-ConvONet: Sign-Agnostic Optimization of Convolutional Occupancy Networks 安装步骤&#xff1a; 一、系统更新 检查系统是否已经更新到最新版本&#xff1a; sudo apt-get update sudo apt-get upgra…

【Linux网络#18】:深入理解select多路转接:传统I/O复用的基石

&#x1f4c3;个人主页&#xff1a;island1314 &#x1f525;个人专栏&#xff1a;Linux—登神长阶 目录 一、前言&#xff1a;&#x1f525; I/O 多路转接 为什么需要I/O多路转接&#xff1f; 二、I/O 多路转接之 select 1. 初识 select2. select 函数原型2.1 关于 fd_set 结…

华院计算3项应用成果入选钢铁行业智能制造解决方案推荐目录(2024年)

近日&#xff0c;中国钢铁工业协会发布《钢铁行业智能制造解决方案推荐目录&#xff08;2024年&#xff09;》。由中国钢铁工业协会、钢铁行业智能制造联盟共同开展了2024年钢铁行业智能制造解决方案及数字化转型典型场景应用案例遴选、智能制造创新大赛&#xff08;钢铁行业赛…

python使用cookie、session、selenium实现网站登录(爬取信息)

一、使用cookie 这段代码演示了如何使用Python的urllib和http.cookiejar模块来实现网站的模拟登录&#xff0c;并在登录后访问需要认证的页面。 # 导入必要的库 import requests from urllib import request, parse# 1. 导入http.cookiejar模块中的CookieJar类&#xff0c;用…

vector模拟实现2

文章目录 vector的模拟实现erase函数resize拷贝构造赋值重载函数模版构造及其细节结语 我们今天又见面啦&#xff0c;给生活加点impetus&#xff01;&#xff01;开启今天的编程之路 今天我们来完善vector剩余的内容&#xff0c;以及再探迭代器失效&#xff01; 作者&#xff…

详解相机的内参和外参,以及内外参的标定方法

1 四个坐标系 要想深入搞清楚相机的内参和外参含义&#xff0c; 首先得清楚以下4个坐标系的定义&#xff1a; 世界坐标系&#xff1a; 名字看着很唬人&#xff0c; 其实没什么大不了的&#xff0c; 这个就是你自己定义的某一个坐标系。 比如&#xff0c; 你把房间的某一个点定…