【数据结构】AVL树(平衡二叉搜索树)

news2024/9/25 3:23:08

文章目录

  • 1.AVL树
    • 1.1 AVL树的概念
    • 1.2 AVL树节点的定义
    • 1.3 AVL树的插入
    • 1.4 AVL树的旋转
      • 1.4.1 左单旋
      • 1.4.2 右单旋
      • 1.4.3 右左双旋
      • 1.4.4 左右双旋
    • 1.5 AVL树的平衡验证
    • 1.6 AVL树的删除
    • 1.7 AVL树的性能

在这里插入图片描述

1.AVL树

在前面,我们已经介绍过了二叉搜索树,也了解到二叉搜索树查找的效率非常的高。但是在数据有序或接近有序时,它就会退化成单边树,那样效率就变得非常低了。所以我们也一直说二叉搜索树的搜索的时间复杂度是O(N)(高度次),并不是O(log2N)。
在这里插入图片描述

因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。

1.1 AVL树的概念

由于二叉搜索树可能会退化,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

为什么要求左右子树的高度之差不超过1就可以呢?

对于所有形状的子树来说,如果他有2n-1个节点,那它就可以保持绝对的平衡;如果他有2n (n >= 1)个节点,就无法做到绝对的平衡,最好的结果就是子树高度相差一。

一棵AVL树要么是空树,要么是具有以下性质的二叉搜索树:

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

在这里插入图片描述

如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果他有n个节点,其高度可保持在log2N,搜索的时间复杂度就是log2N。

1.2 AVL树节点的定义

为了方便确认一棵树是否是AVL树,我们在节点中定义一个平衡因子,通过它来记录每棵树左右子树的高度。

如果右子树比左子树高一层,那平衡因子就是1;如果左右子树一样高,那平衡因子就是0;如果左子树比右子树高一层,那平衡因子就是-1。此时AVL树的性质都没有被打破。

当左子树比右子树高两层,那平衡因子就是-2;或者右子树比左子树高两层,平衡因子就是2,此时AAVL树的性质就被打破了,需要调整。

在调整失衡的AVL树时,我们需要频繁的访问其父节点,因此我们给每个节点定义一个指向父亲的指针

template<class K, class V>
struct AVLTreeNode
{
	pair<K, V> _kv;  //存放数据
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _prev;  //指向当前节点的父亲
	int _bf;//平衡因子

	AVLTreeNode(const pair<K,V>& kv)
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,prev(nullptr)
		,_bf(0)
	{}
};

1.3 AVL树的插入

AVL树的插入与普通二叉搜索树的插入基本一致。唯一的区别就是AVL树插入节点后需要判断AVL树是否失衡,存在失衡就需要调整。

我们平衡因子是使用右子树的高度-左子树的高度,因此,如果新节点插入到父节点的右边,那父亲的平衡因子+1;如果新节点插入到父亲节点的左边,那父亲的平衡因子-1。
在这里插入图片描述
对于情况二来说,新节点的插入不会影响其祖先的平衡因子的改变,因为子树的高度没有改变。
对于情况一来说,新节点的插入会影响其祖先的平衡因子的改变,因为子树的高度变高了。

在这里插入图片描述
所以,对于情况一来说,我们新插入节点后,还要观察其祖先的平衡因子是否变化,变化后是否需要调整。

那么下面我们就先将节点插入到树中:

	bool insert(const pair<K, V>& kv)
	{
		//插入前是空树
		if (_root == nullptr)
		{
			//新插入的节点就是根
			_root = new Node(kv);
			_root->_prev = nullptr;
			return true;
		}
		else
		{
			Node* cur = _root;
			Node* parent = nullptr;
			//寻找插入位置
			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;//节点已经存在,插入失败
				}
			}
			//已找到了插入位置
			cur = new Node(kv);
			//判断连接在父亲的哪一侧
			if (parent->_kv.first < kv.first)
				parent->_right = cur;
			else
				parent->_left = cur;
			cur->_prev = parent;//指向父亲

			//判断树是否失衡
			//...
		}
	}

