【数据结构进阶】AVL树

news2025/2/22 11:03:35

在这里插入图片描述

🔥个人主页: Forcible Bug Maker
🔥专栏: C++ || 数据结构

目录

  • 🌈前言
  • 🔥AVL树的概念
  • 🔥AVL树的自实现
    • ==AVL树结点的定义==
    • ==AVL树需实现的函数接口==
    • ==AVL树的插入==
    • ==AVL树的旋转==
      • 右单旋
      • 左单旋
      • 左右双旋
      • 右左双旋
    • ==Insert==
    • ==Find==
    • ==拷贝构造==
    • ==Size==
    • ==析构函数==
    • ==二叉搜索树的验证==
  • 🌈结语

🌈前言

本篇博客主要内容:AVL树的介绍,以及其底层代码逻辑和实现

前面对map/multimap/set/multiset进行了简单的介绍,在其文档介绍中发现,这几个容器有个共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现
而我们今天所要说的AVL树就是平衡树的一种。

🔥AVL树的概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

一颗AVL树,是具有以下性质的二叉搜索树:

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

在这里插入图片描述
如果一个二叉搜索树树的高度平衡,那它就是AVL树。如果它有n个结点,其高度可以保持在O(log_2 N),搜索复杂度同样为O(log_2 N)

🔥AVL树的自实现

AVL树结点的定义

AVL树的结点包含四个成员变量,一个pair:存储Key和Value;三个指针:指向左孩子结点的指针,指向右孩子结点的指针,指向父结点的指针;最后还有一个_bf,存储当前结点平衡因子的值(反映以当前结点为根的树的平衡状态)。

template<class K, class V>
struct AVLTreeNode
{
	AVLTreeNode<K, V>(const std::pair<K, V>& kv)
		: _kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)
	{}
	std::pair<K, V> _kv;
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;
	int _bf; // balance factor
};

AVL树需实现的函数接口


	template<class K, class V>
	class AVLTree
	{
		typedef AVLTreeNode<K, V> Node;
	public:
		// 默认构造
		AVLTree() = default;

		// 拷贝构造
		AVLTree(const AVLTree<K, V>& t);
		
		// AVL树的插入
		bool Insert(const std::pair<K,V> kv);
	
		// AVL树的查找
		Node* Find(const K& key);

		// AVL树的析构
		~AVLTree();

		// AVL树的验证
		bool IsAVLTree();
		
		// 计算结点数
		size_t Size();

	private:
		Node* _root = nullptr;
	};
}

AVL树的插入

往AVL树中插入元素,首先按照二叉搜索树的插入方式进行结点的插入。再根据插入元素的位置调整平衡因子,如平衡因子的绝对值大于1(结点左右孩子高度差的绝对值大于1),则需要对树的平衡进行调整(这个调整的过程常被称为旋转)。

在已有的AVL树中插入结点,更新平衡因子遵循以下规律:

  • 插入的cur结点为其父节点parent的左孩子,parent结点的平衡因子-1
  • 插入的cur结点为其父节点parent的右孩子,parent结点的平衡因子+1

在父节点parent更新平衡因子之后,是否继续往祖先更新平衡因子,遵循以下规律:

  • parent平衡因子==0(平衡因子更新前为1/-1)
    parent所在子树高度不变,不需继续往上更新。
  • parent的平衡因子==1/-1(平衡因子更新前为0)
    parent所在子树高度变化,需要继续往上更新。
  • parent的平衡因子==2/-2(平衡因子更新前为1/-1)
    插入结点在高的一边,加剧了parent所在子树的不平衡,旋转处理

AVL树的旋转

旋转是为了将不平衡的二叉搜索树调整为平衡的二叉搜索树。
主要分四种旋转,两种单旋:右单旋左单旋;以及两种双旋:左右双旋右左双旋

右单旋

当将新结点插入到较高子树的侧时(左左:右单旋),进行右单旋。
在这里插入图片描述

// 右单旋
void RotateR(Node* parent)
{
	// subL可看作图中30,parent可看作图中60
	Node* subL = parent->_left;
	// subLR可看作图中b
	Node* subLR = subL->_right;
	// parentParent为parent的父节点
	Node* parentParent = parent->_parent;

	if (subLR)
		subLR->_parent = parent;
	subL->_right = parent;
	subL->_parent = parentParent;
	parent->_left = subLR;
	parent->_parent = subL;
	
	if (parentParent == nullptr) {
		_root = subL;
	}
	else {
		if (parentParent->_left == parent) {
			parentParent->_left = subL;
		}
		else
			parentParent->_right = subL;
	}
	
	// 单旋后记得调整平衡因子
	subL->_bf = 0;
	parent->_bf = 0;
}

