map和set(二)——AVL树的简单实现

news2024/9/30 11:41:08

引入

二叉搜索树有其自身的缺陷,假如往树中 插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此 map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。简单来说就是之前二叉搜索树由于可能在某一个节点上一直深入,按照最坏情况算这的时间复杂度就高了起来,而AVL树这其中之一的平衡树

1.AVL树的概念

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

AVL树又称高度平衡二叉搜索树

任何AVL树都满足一下条件:

  • 它的左右子树都是AVL树
  • 任何树及其子树的高度差(也就是平衡因子)的绝对值不超过1

 比如:

当然平衡因子不一定是必须的,它只是一种控制方式(让我们更便捷地控制这棵树) 

为何是不超过1,而不是0呢?0不是更加平衡吗?

由于树的节点是一个个插入的,无法保证绝对的平衡(有些情况无法满足高度差为0);因此高度差不超过1

2.AVL树节点的定义

和二叉搜索树类似,只不过多了个平衡因子

//AVL树的节点
template<class K,class V>
struct AVLTreeNode
{
	AVLTreeNode<K,V>* _left;
	AVLTreeNode<K,V>* _right;
	AVLTreeNode<K,V>* _parent;
	int _bf = 0;//平衡因子
	pair<K, V> _kv;

	//构造
	AVLTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)
		, _kv(kv)
	{}

};

3.AVL树的插入

3.1插入

AVL树就是在二叉搜索树的基础上引进了平衡因子(bf),因此AVL树也可以看做二叉搜索树

AVL树的插入过程可以分为两步:


  1. 以二叉搜索树的方式插入新节点
  2. 调整节点的平衡因子

二叉搜索树的方式插入 

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 = cur->_right;//往右走
			}
			else if (kv.first < cur->_kv.first)//插入的值比遍历的值小
			{
				parent = cur;
				cur = cur->_left;//往左走
			}
			else//说明插入的值已经存在,return false
			{
				return false;
			}

		}
		//走到这说明已经找到可以插入的地方 
		//创建一个新节点
		cur = new Node(kv);
		//判断插入的节点该连接到父节点的左还是右
		if (cur->_kv.first > parent->_kv.first)//cur 的值大于父节点的值,连右
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}

		//接下来就是判断平衡因子的时候了
		return true;
	}

 判断平衡因子

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

插入节点会影响哪些节点的平衡因子呢?新增节点的部分祖先

更新原则:

若cur是此父节点的左子树(节点)那么父节点的平衡因子-- ,是右子树(节点)那么父节点平衡因子++

是否继续更新取决于父节点的高度是否变化,是否会影响爷爷节点
一直往上走直至parent为nullptr(根节点的父节点为空)或者平衡因子为0或者平衡因子为2(需要旋转)

当cur(新增节点插入后),有三种情况

情况1:

更新后 父节点(parent)的平衡因子(bf)为 0 ,parent所在子树高度不变,不会影响爷爷;说明更新前parent的bf为1或-1,往父节点矮的那边插入节点,左右均衡,parent所在子树的高度不变


情况2:

更新后 父节点的平衡因子为1或-1,parent所在子树高度改变了,会影响爷爷,继续往上更新;说明更新前parent的bf为0(本身平衡了),往p的任意一边插入,使父节点变得不均衡,但不违反规则


情况3:

更新后父节点的平衡因子为2或-2,说明父节点所在的子树违反了平衡规则,需要旋转处理

		//接下来就是判断平衡因子的时候了
		cur->_parent = parent;
		while (parent)
		{
			if (cur == parent->_left)//cur在父左 父bf--
			{
				parent->_bf--;
			}
			else//cur在父右 父bf++
			{
				parent->_bf++;
			}
			if (parent->_bf == 0)//父bf==0 break
			{
				break;
			}
			else if (parent->_bf == 1 || parent->_bf == -1)//父bf为1 or -1继续往上
			{
				cur = cur->_parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				//平衡因子为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)//两边都高左右双旋
				{
					RotateLR(parent);
				}
				else//右左双旋
				{
					RotateRL(parent);
				}
			}
			else
			{

				assert(false);
			}
		}

