【C++进阶】深度解析AVL树及其简单模拟实现

news2024/12/23 17:46:34

AVL树的解析和模拟实现

  • 一,什么是AVL树
  • 二,AVL树的特性
  • 三,模拟实现
    • 1. 基本框架
    • 2. 插入(不带旋转)
    • 2. AVL树的旋转
    • 3. AVL树的验证
  • 四,总结

一,什么是AVL树

之前我们学习了二叉搜索树,但是有时候因为节点插入的问题,可能会退化为单支树,这样会导致查找效率变得底下如顺序表。
因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:
当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度

这种树就是AVL树.

二,AVL树的特性

AVL树满足两个条件:

  1. 它的左右子树都是AVL树
  2. 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)

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

在这里插入图片描述
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 O ( l o g 2 n ) O(log_2 n) O(log2n),搜索时间复杂度O( l o g 2 n log_2 n log2n)

三,模拟实现

1. 基本框架

AVL树是一种平衡二叉树,其内部存储的是pair键值对,我们模拟实现的时候直接用pair来存储即可。
我们先来写AVL的节点定义:
每个节点都要有一个平衡因子用来保证其AVL树特性

template<class K,class V>
struct AVLTreeNode {
	typedef AVLTreeNode<K, V> Node;

	AVLTreeNode(const pair<K,V> &kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
		,_kv(kv)
	{}
	
	Node* _left;
	Node* _right;
	Node* _parent;//记录当前节点的父亲
	int _bf;//记录节点的平衡因子
	pair<K, V> _kv;//保存记录的key,val

};

然后我们来写AVL的框架:

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

};

2. 插入(不带旋转)

下面我们来实现AVL树的插入:
插入分为两步:

  1. 按照二叉搜索树那样插入节点
  2. 调整平衡因子

插入节点的部分和二叉搜索树的代码一样,主要是修改平衡因子的部分

当插入新节点后,这个新节点的双亲节点的平衡因子会发生变化:
1.新插入的节点cur在其双亲节点parent的左孩子时,parent的平衡因子-1
2. 新插入的节点在parent的右孩子时,parent的平衡因子+1

代码如下:

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 (kv.first > cur->_kv.first) {
				parent = cur;
				cur = cur->_right;
				
			}
			else if (kv.first < cur->_kv.first) {
				parent = cur;
				cur = cur->_left;
				
			}
			else {
				return false;
			}
		}
		//找到了插入位置
		cur = new Node(kv);
		if (kv.first > parent->_kv.first) {
			parent->_right = cur;
		}
		else {
			parent->_left = cur;
		}
		cur->_parent = parent;

		//修改平衡因子
		while (parent) {
			if (cur == parent->_left) {//如果加在左边,则父节点的平衡因子--
				parent->_bf--;
			}
			else {
				parent->_bf++;//右边则++
			}
			//调整平衡因子
			//...

		}
		return true;
}

修改后这里有三种情况:

  1. 如果pParent的平衡因子为0,说明插入之前pParent的平衡因子为正负1,插入后被调整成0,此时满足AVL树的性质,插入成功
  2. 如果pParent的平衡因子为正负1,说明插入前pParent的平衡因子一定为0,插入后被更新成正负1,此时以pParent为根的树的高度增加,需要继续向上更新
  3. 如果pParent的平衡因子为正负2,则pParent的平衡因子违反平衡树的性质,需要对其进行旋转处理

我们先来说前两种情况,第三种我们在旋转中讲解


第一种情况:
修改后parent的平衡因子为0,这里就不用再进行调整了
在这里插入图片描述

第二种情况:
修改后平衡因子为1,则以parent为根的这颗树的高度发生了变化,则要继续向上调整平衡因子
在这里插入图片描述
代码:

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 (kv.first > cur->_kv.first) {
				parent = cur;
				cur = cur->_right;
				
			}
			else if (kv.first < cur->_kv.first) {
				parent = cur;
				cur = cur->_left;
				
			}
			else {
				return false;
			}
		}
		//找到了插入位置
		cur = new Node(kv);
		if (kv.first > parent->_kv.first) {
			parent->_right = cur;
		}
		else {
			parent->_left = 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 = cur->_parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2) {
				//旋转
				
			}
			else {
				assert(false);//说明插入之前就有问题
			}
		}
		return true;

	}