左单旋

当将新结点插入到较高子树的侧时(右右:左单旋),进行左单旋。
在这里插入图片描述

// 左单旋
void RotateL(Node* parent)
{
	// subR可看作图中60,parent可看作图中30
	Node* subR = parent->_right;
	// subRL可看作图中b
	Node* subRL = subR->_left;
	// parentParent为parent的父结点
	Node* parentParent = parent->_parent;

	if (subRL)
		subRL->_parent = parent;
	subR->_left = parent;
	subR->_parent = parentParent;
	parent->_right = subRL;
	parent->_parent = subR;

	if (parentParent == nullptr) {
		_root = subR;
	}
	else {
		if (parentParent->_left == parent) {
			parentParent->_left = subR;
		}
		else
			parentParent->_right = subR;
	}
	
	// 单旋结束调整平衡因子
	subR->_bf = 0;
	parent->_bf = 0;
}

左右双旋

双旋中,主要考虑的因素是平衡因子的调整

当新结点插入较高左子树的右侧(左右:先左单旋再右单旋),进行左右双旋。
在这里插入图片描述

void RotateLR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	// 根据subLR的平衡因子
	// 可以知道结点插入的位置(其 左子树/右子树)
	// 从而便于调整subL和parent的平衡因子
	int bf = subLR->_bf;
	
	// 左右双旋
	RotateL(parent->_left);
	RotateR(parent);

	// 调整平衡因子
	if (bf == 0) {
		subL->_bf = 0;
		subLR->_bf = 0;
		parent->_bf = 0;
	}
	else if (bf == 1) {
		subL->_bf = -1;
		subLR->_bf = 0;
		parent->_bf = 0;
	}
	else if (bf == -1) {
		subL->_bf = 0;
		subLR->_bf = 0;
		parent->_bf = 1;
	}
	else assert(false);
}

右左双旋

双旋中,主要考虑的因素是平衡因子的调整

当新结点插入较高右子树的左侧(右左:先右单旋再左单旋),进行右左双旋。
在这里插入图片描述

void RotateRL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	// 根据subRL的平衡因子
	// 可以知道结点插入的位置(其 左子树/右子树)
	// 从而便于调整subR和parent的平衡因子
	int bf = subRL->_bf;

	// 右左双旋
	RotateR(parent->_right);
	RotateL(parent);

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

Insert

AVL树,插入实现代码:

bool Insert(const std::pair<K,V> kv)
{
	// 开始是二叉搜索树的插入
	if (_root == nullptr) {
		_root = new Node(kv);
		return true;
	}
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur) {
		if (cur->_kv.first < kv.first) {
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_kv.first > kv.first) {
			parent = cur;
			cur = cur->_left;
		}
		else return false;
	}
	if (parent->_kv.first < kv.first) {
		cur = new Node(kv);
		parent->_right = cur;
		cur->_parent = parent;
	}
	else {
		cur = new Node(kv);
		parent->_left = cur;
		cur->_parent = parent;
	}
	
	// 开始对平衡因子进行更新
	// 出现不平衡进行时旋转调整
	while (parent) {
		if (parent->_right == cur)
			parent->_bf++;
		else if (parent->_left == cur)
			parent->_bf--;
		if (parent->_bf == 0)
			break;
		else if (parent->_bf == 2) {
			if (parent->_right->_bf == 1) {
				RotateL(parent);
			}
			else if (parent->_right->_bf == -1) {
				RotateRL(parent);
			}
			break;
		}
		else if (parent->_bf == -2) {
			if (parent->_left->_bf == -1) {
				RotateR(parent);
			}
			else if (parent->_left->_bf == 1) {
				RotateLR(parent);
			}
			break;
		}
		cur = parent;
		parent = parent->_parent;
	}
	return true;
}

Find

AVL树的查找和二叉搜索树相同:

Node* Find(const K& key)
{
	Node* cur = _root;
	while (cur) {
		if (cur->_kv.first < key)
			cur = cur->_right;
		else if (cur->_kv.first > key)
			cur = cur->_left;
		else return cur;
	}
	return nullptr;
}

拷贝构造

与二叉搜索树相同。

AVLTree(const AVLTree<K, V>& t)
{
	_root = _Copy(t._root);
}

Node* _Copy(Node* root)
{
	if (root == nullptr)return nullptr;
	Node* newRoot = new Node(root->_kv);
	newRoot->_left = _Copy(root->_left);
	newRoot->_right = _Copy(root->_right);
	return newRoot;
}

Size

计算AVL树的结点个数。

size_t Size()
{
	return _Size(_root);
}