合起来

	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 = cur->_right;//往右走
			}
			else if (kv.first < cur->_kv.first)//插入的值比遍历的值小
			{
				parent = cur;
				cur = cur->_left;//往左走
			}
			else//说明插入的值已经存在,return false
			{
				return false;
			}

		}
		//走到这说明已经找到可以插入的地方 
		//创建一个新节点
		cur = new Node(kv);
		//判断插入的节点该连接到父节点的左还是右
		if (cur->_kv.first > parent->_kv.first)//cur 的值大于父节点的值,连右
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}

		//接下来就是判断平衡因子的时候了
		cur->_parent = parent;
		//若cur是此父节点的左子树(节点)那么父节点的平衡因子-- ,是右子树(节点)那么父节点平衡因子++
		//一直往上走直至parent为nullptr(根节点的父节点为空)或者平衡因子为0或者平衡因子为2(需要旋转)
		while (parent)
		{
			if (cur == parent->_left)//cur在父左 父bf--
			{
				parent->_bf--;
			}
			else//cur在父右 父bf++
			{
				parent->_bf++;
			}
			if (parent->_bf == 0)//父bf==0 break
			{
				break;
			}
			else if (parent->_bf == 1 || parent->_bf == -1)//父bf为1 or -1继续往上
			{
				cur = cur->_parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				//平衡因子为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)//两边都高左右双旋
				{
					RotateLR(parent);
				}
				else//右左双旋
				{
					RotateRL(parent);
				}
			}
			else
			{

				assert(false);
			}
		}
		return true;
	}

3.2旋转

如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:左单旋、右单旋、左右双旋以及右左双旋

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

void RotateL(Node* parent)//左单旋
	{
		//sub是parent ,subR是parent的右节点,subRL是subR的左节点
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		parent->_right = subRL;//父节点的右节点指向subRL
		//subRL可能为空节点,也就是这颗树(子树)只有sub(parent节点)、subR、以及新增节点这三个节点 如果subRL为空,就不用将其父节点指向sub了
		if (subRL)
		{
			subRL->_parent = parent;
		}
		Node* ppnode = parent->_parent;
		parent->_parent = subR;//把父节点的父节点指向subR
		//父节点(sub)有可能是一颗树(子树)的根节点 
		//如果(sub)是一颗树的根节点的话 subR直接为根节点,subR的parent直接是空
		if (parent = _root)
		{
			subR = _root;
			subR->_parent = nullptr;
		}
		else//sub是一颗子树的根节点 
		{
			//得看sub是其父节点的左节点还是右节点
			//然后subR连接sub的父节点
			if (parent == ppnode->_left)
			{
				ppnode->_left = subR;
			}
			else
			{
				ppnode->_right = subR;
			}
			subR->_parent = ppnode;
		}
		//sub和subR平衡因子置为0
		subR->_bf = 0;
		parent->_bf = 0;

	}

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

//右单旋思路和左单旋差不多
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;
		Node* ppnode = parent->_parent;
		parent->_parent = subL;
		if (parent == _root)
		{
			subL = _root;
			subL->_parent = nullptr;
		}
		else
		{
			if (parent == ppnode->_left)
			{
				ppnode->_left = subL;
			}
			else
			{
				ppnode->_right = subL;
			}
			subL->_parent = ppnode;
		}
		subL->_bf = 0;
		parent->_bf = 0;
	}

左右双旋—— 新节点插入较高左子树的右侧---左右(先左单旋再右单旋)

左右两边都高单旋解决不了问题

//左右双旋
	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		//subLR的平衡因子不同时对其它节点的平衡因子的改变也不同
		int bf = subLR->_bf;

		RotateL(parent->_left);//先走左单旋
		RotateR(parent);//再走右单旋

		if (bf == -1)//若subLR的bf为-1,则新增节点是subLR的左子树
		{
			subLR->_bf = 0;
			subL->_bf = 0;
			parent->_bf = 1;
		}
		else if (bf == 1)//若subLR的bf为1,则新增节点是subLR的右子树
		{
			subLR->_bf = 0;
			subL->_bf = -1;
			parent->_bf = 0;
		}
		else if (bf == 0) //若subLR的bf为0,那么subLR其本身就是新增节点
		{
			subLR->_bf = 0;
			subL->_bf = 0;
			parent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

右左双旋——新节点插入较高右子树的左侧---右左(先右单旋再左单旋)

void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;
		RotateR(subR);
		RotateL(parent);
		if (bf == -1)
		{
			subRL->_bf = 0;
			subR->_bf = 1;
			parent->_bf = 0;
		}
		else if (bf == 1)
		{
			subRL->_bf = 0;
			subR->_bf = 0;
			parent->_bf = -1;
		}
		else if (bf == 0)
		{
			subRL->_bf = 0;
			subR->_bf = 0;
			parent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

 4.判断是否为AVL树

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

1. 验证其为二叉搜索树 如果中序遍历可得到一个有序的序列,就说明为二叉搜索树

	void _Inorder(Node* root)
	{
		if (root == nullptr)
			return;
		_Inorder(root->_left);
		cout << root->_kv.first << "[" << root->_bf << "]" << endl;
		_Inorder(root->_right);
	}

	void Inorder()
	{
		_Inorder(_root);
	}

2. 验证其为平衡树 每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子) 节点的平衡因子是否计算正确

	int _Height(Node* root)
	{
		if (root == nullptr)
			return;
		int leftHight = Height(root->_left);
		int rightHight = Height(root->_right);
		return leftHight > rightHight ? leftHight + 1 : rightHight + 1;

	}
	int Height()
	{
		return _Height(_root);
	}

	bool _IsAVLTree(Node* root)
{
	if (root == nullptr)
	{
		return true;
	}
	
	int leftHeight = Height(root->_left);

	int	rightHeight = Height(root->_right);

	if (abs(leftHeight - rightHeight) >= 2)
	{
		cout << "不平衡" << endl;
		return false;
	}
	if (rightHeight - leftHeight != root->_bf)
	{
		cout << root->_kv.first << "平衡因子异常" << endl;
		return false;
	}
	return _IsAVLTree(root->_left)&&_IsAVLTree(root->_right);
}
	bool IsAVLTree()
	{
		return _IsAVLTree(_root);
	}

如果走前序递归的话计算高度和前序递归会存在大量重复,所以还是走后序的同时求高度

后序先走左子树判断平衡返回高度;再走右子树判断平衡返回高度

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

		height = leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;

		return true;
	}
	bool IsAVLTree()
	{
		int height = 0;
		return _IsAVLTree(_root, height);
	}

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

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