2. AVL树的旋转

上面的两种情况,更新parent的平衡因子后AVL树的特性还保持着,但是第三种情况更新后,双亲的平衡因子为2/-2,破坏了平衡二叉搜索树的特性,所以就要进行以parent为根的树的旋转

AVL树的旋转也分为四种情况

1. 新节点插入较高左子树的左侧:右单旋
2. 新节点插入较高右子树的右侧:左单旋
3. 新节点插入较高左子树的右侧—左右双旋:先左单旋再右单旋
4. 新节点插入较高右子树的左侧—右左双旋:先右单旋再左单旋

下面我们来依次解释:


右单旋

右单旋是新插入的节点在左子树中,使其整棵树右高左低,所以要旋转右子树来降低高度差使这棵树变得平衡

具体过程看下图:
在这里插入图片描述
这里我们来看一下具体当各子树高度的情况:

在这里插入图片描述

下面我们来编写一下右单璇的代码:
首先我们定义两个节点用来标记当前需要旋转的节点。
在这里插入图片描述

对于右单旋,我们找当前parent的左孩子 subL 和该左孩子的右孩子 subLR

然后我们在旋转时还要注意一下:
(1) 30这个节点的右子树可能存在也可能不存在
不存在时我们就不能直接将subL的parent指针直接指向60

void RotateR(Node* parent) {//右单旋
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		
		parent->_left = subLR;
		if (subLR) {//当subLR存在时,subLR的parent才指向parent
			subLR->_parent = parent;
		}
		//...
}

(2) 60这个节点可能是根也可能不为根。
不为根时,我们还需要一个 ppnode 节点来标记parent的双亲节点,用来将新的’根’节点去链接到原来的树上。

void RotateR(Node* parent) {//右单旋
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		
		parent->_left = subLR;
		if (subLR) {//当subLR存在时,subLR的parent才指向parent
			subLR->_parent = parent;
		}
		if (parent == _root) {//如果p是根,则subL更新为根
			_root = subL;
			subL->_parent = nullptr;
		}
		else {//将旋转后的子树的根节点链接到原来的树上
			if (ppnode->_left == parent) {
				ppnode->_left = subL;
			}
			else {
				ppnode->_right = subL;
			}
			subL->_parent = ppnode;
		}
		//...
}

最后一步就是修改平衡因子,旋转后的节点不用再调整,因为旋转后子树的两端高度都相等,达到平衡。

void RotateR(Node* parent) {//右单旋
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	parent->_left = subLR;
	if (subLR) {//当subLR存在时,subLR的parent才指向parent
		subLR->_parent = parent;
	}

	subL->_right = parent;
	Node* ppnode = parent->_parent;//保存p的parent指向
	parent->_parent = subL;

	if (parent == _root) {//如果p是根,则subL更新为根
		_root = subL;
		subL->_parent = nullptr;
	}
	else {//将旋转后的子树的根节点链接到原来的树上
		if (ppnode->_left == parent) {
			ppnode->_left = subL;
		}
		else {
			ppnode->_right = subL;
		}
		subL->_parent = ppnode;
	}
	//更新节点的平衡因子
	parent->_bf = 0;
	subL->_bf = 0;
}

现在来看左单旋

左单旋和右单旋相反,左边高右边低,所以进行左单旋来降低高度差

在这里插入图片描述
这里的情况都和右单旋转相反,我们直接上代码:

void RotateL(Node* parent) {//左单旋
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	parent->_right = subRL;
	if (subRL) {//当subRL存在时,subRL的parent才指向parent
		subRL->_parent = parent;
	}

	subR->_left = parent;
	Node* ppnode = parent->_parent;//保存p的parent指向
	parent->_parent = subR;

	if (parent == _root) {//如果p是根,则subR更新为根
		_root = subR;
		subR->_parent = nullptr;
	}
	else {//将旋转后的子树的根节点链接到原来的树上
		if (ppnode->_left == parent) {
			ppnode->_left = subR;
		}
		else {
			ppnode->_right = subR;
		}
		subR->_parent = ppnode;
	}
	//更新节点的平衡因子
	parent->_bf = 0;
	subR->_bf = 0;
}

我们现在来看左右双旋,先左单旋再右单旋

这里的左右双旋其实就是相当右单旋的那种场景下,将b子树拆分成两颗子树,再将新增节点加在拆分后的子树上
在这里插入图片描述
我们以加在左子树为例讲解:
对新增节点后的树进行旋转,先以subL为根进行左旋,

在这里插入图片描述

再对parent为根进行右旋
在这里插入图片描述

加在右子树的过程和这个相反,希望大家可以自己去推导…

知道了大概的过程,我们现在来写代码:
和单旋一样,我们定义变量来协助我们旋转

void RotateRL(Node* parent) {//双旋(先左单旋,再右单旋)
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		//..
}

这里需要注意:新增节点所加的位置有三种情况
(1) 加在拆分后的左子树上
(2) 加在拆分后的右子树上
(3) 上图的60这个位置本身是空的
在这里插入图片描述
那么我们如何区分这三种情况呢,
我们可以用subLR的平衡因子来区分,subLR的因子为-1时,说明左高,则加在了左子树上,为1时,说明右高,则加在了右子树,为0时说明这个位置原来是空的。