size_t _Size(Node* root)
{
	return root == nullptr ? 0 : _Size(root->_left) + _Size(root->_right) + 1;
}

析构函数

析构也是通过递归实现,释放结点。

~AVLTree()
{
	Destroy(_root);
	_root = nullptr;
}

void Destroy(Node* root)
{
	if (root == nullptr)return;
	Destroy(root->_left);
	Destroy(root->_right);
	delete root;
}

二叉搜索树的验证

AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:

  1. 验证其为二叉搜索树
    如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
  2. 验证其为平衡树
    • 每个节点子树高度差的绝对值不超过1
    • 节点的平衡因子是否计算正确(右树高度 - 左树高度 == 当前结点平衡因子)
// AVL树的验证
bool IsAVLTree()
{
	return _IsAVLTree(_root);
}

bool _IsAVLTree(Node* root)
{
	if (root == nullptr)
		return true;

	int left_bf = _Height(root->_left);
	int right_bf = _Height(root->_right);
	
	// 每个节点子树高度差的绝对值不超过1
	if (abs(left_bf - right_bf) >= 2) {
		return false;
	}
	// 节点的平衡因子是否计算正确
	if (right_bf - left_bf != root->_bf)
		return false;

	return _IsAVLTree(root->_left)
		&& _IsAVLTree(root->_right);
}

🌈结语

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log_2 (N)。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但如果要对其进行大量增删,则不建议使用。
本篇博客介绍了AVL树及其基本实现,感谢大家的支持♥

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

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

相关文章

【Docker虚拟机】在极空间上快速部署智能家居自动化平台『Home Assistant 』

【Docker&虚拟机】在极空间上快速部署智能家居自动化平台『Home Assistant 』 哈喽小伙伴们好&#xff0c;我是Stark-C~ 前段时间被粉丝问到怎么在极空间上部署Home Assistant&#xff0c;之前只是使用命令在威联通上部署过&#xff0c;所以最近正好有时间&#xff0c;在…

Ethernet

目录 1. Physical Layer(PHY)2. MAC2.1. MAC帧格式2.2. MAC地址与IP地址3. RGMII接口FPGA实现以太网(一)——以太网简介 以太网(Ethernet)是指遵守 IEEE 802.3 标准组成的局域网通信标准, IEEE 802.3 标准规定的主要是OSI参考模型中的物理层(PHY)和数据链路层中的介质访问控…

搭建DNS正向解析,反向解析+搭建DNS主从架构+搭建DNS多区域+时间同步

主要在局域网中配置&#xff0c;不存在外网 正向解析&#xff1a;域名解析为IP named.conf 解决权限 named.rfc1912.zones 解决解析方式 环境准备&#xff1a;三台机器都做下面的操作 基础配置&#xff1a;网络配置&#xff0c;关闭安全架构&#xff0c;关闭防火墙&#x…

Linux进程控制——进程等待

文章目录 进程等待进程等待的必要性进程等待的方法status参数option参数 进程等待 进程等待的过程其实是父进程等待子进程死亡的过程 进程等待的必要性 如果子进程退出&#xff0c;父进程不进行处理&#xff0c;子进程会变成僵尸进程&#xff0c;有内存泄漏的风险 僵尸进程…

docker容器cuda不可用,怎么解决?

通过Docker 构建的镜像中,启动之后,发现容器内部读取不到显卡驱动nvidia-smi 1、设置 NVIDIA Docker 存储库 distribution=$(. /etc/os-release;echo $ID$VERSION_ID) curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add

超逼真AI生成电影来了!《泰坦尼克号》AI重生!浙大阿里发布MovieDreamer,纯AI生成电影引爆热议!

视频生成领域的最新进展主要利用了短时内容的扩散模型。然而&#xff0c;这些方法往往无法对复杂的叙事进行建模&#xff0c;也无法在较长时间内保持角色的一致性&#xff0c;而这对于电影等长篇视频制作至关重要。 对此&#xff0c;浙大&阿里发布了一种新颖的分层框架Mov…

Kafka知识总结(选举机制+控制器+幂等性)

文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 选举机制 控制器&#xff08;Broker&#xff09;选举 控制器就是…

大模型算法面试题(十五)

本系列收纳各种大模型面试题及答案。 1、大模型LLM进行SFT如何对样本进行优化 大模型LLM&#xff08;Language Model&#xff0c;语言模型&#xff09;进行SFT&#xff08;Structured Fine-Tuning&#xff0c;结构化微调&#xff09;时&#xff0c;对样本的优化是提升模型性能…

从0开始搭建vue + flask 旅游景点数据分析系统(四):编写前端首页【数据驾驶舱】