更新父亲/祖先的平衡因子

		//判断树是否失衡
			while (parent)
			{
				//更新父亲的平衡因子
				if (parent->_left == cur)
					parent->_bf--;
				else
					parent->_bf++;
					
				if (parent->_bf == 0)//属于情况二,不影响祖先
					break;
				//情况一,影响祖先的平衡因子,需要继续更新
				else if (parent->_bf == 1 || parent->_bf == -1)
				{
					cur = parent;
					parent = cur->_prev;
				}
				else if (parent->_bf == 2 || parent->_bf == -2)
				{
					//旋转处理
					break;
				}
				else
				{
					assert(false);//平衡因子异常了
				}
			}

1.4 AVL树的旋转

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

  • 左单旋
  • 右单旋
  • 左右双旋
  • 右左双旋

下面,我们对这四种情况具体分析

1.4.1 左单旋

新节点插入到较高右子树的右侧- -左单旋
在这里插入图片描述
在旋转过程中,有以下几种情况需要考虑:

  1. 7节点的左孩子可能存在,也可能不存在
  2. 6可能是根节点,也可能是子树
    • 如果是根节点,旋转完成后,根节点就是7
    • 如果是子树,可能是某个节点的左子树,也可能是右子树,需要判断以后连接
void RotateL(Node* parent)
	{
		Node* subR = parent->_right;//父亲的右孩子

		Node* subRL = subR->_left;//右孩子的左孩子
		//升高度,连孙子
		parent->_right = subRL;
		if (subRL)
			subRL->_prev = parent;
		
		Node* parentPrev = parent->_prev;
		//降高度,变父亲
		subR->_left = parent;
		parent->_prev = subR;

		//连接旋转后的子树
		subR->_prev = parentPrev;
		if (parentPrev == nullptr)
		{
			//如果原父亲就是根,旋转后的儿子变根
			_root = subR;
		}
		else
		{
			//原父亲是子树,判断儿子连接在哪一边
			if (parentPrev->_left == parent)
				parentPrev->_left = subR;
			else
				parentPrev->_right = subR;
		}
		//更新平衡因子
		subR->_bf = 0;
		parent->_bf = 0;
	}

1.4.2 右单旋

新节点插入到较高左子树的左侧- - 右旋

在这里插入图片描述
在旋转过程中,有以下几种情况需要考虑:

  1. 5节点的右孩子可能存在,也可能不存在
  2. 6可能是根节点,也可能是子树
    • 如果是根节点,旋转完成后,根节点就是5
    • 如果是子树,可能是某个节点的左子树,也可能是右子树,需要判断以后连接
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		//升高度,连孙子
		parent->_left = subLR;
		if (subLR)
			subLR->_prev = parent;

		Node* parentPrev = parent->_prev;
		//降高度,变父亲
		subL->_right = parent;
		parent->_prev = subL;

		//新父亲指向前
		subL->_prev = parentPrev;

		if (nullptr == parentPrev)
		{
			//如果原父亲就是根,旋转后的儿子变根
			_root = subL;
		}
		else
		{
			//原父亲是子树,判断儿子连接在哪一边
			if (parentPrev->_left == parent)
				parentPrev->_left = subL;
			else
				parentPrev->_right = subL;
		}

		subL->_bf = 0;
		parent->_bf = 0;
	}

1.4.3 右左双旋

新节点插入到较高右子树的左侧- -右左双旋
在这里插入图片描述
所以,为了解决这种情况,我们可以将这种歪脖树先变成一棵单边树,然后再进行单旋即可。
在这里插入图片描述

为了满足所有情况,我们下面使用抽象图再画一遍
在这里插入图片描述

在这里插入图片描述

观察上图我们可以发现,双旋就是一种抛弃自己的孩子,独自登基的感觉,因此对于旋转后平衡因子的改变,取决于新节点插入到哪一边,最后又到哪里去