void RotateLR(Node* parent) {
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	int bf = subLR->_bf;
	RotateL(subL);
	RotateR(parent);

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

右左双旋
右左双旋和左右双旋相反,各位老铁可以参考左右双旋来自己去画出图来理解

在这里插入图片描述

代码:

void RotateRL(Node* parent) {//双旋(先右单旋,再左单旋)
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	int bf = subRL->_bf;//以subRL的因子为标准判断所加的子树的位置
	RotateR(subR);
	RotateL(parent);

	//修改平衡因子
	//增加节点后有三种情况
	subRL->_bf = 0;
	if (bf == -1) {//加在左子树上
		parent->_bf = 0;
		subR->_bf = 1;
	}
	else if (bf == 1) {
		parent->_bf = -1;
		subR->_bf = 0;
	}
	else if (bf == 0) {
		parent->_bf = 0;
		subR->_bf = 0;
	}
	else {
		assert(false);
	}
}

3. AVL树的验证

AVL的插入讲完了,我们来看看如何证明咋们模拟实现的就是AVL树
一棵树如果是AVL树,那么首先它是一个二叉搜索树,其次他的每个子树的高度差不能超过1

这里我们做了一点小优化,我们在判断时先传入高度,判断高度差时类似于后序遍历的方式,从下往上去计算高度差

代码如下:

bool IsBalance() {
	int height = 0;
	return _IsBalance(_root, height);
}


bool _IsBalance(Node* root,int &height) {
	if (root == nullptr) {
		height = 0;
		return true;
	}
	int leftHeight = 0, rightHeight = 0;
	if (!_IsBalance(root->_left, leftHeight) || !_IsBalance(root->_right, rightHeight)) {
		return false;
	}
	if (abs(rightHeight - leftHeight) >= 2) {
		cout <<root->_kv.first<< "不平衡" << endl;
		return false;
	}
	if (rightHeight - leftHeight != root->_bf) {
		cout << root->_kv.first << "平衡因子异常" << endl;
		return false;
	}
	height = leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
	return true;

}

四,总结

我们今天终于讲完了AVL树,我们的C++部分也开始上了难度,希望大家可以跟上我们的讲解,下一节我们要来开始手撕红黑树!!!
在此之前我们要熟悉前面的二叉搜索树和AVL树的内容,希望大家都能学好C++。

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

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

相关文章

【每日力扣】40.组合总和II与701. 二叉搜索树中的插入操作

&#x1f525; 个人主页: 黑洞晓威 &#x1f600;你不必等到非常厉害&#xff0c;才敢开始&#xff0c;你需要开始&#xff0c;才会变的非常厉害。 40.组合总和II 给定一个候选人编号的集合 candidates 和一个目标数 target &#xff0c;找出 candidates 中所有可以使数字和为…

【小白学机器学习8】统计里的自由度DF=degree of freedom, 以及关于df=n-k, df=n-k-1, df=n-1 等自由度公式

目录 1 自由度 /degree of freedom / df 1.1 物理学的自由度 1.2 数学里的自由度 1.2.1 数学里的自由度 1.2.2 用线性代数来理解自由度&#xff08;需要补充&#xff09; 1.2.3 统计里的自由度 1.3 统计学里自由度的定义 2 不同对象的自由度 2.1 纯公式的自由度&#…

报Invalid value type for attribute ‘factoryBeanObjectType‘: java.lang.String错误

在springboot中使用Mybatis出现Invalid value type for attribute factoryBeanObjectType: java.lang.String 1、没有使用mybatis 检查pom文件里面的mybatis 可能是缺少这个依赖&#xff0c;或者版本过低 重新导入依赖 <dependency><groupId>org.mybatis.spri…

华为数通方向HCIP-DataCom H12-821题库(多选题:141-160)

第141题 以下关于802.1X认证的触发机制,描述正确的有? A、802.1X认证不能由认证设备(如802.1交换机)发起 B、802.1X客户端可以组播或广播方式触发认证 C、认证设备可以以组播或单播方式触发认证 D、802.1X认证只能由客户端主动发起 【参考答案】BC 【答案解析】 第142题 以…

集合系列(二) -List接口详解

一、List简介 List 的数据结构就是一个序列&#xff0c;存储内容时直接在内存中开辟一块连续的空间&#xff0c;然后将空间地址与索引对应。 以下是List集合简易架构图 由图中的继承关系&#xff0c;可以知道&#xff0c;ArrayList、LinkedList、Vector、Stack都是List的四个…

B3620 x 进制转 10 进制(详解)

题目 思路 八进制数567怎么转化为十进制数。首先八进制就是逢八进一&#xff0c;也就是说这里面最大的数也就7&#xff0c;没有≥8的数。下面我们就讲一下567怎么转化为十进制&#xff1a;首先7是个位&#xff0c;可以直接写成十进制的7&#xff0c;6是十位&#xff0c;它是通…

springboot基于java的畅销图书推荐系统

摘 要 二十一世纪我们的社会进入了信息时代&#xff0c;信息管理系统的建立&#xff0c;大大提高了人们信息化水平。传统的管理方式对时间、地点的限制太多&#xff0c;而在线管理系统刚好能满足这些需求&#xff0c;在线管理系统突破了传统管理方式的局限性。于是本文针对这一…

AI_寻路系统_修改寻路网格体

学习笔记&#xff0c;仅供参考&#xff01; 一、完成创建关卡和AI代理的初步步骤&#xff0c;以演示可以修改导航系统的不同方法。 创建简单关卡&#xff0c;并通过在关卡中放入导航网格体边界体积Actor来添加导航。 将ThirdPersonCharacter蓝图修改为使用导航系统在关卡中四…

vuepress-theme-vdoing博客搭建教程

搭建流程 前言 这是笔者搭建个人博客所经历的流程&#xff0c;特附上笔记 笔者个人博客地址&#xff1a;沉梦听雨的编程指南 一、主题介绍 本博客使用的主题为&#xff1a;vuepress-theme-vdoing&#xff0c;相关介绍和使用方法可以参考该主题的官方文档 官方文档快速上手…

力扣趣味题:找不同

经典面向样例编程 char findTheDifference(char* s, char* t) {if(sNULL){return t[0];}for(int x0;x<strlen(s);x){for(int y0;y<strlen(t);y){if(s[x]t[y]){t[y]1;break;}}}for(int x0;x<strlen(t);x){if(t[x]!1){return t[x];}}return NULL; }

银发经济@315:消费、陷阱与孤独的老人

【潮汐商业评论/文】 又是一年315。 这一天&#xff0c;从品牌到消费者&#xff0c;从线下到网络&#xff0c;都不约而同地将目光锁定在大众消费生活和与其相伴的消费“陷阱”上。 这其中&#xff0c;作为“有闲又有钱”且与社会经济发展速度相对有一定“代沟”的老年消费者群…

新加坡大带宽服务器托管优势

在数字化快速发展的今天&#xff0c;服务器托管成为企业拓展业务、提高服务质量的关键环节。而新加坡作为一个国际性的金融、贸易和科技创新中心&#xff0c;其大带宽服务器托管服务在全球范围内享有盛誉。本文将为您科普新加坡大带宽服务器托管的诸多优势。 首先&#xff0c;新…

AXI CANFD MicroBlaze 测试笔记

文章目录 前言测试用的硬件连接Vivado 配置Vitis MicroBlaze CANFD 代码测试代码测试截图Github Link 前言 官网: CAN with Flexible Data Rate (CAN FD) (xilinx.com) 特征: 支持8Mb/s的CANFD多达 3 个数据位发送器延迟补偿(TDC, transmitter delay compensation)32-deep T…

VS Code上,QT基于cmake,qmake的构建方法(非常详细)

VS Code上,QT基于cmake&#xff0c;qmake的构建方法 1 前言2 QT基于cmake的构建方法2.1 VS Code关键插件安装2.2 系统环境变量配置2.3 VS Code中&#xff0c;环境变量配置2.4 Cmake新建一个新的Porject 3 QT基于qmake的构建方法 1 前言 最近&#xff0c;由于认证了github的学生…

RabbitMQ学习总结-延迟消息

1.死信交换机 一致不被消费的信息/过期的信息/被标记nack/reject的信息&#xff0c;这些消息都可以进入死信交换机&#xff0c;但是首先要配置的有私信交换机。私信交换机可以再RabbitMQ的客户端上选定配置-dead-letter-exchange。 2.延迟消息 像我们买车票&#xff0c;外卖…

PHP 生成图片

1.先确认是否有GD库 echo phpinfo(); // 创建一个真彩色图像 $image imagecreatetruecolor(120, 50);// 分配颜色 $bgColor imagecolorallocate($image, 255, 255, 255); // 白色背景 $textColor imagecolorallocate($image, 230, 230, 230); // 黑色文字// 填充背景 image…

MyFileServer

靶场下载地址 https://download.vulnhub.com/myfileserver/My_file_server_1.ova 信息收集 # nmap -sn 192.168.56.0/24 -oN live.nmap Starting Nmap 7.94 ( https://nmap.org ) at 2024-02-24 22:07 CST Nmap scan report for 192.168.56.2 (192.168.56.2) Host is up (0.…

Java学习笔记(13)

阶段项目 拼图小游戏 JFrame JMenuBar JMenu JMenuItem 用add方法添加到不同的对象中 添加图片 先创建一个图片ImageIcon的对象&#xff0c;写入图片的路径 再创建JLabel管理容器对象&#xff0c;把图片放到这个容器中&#xff0c;再把容器添加到界面 界面坐标位置 改变图…

nmcli --help(nmcli -h)nmcli文档、nmcli手册

文章目录 nmcli --helpOPTION解释OBJECT解释1. g[eneral]&#xff1a;查看NetworkManager的状态2. n[etworking]&#xff1a;启用或禁用网络3. r[adio]&#xff1a;查看无线电状态&#xff08;例如&#xff0c;Wi-Fi&#xff09;4. c[onnection]&#xff1a;列出所有的网络连接…

openwrt下部署clouddrive2

在启动项上增加启动参数 在exit 0前面增加 mount --make-shared /mnt/data480g注意&#xff0c;后面的/mnt/data480g要替换成你设置的共享映射券。 拉取镜像 docker pull cloudnas/clouddrive2启动镜像 一定要用ssh在后台用docker run命令启动&#xff0c;因为openwrt前台…