本期我们编写数据驾驶舱页面(Dashboard)这个页面。主要任务是引入echarts 组件编写数据驾驶舱页面。 视频教程后续会更新在我的B站&#xff1a;https://space.bilibili.com/1583208775?spm_id_from666.25.0.0 推荐从教程第一集开始从零开始学习&#xff1a;https://blog.csdn…

PyCharm2024 专业版激活设置中文

PyCharm2024 专业版激活设置中文 官网下载最新版&#xff1a;https://www.jetbrains.com/zh-cn/pycharm/download 「hack-jet激活idea家族.zip」链接&#xff1a;https://pan.quark.cn/s/4929a884d8fe 激活步骤&#xff1a; 官网下载安装PyCharm &#xff1b;测试使用的202…

HDMI的等长要求到底是多少?

四对差分走线对内误差最好做到 5mil 范围之内&#xff0c;对与对的差分误差最好控制在 10mil 范围之内。同时&#xff0c;对与对之间的间距要求做到 15mil&#xff0c;空间准许的情况下尽量拉开&#xff0c;减小串扰。 作者&#xff1a;凡亿教育 https://www.bilibili.com/rea…

VulnHub:doubletrouble1

靶机下载地址 trouble1 信息收集 主机发现 攻击机ip&#xff1a;192.168.31.218&#xff0c;扫描攻击机同网段存活ip。 nmap 192.168.31.0/24 -Pn -T4 确认目标机ip&#xff1a;192.168.31.174 端口扫描 nmap 192.168.31.174 -A -p- -T4 开放了22,80端口。 目录扫描 访…

小白学大模型:LLaMA-Factory 介绍与使用

最近这一两周看到不少互联网公司都已经开始秋招提前批了。 不同以往的是&#xff0c;当前职场环境已不再是那个双向奔赴时代了。求职者在变多&#xff0c;HC 在变少&#xff0c;岗位要求还更高了。 最近&#xff0c;我们又陆续整理了很多大厂的面试题&#xff0c;帮助一些球友…

Linux:进程信号(二.信号的保存与处理、递达、volatile关键字、SIGCHLD信号)

上次介绍了&#xff1a;(Linux&#xff1a;进程信号&#xff08;一.认识信号、信号的产生及深层理解、Term与Core&#xff09;)[https://blog.csdn.net/qq_74415153/article/details/140624810] 文章目录 1.信号保存1.1递达、未决、阻塞等概念1.2再次理解信号产生与保存1.3信号…

ES6语法详解,面试必会,通俗易懂版

目录 Set的基本使用WeakSet 使用Set 和 WeakSet 区别内存泄漏示例&#xff1a;使用普通 Set 保存 DOM 节点如何避免这个内存泄漏MapWeakMap 的使用 Set的基本使用 在ES6之前&#xff0c;我们存储数据的结构主要有两种&#xff1a;数组、对象。 在ES6中新增了另外两种数据结构&a…

Pytorch深度学习快速入门(中)

Pytorch深度学习快速入门&#xff08;中&#xff09; 一、Containers&#xff08;神经网络的基本骨架&#xff09;&#xff08;一&#xff09;Module 的使用&#xff08;二&#xff09;Sequential 的使用<搭建小实战> 二、Convolution Layers&#xff08;卷积层&#xff…

加密货币赋能跨境电商:PayPal供应链金融服务如何引领行业新趋势

跨境电商行业近年来呈现出爆发式增长&#xff0c;随着全球化贸易壁垒的降低和数字经济的快速发展&#xff0c;越来越多的商家和消费者跨越国界进行交易。根据eMarketer的数据&#xff0c;全球跨境电商交易额在2023年已超过4万亿美元&#xff0c;并预计在未来几年内仍将保持两位…

Golang | Leetcode Golang题解之第301题删除无效的括号

题目&#xff1a; 题解&#xff1a; func checkValid(str string, lmask, rmask int, left, right []int) bool {cnt : 0pos1, pos2 : 0, 0for i : range str {if pos1 < len(left) && i left[pos1] {if lmask>>pos1&1 0 {cnt}pos1} else if pos2 <…

403 forbidden (13: Permission denied)

403 forbidden (13: Permission denied) 目录 403 forbidden (13: Permission denied) 【常见模块错误】 【解决方案】 欢迎来到我的主页&#xff0c;我是博主英杰&#xff0c;211科班出身&#xff0c;就职于医疗科技公司&#xff0c;热衷分享知识&#xff0c;武汉城市开发者…

手撕Leetcode个人笔记【第二周-数组-链表】

2. 两数相加 中等 给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&#xff0c;并以相同形式返回一个表示和的链表。 你可以假设除了数字 0 之外&#xff0c;…