void RotateRL(Node* parent)
	{
		//由于单旋会改变节点的位置以及平衡因子,所以提前记录

		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;

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

		//修改平衡因子
		if (bf == 0)//subRL插入前是空
		{
			parent->_bf = 0;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == 1)//subRL之前是一棵树,新节点插入其  右侧
		{
			parent->_bf = -1;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else			 //subRL之前是一棵树,新节点插入其  左侧
		{
			parent->_bf = 0;
			subR->_bf = -1;
			subRL->_bf = 0;
		}
	}

在这里插入图片描述

1.4.4 左右双旋

新节点插入到较高左子树的右侧–左右双旋

最简单的情况,2的右子树插入前为空
在这里插入图片描述

抽象图
在这里插入图片描述
跟右左双旋基本一样,这里就不赘述了,咱们直接开旋。
在这里插入图片描述
平衡因子的改变也需要小心

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

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

		if (bf == 0) // subLR插入前为空
		{
			parent->_bf = 0;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else if (bf == 1) //subLR是一棵树,新节点插入到树的  右侧
		{
			parent->_bf = 0;
			subL->_bf = -1;
			subLR->_bf = 0;
		}
		else              //subLR是一棵树,新节点插入到树的  左侧
		{
			parent->_bf = 1;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
	}

在这里插入图片描述

下面就是更新平衡因子的整体思路:

			//判断树是否失衡
			while (parent)
			{
				//更新父亲的平衡因子
				if (parent->_left == cur)
					parent->_bf--;
				else
					parent->_bf++;

				if (parent->_bf == 0)//属于情况二,不影响祖先
					break;
				//情况一,影响祖先的平衡因子,需要继续更新
				else if (parent->_bf == 1 || parent->_bf == -1)
				{
					cur = parent;
					parent = cur->_prev;
				}
				else if (parent->_bf == 2 || parent->_bf == -2)
				{
					//旋转处理
					//左单旋
					//    p                     cur
					//      cur      --->    p      new
					//         new
					if (parent->_bf == 2 && cur->_bf == 1)
						RotateL(parent);
					//右单旋
					//			p               cur
					//		cur      --->   new     p
					//	new
					else if (parent->_bf == -2 && cur->_bf == -1)
						RotateR(parent);
					//右左双旋
					//     p            p                  cur
					//        cur  -->    cur      -->   p     new
					//     new                new
					else if (parent->_bf == 2 && cur->_bf == -1)
						RotateRL(parent);
					//左右双旋
					//      p             p            cur
					//  cur      -->    cur      --> new   p     
					//     new        new
					else if (parent->_bf == -2 && cur->_bf == 1)
						RotateLR(parent);
					else
						assert(false);

					break;//旋转后,以parent为根的树已经平衡,无需继续向上更新
				}
				else
				{
					assert(false);//平衡因子异常了
				}

总结:
假如以Parent为根的子树不平衡,即Parent的平衡因子为2或者-2,分以下情况考虑

  1. Parent的平衡因子为2,说明Parent的右子树高,设Parent的右子树的根为SubR
    • 当SubR的平衡因子为1时,执行左单旋
    • 当SubR的平衡因子为-1时,执行右左双旋
  2. Parent的平衡因子为-2,说明Parent的左子树高,设Parent的左子树的根为SubL
    • 当SubL的平衡因子为-1是,执行右单旋
    • 当SubL的平衡因子为1时,执行左右双旋

旋转完成后,原Parent为根的子树个高度降低,已经平衡,不需要再向上更新。

1.5 AVL树的平衡验证

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

  1. 验证其为二叉搜索树
    • 如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
  2. 验证其为平衡树
    • 每个节点子树高度差的绝对值不能超过1
    • 节点的平衡因子是否计算正确
	int Height(Node* root)
	{
		if (root == nullptr)
			return 0;
		int leftHeight = Height(root->_left);
		int rightHeight = Height(root->_right);

		return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
	}
	
	bool _isBalanceTree()
	{
		return isBalanceTree(_root);
	}

	bool isBalanceTree(Node* root)
	{
		if (root == nullptr)//空树也是平衡二叉搜索树
			return true;

		int leftHeight = Height(root->_left);
		int rightHeight = Height(root->_right);

		int gap = rightHeight - leftHeight;  //计算root的平衡因子
		// 如果计算出的平衡因子与root的平衡因子不相等,或者
		//root平衡因子的绝对值超过1,则一定不是AVL树
		if (gap != root->_bf || (gap > 1 || gap < -1))
		{
			return false;
		}
		return isBalanceTree(root->_left) && isBalanceTree(root->_right);
	}

在这里插入图片描述

1.6 AVL树的删除

  • 因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除
  • (分析有无左右孩子,若仅有一个孩子,则直接将删除节点连接孩子;若有两个孩子,将删除节点替换为删除节点的左子树的最大或右子树的最小)
  • 然后再更新平衡因子,只不过与插入不同的是,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。

1.7 AVL树的性能

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log2N。
但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。
因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

在这里插入图片描述

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

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

相关文章

03 capture软件操作界面和常用设置介绍04 capture软件自带元件库设置

03 capture软件操作界面和常用设置介绍&&04 capture软件自带元件库设置 第一部分 03 capture软件操作界面和常用设置介绍一、分辨率二、产品选择三、颜色设置四、格点设置 第二部分 04 capture软件自带元件库设置 第一部分 03 capture软件操作界面和常用设置介绍 一、…

初识c++:string类(2)

#本节主要讲解c&#xff1a;string类的模拟实现 全部代码的实现在最后面&#xff01;&#xff01;&#xff01;有需要的自己往下滑&#xff0c;自取&#xff01;&#xff01;&#xff01;1.string类的模拟实现 2.浅拷贝 3.深拷贝 目录 #本节主要讲解c&#xff1a;string类…

使用Ollama和OpenWebUI,轻松探索Meta Llama3–8B

大家好&#xff0c;2024年4月&#xff0c;Meta公司开源了Llama 3 AI模型&#xff0c;迅速在AI社区引起轰动。紧接着&#xff0c;Ollama工具宣布支持Llama 3&#xff0c;为本地部署大型模型提供了极大的便利。 本文将介绍如何利用Ollama工具&#xff0c;实现Llama 3–8B模型的本…

WEB前端10- Fetch API(同步/异步/跨域处理)

Fetch API Fetch API 可以用来获取远程数据&#xff0c;用于在 Web 应用程序中发起和处理 HTTP 请求。它基于 Promise&#xff0c;提供了一种简单而强大的方式来处理网络通信&#xff0c;替代了传统的 XMLHttpRequest。 Promise对象 Promise 对象是 JavaScript 中处理异步操…

Netty:基于NIO的 Java 网络应用编程框架

Netty 是一个被广泛使用的&#xff0c;基于NIO的 Java 网络应用编程框架&#xff0c;Netty框架可以帮助开发者快速、简单的实现客户端和服务端的网络应用程序。“快速”和“简单”并不用产生维护性或性能上的问题。Netty 利用 Java 语言的NIO网络编程的能力&#xff0c;并隐藏其…

C++ 鼠标轨迹API【神诺科技SDK】

一.鼠标轨迹模拟简介 传统的鼠标轨迹模拟依赖于简单的数学模型&#xff0c;如直线或曲线路径。然而&#xff0c;这种方法难以捕捉到人类操作的复杂性和多样性。AI大模型的出现&#xff0c;使得神诺科技 能够通过深度学习技术&#xff0c;学习并模拟更自然的鼠标移动行为。 二.…

Spring Security 介绍

1.概要 Spring Security是一个用于在Java应用程序中实现身份验证和访问控制的强大框架。它可以轻松地集成到任何基于Spring的应用程序中&#xff0c;提供了一套丰富的功能来保护应用程序的安全性。 https://spring.io/projects/spring-security/ demo:https://docs.spring.i…

Java使用AsposePDF和AsposeWords进行表单填充

声明&#xff1a;本文为作者Huathy原创文章&#xff0c;禁止转载、爬取&#xff01;否则&#xff0c;本人将保留追究法律责任的权力&#xff01; 文章目录 AsposePDF填充表单adobe pdf表单准备引入依赖编写测试类 AsposeWord表单填充表单模板准备与生成效果引入依赖编码 参考文…

Java | Leetcode Java题解之第275题H指数II

题目&#xff1a; 题解&#xff1a; class Solution {public int hIndex(int[] citations) {int n citations.length;int left 0, right n - 1;while (left < right) {int mid left (right - left) / 2;if (citations[mid] > n - mid) {right mid - 1;} else {lef…

【Hot100】LeetCode—322. 零钱兑换

目录 题目1- 思路2- 实现⭐322. 零钱兑换——题解思路 3- ACM 实现 题目 原题连接&#xff1a;322. 零钱兑换 1- 思路 思路 其中 amount 是背包容量 ——> 其中 nums 数组代表的背包重量 2- 实现 ⭐322. 零钱兑换——题解思路 class Solution {public int coinChange(in…

计算机网络基础:3.DNS服务器、域名分类

一、DNS服务器 DNS服务器在网络中的作用类似于餐厅中的“顾客座位对照表”&#xff0c;它帮助前台&#xff08;路由器&#xff09;将顾客&#xff08;用户&#xff09;的请求转发到正确的餐桌&#xff08;目标设备&#xff09;。 (1)概念与原理 DNS的基本概念 DNS&…

构建智慧农业监管系统:架构设计与技术创新

随着农业现代化的推进和消费者对食品安全的关注增加&#xff0c;智慧农业监管系统的设计变得至关重要。本文将探讨如何利用先进的技术和创新的系统架构&#xff0c;确保农产品生产过程的透明性、安全性和合规性&#xff0c;为农业发展注入新的动力和保障。 ### 1. 系统架构概述…

信息收集Part3-资产监控

Github监控 便于收集整理最新exp或poc 便于发现相关测试目标的资产 各种子域名查询 DNS,备案&#xff0c;证书 全球节点请求cdn 枚举爆破或解析子域名对应 便于发现管理员相关的注册信息 通过Server酱接口接收漏洞信息 https://sct.ftqq.com/ https://github.com/easych…

go中map

文章目录 Map简介哈希表与Map的概念Go语言内建的Map类型Map的声明Map的初始化Map的访问Map的添加和修改Map的删除Map的遍历 Map的基本使用Map的声明与初始化Map的访问与操作Map的删除Map的遍历Map的并发问题实现线程安全的Map 3. Map的访问与操作3.1 访问Map元素代码示例&#…

生成树协议配置与分析

前言&#xff1a;本博客仅作记录学习使用&#xff0c;部分图片出自网络&#xff0c;如有侵犯您的权益&#xff0c;请联系删除 一、相关知识 1、生成树协议简介 生成树协议&#xff08;STP&#xff09;是一种避免数据链路层逻辑环路的机制&#xff0c;它通过信息交互识别环路并…

C语言刷题小记2

前言 本篇博客还是为大家分享一些C语言的OJ题目&#xff0c;如果你感兴趣&#xff0c;希望大佬一键三连。多多支持。下面进入正文部分。 题目1竞选社长 分析&#xff1a;本题要求我们输入一串字符&#xff0c;并且统计个数的多少&#xff0c;那么我们可以通过getchar函数来获…

Adaboost集成学习 | Matlab实现基于LSTM-Adaboost长短期记忆神经网络结合Adaboost集成学习多输入单输出时间序列预测

目录 效果一览基本介绍模型设计程序设计参考资料效果一览 基本介绍 Adaboost集成学习 | Matlab实现基于LSTM-Adaboost长短期记忆神经网络结合Adaboost集成学习时间序列预测(股票价格预测) 模型设计 步骤1: 数据准备 收集和整理历史数据。确保数据集经过适当的预处理,如归一…

【数据结构初阶】一篇文章带你超深度理解【单链表】

hi &#xff01; 目录 前言&#xff1a; 1、链表的概念和结构 2、单链表&#xff08;Single List&#xff0c;简写SList&#xff09;的实现 2.1 定义链表&#xff08;结点&#xff09;的结构 2.2 创建一个链表 2.3 打印链表 2.4 尾插 2.5 头插 2.6 尾删 2.7 头…

Aigtek高压放大器指标参数要求及其应用

高压放大器是一类特殊的放大器&#xff0c;其主要功能是将输入的低电压信号放大为输出的高电压信号。在各种应用中&#xff0c;如音频放大、通信系统、医学设备等&#xff0c;高压放大器都扮演着至关重要的角色。为了确保高压放大器能够满足实际应用的需求&#xff0c;并且具有…

Redis持久化和集群模式

目录 1、什么是持久化&#xff1f; 2、Redis实现持久化的方式 3、RDB&#xff08;Redis DataBase&#xff09;快照模式 3.1 手动触发 3.1.1 save 3.1.2 bgsave 3.2 自动触发 4、AOF&#xff08;append only File&#xff09;日志追加模式 4.1 开启aof 4.2 RBD和AOF的区…