相关文章

深入了解二叉搜索树:原理、实现与应用

目录 一、介绍二叉搜索树 二、二叉搜索树的基本性质 三、二叉搜索树的实现 四、总结 在计算机科学中&#xff0c;数据结构是构建算法和程序的基础。其中&#xff0c;二叉搜索树&#xff08;Binary Search Tree&#xff0c;简称 BST&#xff09;作为一种常见的数据结构&#…

从新手到高手:一站式 SQL Server 学习平台!

介绍&#xff1a;SQL Server是由微软公司开发的关系数据库管理系统&#xff08;RDBMS&#xff09;&#xff0c;自1989年推出以来&#xff0c;已成为全球主流的数据库之一。以下是对SQL Server的详细介绍&#xff1a; 易用性与可伸缩性&#xff1a;SQL Server以其易用性和良好的…

题目:泡澡(蓝桥OJ 3898)

问题描述&#xff1a; 解题思路&#xff1a; 图解&#xff1a;&#xff08;以题目样例为例子&#xff09; 注意点&#xff1a;题目的W是每分钟最大出水量&#xff0c;因此有一分钟的用水量大于出水量则不通过。 补充&#xff1a;差分一般用于对一段区间每个元素加相同值&#x…

arp 代理配置示例

一、应用场景&#xff1a; 当 R1 和 R3 配置静态路由下一跳为接口的时候&#xff0c;让 R2 充当 arp 代理&#xff0c;允许 R1、R3 互访 二、拓朴如下&#xff1a; 三、配置代码&#xff1a; [R1] ip route-static 10.1.23.0 255.255.255.0 GigabitEthernet0/0/0[R2] interf…

Git学习笔记(流程图+示例)

概念 图中左侧为工作区&#xff0c;右侧为版本库。Git 的版本库里存了很多东西&#xff0c;其中最重要的就是暂存区。 • 在创建 Git 版本库时&#xff0c;Git 会为我们自动创建一个唯一的 master 分支&#xff0c;以及指向 master 的一个指 针叫 HEAD。&#xff08;分支和HEAD…

服务器又被挖矿记录

写在前面 23年11月的时候我写过一篇记录服务器被挖矿的情况&#xff0c;点我查看。当时是在桌面看到了bash进程CPU占用异常发现了服务器被挖矿。 而过了几个月没想到又被攻击&#xff0c;这次比上次攻击手段要更高明点&#xff0c;在这记录下吧。 发现过程 服务器用的是4090…

【数据结构】详解时间复杂度和空间复杂度的计算

一、时间复杂度&#xff08;执行的次数&#xff09; 1.1时间复杂度的概念 1.2时间复杂度的表示方法 1.3算法复杂度的几种情况 1.4简单时间复杂度的计算 例一 例二 例三 1.5复杂时间复杂度的计算 例一&#xff1a;未优化冒泡排序时间复杂度 例二&#xff1a;经过优化…

Go语言必知必会100问题-19 浮点数溢出问题

问题呈现 在Go语言中&#xff0c;有两种浮点数类型&#xff08;虚数除外&#xff09;&#xff1a;float32和float64. 浮点数是用来解决整数不能表示小数的问题。我们需要知道浮点数算术运算是实数算术运算的近似&#xff0c;下面通过例子说明浮点数运算采用近似值的影响以及如…

LeetCode:143.重排链表

143. 重排链表 解题过程 /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}* ListNode(int val) { this.val val; }* ListNode(int val, ListNode next) { this.val val; this.next next; …

python 蓝桥杯之并查集

文章目录 总述合并过程查找过程算法实战实战1 总述 并查集&#xff08;Disjoint-set Union&#xff0c;简称并查集&#xff09;是一种用来管理元素分组情况的数据结构。它主要用于解决集合的合并与查询问题&#xff0c;通常涉及到以下两种操作&#xff1a; 合并&#xff08;Uni…

Redis基础篇:初识Redis(认识NoSQL,单机安装Redis,配置Redis自启动,Redis客户端的基本使用)

目录 1.认识NoSQL2.认识Redis3.安装Redis1.单机安装Redis2.配置redis后台启动3.设置redis开机自启 4.Redis客户端1.Redis命令行客户端2.图形化桌面客户端 1.认识NoSQL NoSQL&#xff08;Not Only SQL&#xff09;数据库是一种非关系型数据库&#xff0c;它不使用传统的关系型数…

Android14 Handle机制

Handle是进程内部, 线程之间的通信机制. handle主要接受子线程发送的数据, 并用此数据配合主线程更新UI handle可以分发Message对象和Runnable对象到主线程中, 每个handle实例, 都会绑定到创建他的线程中, 它有两个作用,: (1) 安排消息在某个主线程中某个地方执行 (2) 安排…

解放生产力,AI加持你也能做这些事!

去年网上流行一个说法叫一人企业或超级IP。一个人就是一家公司&#xff0c;可以更加专注于自身核心技能。既能对工作拥有更大的自主性和控制力&#xff0c;又能舍弃了传统公司管理等繁琐的事务工作&#xff0c;可以全面释放自己的兴趣和潜力。 这个概念给笔者留下了比较深的印…

开源的python 游戏开发库介绍

本文将为您详细讲解开源的 Python 游戏开发库&#xff0c;以及它们的特点、区别和应用场景。Python 社区提供了多种游戏开发库&#xff0c;这些库可以帮助您在 Python 应用程序中实现游戏逻辑、图形渲染、声音处理等功能。 1. Pygame 特点 - 基于 Python 的游戏开发库。…

第3章 数据链路层(1)

3.1数据链路层的功能 加强物理层传输原始比特流的功能,将可能出差错的物理连接改成逻辑上无差错的数据链路[节点的逻辑通道] 3.1.1 为网络提供服务 (1).无确认的无连接服务 适合通信质量好的有线传输链路(实时通信或误码率较低的通信信道)【例如以太网】(2).有确认的无连接服务…

WIN32部分知识介绍

&#x1f308;前言&#xff1a;此篇博客是为下一篇的《贪吃蛇》的做的前戏工作&#xff0c;这篇会讲到贪吃蛇所用到的一些工具以及函数。 首先在讲WIN32的内容时我们想了解一下他的基本概念&#xff1a; Windows 这个多作业系统除了协调应⽤程序的执⾏、分配内存、管理资源之外…

指数移动平均(EMA)

文章目录 前言EMA的定义在深度学习中的应用PyTorch代码实现yolov5中模型的EMA实现 参考 前言 在深度学习中&#xff0c;经常会使用EMA&#xff08;指数移动平均&#xff09;这个方法对模型的参数做平均&#xff0c;以求提高测试指标并增加模型鲁棒。实际上&#xff0c;_EMA可以…

全栈的自我修养 ———— css中常用的布局方法flex和grid

在项目里面有两种常用的主要布局:flex和grid布局&#xff08;b站布局&#xff09;&#xff0c;今天分享给大家这两种的常用的简单方法&#xff01; 一、flex布局1、原图2、中心对齐3、主轴末尾或者开始对其4、互相间隔 二、grid布局1、基本效果2、加间隔3、放大某一个元素 一、…

数据的加密方式及操作方法

目录 一 什么是加密 二 加密方法 对称加密&#xff08;如AES加密&#xff09; 非对称加密&#xff08;如RSA加密&#xff09; 散列&#xff08;如MD5加密&#xff09; 三 加密操作 1 MD5加密&#xff08;散列&#xff09; 2 AES加密&#xff08;对称加密&#xff09; …

HTMK5七天学会基础动画网页10(2)

制作立方体 学完前面的基础内容&#xff0c;制作立方体是个不错的练习方法&#xff0c;先看成品 再分析一下&#xff0c;六个面让每个面旋转平移就可以实现一个立方体&#xff0c;来看代码: <title> 制作立方体</title> <style> *{ margin: 0; padding: 0; …