C++中高阶数据结构(AVL树的原理讲解)

news2024/11/25 10:43:20

AVL树

AVL树的定义

avl本质是搜索树,是高度平衡二叉搜索树.特点是:任何树的左右子树的高度差不超过1.最大的高度差值最大也只能是1,也称之为平衡因子,

平衡因子就是右子树减去左子树的值,这个值的绝对值的最大值只能是1.这个平衡因子不是必须的,只是一种控制方式,方便我们更便捷的控制树.

节点的定义

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

向AVL树插入节点

  • 先按着搜索树的规则插入节点
  • 接着利用平衡因子来观测该树是否还是AVL树
    • 新插入的节点可能会影响该节点的部分祖先的平衡因子的值.
    • 更新规则:在c的左边新增,那么p->bf–,在c的右边新增,那么p->bf++,那么是否还会继续影响祖先呢?取决于p的高度是否变化.
    • 更新之后:父亲的 平衡因子如果是0的话,那么p所在的子树的高度不变不会影响爷爷.(如果p的平衡因子更新之后是0,就说明过更新之前是1或者是-1,说明是在矮的那一边插入了节点,p的高度不变,不会影响爷爷.),此时更新结束
    • 更新之后p的平衡因子是1或者是-1,那么p所在的子树的高度变了.会影响爷爷,说明更新之前p->bf是0,在p的有一边插入之后,p的高度变化了,就会影响爷爷.
    • 更新之后p的平衡因子成了2或者是-2,此时p所在子树就不是AVL树了,那么就需要进行旋转处理了.
    • 最后c成了根节点之后,那么就是更新条件结束了
  • 三种结束条件:
    • 更新到root结束,p->bf==0结束,旋转让parent所在子树的高度回到了插入之前,不会对上层的bf有影响,结束.

代码实现:

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 (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->_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)
			{
				// 旋转处理
				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);
				}

				break;
			}
			else
			{
				// 插入之前AVL树就有问题
				assert(false);
			}
		}

		return true;
	}

AVL中的旋转处理

根据插入数据的不同情况可以分为四种情况的

左单旋

当碰到如下图的情况:(红色的是插入节点之后节点对应的平衡因子的值,红色的方块代表插入了一个节点)

当c的右边增加了节点导致了p的平衡因子变成了2之后,此时将subR的左子树连接到parent的右子树上,紧接着将整个parent为根,a为左子树,b为右子树的整棵树连接到subR的左边.此时再计算平衡因子,发现都成了0,整棵树就满足了avl树的规则.
在这里插入图片描述

这里subR在插入节点之前,b子树和c子树的高度一定是相同的(高度也可以是0),只有如此,subR的节点的平衡因子才是0,假如b子树和c子树的高度不同,那么subR的平衡因子是值可能是1或者是-1,此时在subR的右边插入节点,subR节点的平衡因子可能会变成0或者是2,若变成0,avl树的更新就结束了,根本就不需要调整,若变成了2,那么需要调整的树就是subR这颗树,而不是parent这个树.

同理,a子树的节点的高度也一定是h

  • 如果a子树的高度是h+1,那么parent的平衡因子就是0了,此时无论是在parent的左边还是右边插入元素,都无法使parent的平衡因子更新成2或者是-2
  • 如果a子树的高度使h-1,那么此时这颗树根本就不是AVL树了,说明在新节点插入之前就不是AVL树

代码实现:

void RotateL(Node* parent)
{
	Node* subR = parent->_left;
    Node* subRL = subR->_right;
	Node* ppnode = parent->_parent;
    
    
    parent->_right = subRL;
    if(subRL) // subRL的高度可能是0
    subRL->_parent = parent;
    
    subR->_left = parent;
    parent->_parent = subR;
    
    // 假如插入节点之前parent就是整棵树的根
	if(ppnode == nullptr)
    {
        _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;
}

右单旋

当遇到如下图的情况时:(红色是更新之后的平衡因子的值):

新节点插入到较高左子树的左侧,就使用右单旋.因为此时的树看上去就是整个左侧高,右侧低的形式

右单旋的方法:将subL的b这个右子树连接到parent的左边,紧接着,以parent为根,b为左子树,c为右子树的整棵树连接到subL的右边.

在这里插入图片描述

代码实现:

void RotateR(Node* parent)
{
	Node* subL = parent->_left;
    Node* subLR = subL->_right;
    Node* grandParent = parent->_parent;
    
    parent->_left = subLR;
    // 同理,subLR的高度可能是0 
    if(subLR)
    subLR->_parent = parent;
    
    subL->_right = parent;
    parent->_parent = subL;
    
    // 假如插入节点之前parent就是整棵树的根
    if(_root == parent)
    {
        _root = subL;
        subL->_parent = nullptr;
    }
    else
    {
        if(grandParent->_left ==  parent)
        {
            grandParent->_left = subL;
        }
        else
        {
            grandParent->_right = subL;
		}
        subL->_parent = grandParent;
    }
    parent->_bf =0;
    subL->_bf = 0;
}

左右双旋

当新的节点插入在较高左树的右边时,需要进行左右双旋.

假定是如下图的情况:
在这里插入图片描述

此时在h这个子树插入数据时,整棵树的左边是较高的,此时我们假如采用右旋转,则会:
在这里插入图片描述

会发现,右旋之后,值为30的节点的平衡因子还是-2,所以我们不能采用单旋了,使用双旋.

  • 假设h是大于0的情况:

    先将b子树拆分:
    在这里插入图片描述

    将b拆分为值为40(这里40只是例子,只要满足时大于30小于60即可)的节点和e子树和f子树,那么此时由e和f两个子树了,新的节点就会有两种选择了.

    • 当在e子树插入数据时:以30为断点进行左单旋,接着对整棵树进行右单旋
      在这里插入图片描述

      通过图可知,最后形成的树是符合要求的,并且各自的平衡因子经过计算都是满足要求的.

    • 当在f子树插入节点时:
      对仍然对局部进行左单旋,接着对整棵树进行右单旋.

      在这里插入图片描述

      并且经过计算之后的平衡因子经过计算之后也是符合要求的.

    当h==0时:
    此时e和f就不存在了,

    • 当在30的右边插入节点时,就先对30为根的树进行做单旋,对整棵树进行右单旋.如下图所示,最终计算出的平衡因子都是符合要求的.

    在这里插入图片描述

    • 当在30的左边插入节点时:可以直接对整棵树进行右单旋即可.

    最终形成的树的关键节点的平衡因子的如何确定:由上图可以看出规律

    • 当h!=0时
      • 当新节点在e子树插入时:40所在节点的平衡因子是0,30所在节点的平衡因子是0,60所在节点的平衡因子是1.
      • 当新节点在f子树插入时:40所在节点的平衡因子是0,30所在节点的平衡因子是-1,60所在节点的平衡因子是0.
    • 当h==0时,这个三个关键节点的平衡因子都是0

    如何确定规律呢?

    因为e和f这两颗子树的高度是相同的.所以在e树插入数据时,e的parent的平衡因子就会更新为-1,
    在这里插入图片描述

    当是在f树插入节点时,f的parent的平衡因子就会更新成1.
    在这里插入图片描述

    当e和f不存在时:
    在这里插入图片描述

    代码实现:

    可以就可以利用subL的右节点的平衡因子的值来确定关键节点的平衡因子的值

    这里可以复用前面的代码,但是前面的左旋和右旋都会将节点的平衡因子都改成0.所以需要提前保存这个值

    void RotateLR(Node* parent)
    {
    	// 记录节点,为了保存节点里的_bf的值.
        Node* subL = parent->_left;
    	Node* subLR = subL->_right;
       
        // 提前保存好平衡因子,防止被修改
        int bf = subLR->_bf;
        // 直接调用
        RotateL(parent->_left);
        RotateR(parent);
        
    	if(bf == -1)
        {
            subLR->bf = 0;
            subL->_bf = 0;
            parent->_bf = 1;
        }
        else if(bf == 1)
        {
            subLR->_bf = 0;
           	subL->_bf = -1;
            parent->_bf = 0;
        }
        else if(bf == 0)
        {
    		subLR->_bf = 0;
           	subL->_bf = 0;
            parent->_bf = 0;
        }
        else
        {
            // 此时就不是AVL树. ![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/3a4298086b6f436385586435bd9eaad7.png)
    
            assert(false);  
        }
    }
    

右左双旋

当新节点插入在较高右树的左侧时,需要右左双旋了.采用的是和左右双旋类似的思想

在这里插入图片描述

  • 当h!=0时

    • 当在e树插入时:
      在这里插入图片描述

    • 当在f树插入时:
      在这里插入图片描述

  • 当h==0时
    在这里插入图片描述

代码实现:

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 =0;
		subR ->_bf = 1;
	}
	else if(bf == 1)
	{
		subRL->_bf = 0;
		parent->_bf =-1;
		subR->_bf = 0;
	}
	else if(bf ==0)
	{
		subRL->_bf = 0;
		parent->_bf =0;
		subR->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

如何验证一个树是否是avl树

可以计算每一个子树的高度,观察高度差.

可以先写一个检查高度的函数

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;
	}
	int Height()
	{
		return _Height(_root);
	}

在写一个Isbalance函数检查函数的左右高度是否符合要求

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

		int balance = _Height(root->_right) - _Height(root->_left);

		if (abs(balance) <= 2)
		{
			cout << "平衡" << endl;
			return true;
		}
		else
		{
			cout << "不平衡" << endl;
			return false;
		}
		if (balance != root->_bf)
		{
			cout << " 平衡因子异常 ";
			return false;
		}

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

但是这个函数使用的递归太多了,复杂度太高了.每次在计算高度差的时候,已经将高度给计算出来了,但是函数的最后还是要计算左右子树的高度.

balance的优化,使用一个引用来记录height的左右高度.

bool _IsBalance(Node* root,int& Height)
	{
		if (root == nullptr)
        {
            height = 0;
            return true;            
        }
			

		int balance = _Height(root->_right) - _Height(root->_left);
		int leftHeight = 0,rightHeight = 0;
    // 左右子树只要有一个不是平衡树,就返回false
    	if (!_IsBalance(root->_left, leftHeight) || !_IsBalance(root->_right, rightHeight))
		{
			return false;
		}
		if (abs(rightHeight - leftHeight) >= 2)
		{
			cout <<root->_kv.first<<"不平衡" << endl;
			return false;
		}
		if (balance != root->_bf)
		{
			cout << " 平衡因子异常 ";
			return false;
		}
		height = leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
    	return true;
	}

结束

关于AVL树的讲解就到这里啦,如有不足,请在评论区指正,下期见!

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

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

相关文章

stable diffusion的从安装到使用

stable-diffusion&#xff0c;一个免费开源的文生图软件&#xff0c;文章主要讲怎么从源码开始安装&#xff0c;以及使用的方式 git地址&#xff1a;https://github.com/AUTOMATIC1111/stable-diffusion-webui 本人电脑环境win10&#xff0c;软件pycharm&#xff0c;需要提前…

【话题:工作生活】2022年工作总结--疫情下的上海,疫情中的我。

现在是阳历2023年11月27日星期一&#xff0c;我再次开始撰写自己的年终工作总结。希望再过1、2个月&#xff0c;这份年终总结能够出炉&#xff0c;与大家相遇。 给自己定个小目标&#xff0c;年终的工作生活总结坚持写10年。我2017年毕业&#xff0c;之后就开始写每年的年终总结…

密码应用方案测评要点及测评过程

&#xff08;1&#xff09;背景 《GBT39786-2021 信息系统密码应用基本要求》中第1-4级密码应用基本要求均包括“应依据相关标准和密码应用需求&#xff0c;制定密码应用方案”。第1-4级密码应用基本要求对于“投入运行前进行密码应用安全性评估”的具体如下。 第一级&#xff…

微信小程序Skyline模式下瀑布长列表优化成虚拟列表,解决内存问题

微信小程序长列表&#xff0c;渲染的越多就会导致内存吃的越多。特别是长列表的图片组件和广告组件。 为了解决内存问题&#xff0c;所以看了很多人的资料&#xff0c;都不太符合通用的解决方式&#xff0c;很多需要固定子组件高度&#xff0c;但是瀑布流是无法固定的&#xf…

SpringBoot常用注解及其使用示例

Spring Boot是一个用于快速构建Java应用程序的框架&#xff0c;它简化了Spring应用程序的创建和部署过程。 Spring Boot提供了很多注解&#xff0c;用于简化开发过程&#xff0c;提高开发效率。本文将介绍几个Spring Boot常用注解的使用案例&#xff0c;包括Controller、Reques…

【Xilinx】FPGA中的HPI/O和HRI/O的说明 (hpio hrio)

Xilinx 一些系列FPGA IO Bank分为HP Bank和HR Bank&#xff0c;HP IO接口电压范围为1.2V~1.8V&#xff0c;可以实现高性能&#xff0c;HR IO接口电压范围为1.2V~3.3V。当HR Bank与2.5V或者3.3V外设互联时&#xff0c;需要考虑接口电平的兼容性。 含义 电平 HP接口为高速接口 …

实验模拟gfs 五大逻辑卷

目录 一 实验环境 二 4台服务端准备工作 1&#xff0c;66,77,88,99 四台机器加4块磁盘 2&#xff0c; scan 刷新磁盘供电接口 并查看 3&#xff0c;改主机名&#xff0c;方便后续操作 4&#xff0c;为加快访问速度 写hosts文件 做映射&#xff08;55客户机也写&…

13-pyspark的共享变量用法总结

目录 前言广播变量广播变量的作用 广播变量的使用方式 累加器累加器的作用累加器的优缺点累加器的使用方式 PySpark实战笔记系列第四篇 10-用PySpark建立第一个Spark RDD(PySpark实战笔记系列第一篇)11-pyspark的RDD的变换与动作算子总结(PySpark实战笔记系列第二篇))12-pysp…

【AcWing】蓝桥杯集训每日一题Day16|哈希|FloodFill算法|字典序最小|映射|1402.星空之夜(C++)

1402.星空之夜 1402. 星空之夜 - AcWing题库难度&#xff1a;中等时/空限制&#xff1a;1s / 64MB总通过数&#xff1a;3415总尝试数&#xff1a;7434来源&#xff1a;usaco training 5.1算法标签Flood Fill哈希DFSBFS 题目内容 夜空深处&#xff0c;闪亮的星星以星群的形式出…

雅特力AT32引脚复用记录

用作USB的话&#xff0c;PA9不能再用作其他功能了 被复用了。这个关联信号是OTG_FS_VBUS。stm32是能这么复用的。

大型央国企“信创化”与数字化转型建设思路

一、央国企信创化与数字化转型时代背景 1、信创概念普及&#xff1a; 信创&#xff0c;即“信息技术应用创新”。是我国自主信息产业聚焦的核心&#xff0c;旨在通过对IT硬件、软件等各个环节的重构&#xff0c;基于我国自有IT底层架构和标准&#xff0c;形成自有开放生态&am…

阿里云OSS使用流程

准备工作 无论怎么样&#xff0c;你需要准备一个阿里云账号&#xff0c;点击&#xff1a;注册阿里云 输入相关信息&#xff0c;然后注册成功以后&#xff0c;点击 然后注册成功了&#xff0c;实名一下阿里云账号。打开实名入口&#xff0c;选择个人实名或者企业实名。 如果你…

Xinstall:专业的App下载量统计工具,让推广效果可衡量

在移动互联网时代&#xff0c;App的下载量是衡量一个应用受欢迎程度的重要指标。然而&#xff0c;很多开发者和广告主在推广App时&#xff0c;都面临着一个共同的问题&#xff1a;如何准确统计App的下载量&#xff1f;这不仅关系到推广效果的评估&#xff0c;还直接影响到广告R…

【Linux 学习】进程优先级和命令行参数!

1. 什么是优先级? 指定进程获取某种资源&#xff08;CPU&#xff09;的先后顺序&#xff1b; Linux 中优先级数字越小&#xff0c;优先级越高&#xff1b; 1.1 优先级和权限的区别&#xff1f; 权限 &#xff1a; 能不能做 优先级&#xff1a; 已经能了&#xff0c;但是获…

Linux初学(十七)防火墙

一、防火墙简介 1.1 防火墙的类别 安全产品 杀毒&#xff1a; 针对病毒&#xff0c;特征篡改系统中的文件杀毒软件针对处理病毒程序防火墙&#xff1a; 针对木马&#xff0c;特征系统窃取防火墙针对处理木马 防火墙分为两种 硬件防火墙软件防火墙 硬件防火墙 各个网络安全…

模型量化——NVIDIA——方案选择(PTQ、 partialPTQ、 QAT)

PTQ、 partialPTQ、 QAT 选择流程 PTQ、 partialPTQ、 QAT 咨询NVIDIA 官方后&#xff0c;他们的校正过程一致&#xff0c;支持的量化算子本质是一样的&#xff0c;那么如果你的算子不是如下几类&#xff0c;那么需要自己编写算子。参考TensorRT/tools/pytorch-quantization/py…

淘宝销量API商品详情页原数据APP接口测试㊣

淘宝/天猫获得淘宝app商品详情原数据 API 返回值说明 item_get_app-获得淘宝app商品详情原数据 公共参数 名称类型必须描述keyString是调用key&#xff08;必须以GET方式拼接在URL中&#xff09;secretString是调用密钥api_nameString是API接口名称&#xff08;包括在请求地…

码蹄集部分题目(第五弹;OJ赛2024年第10期)

&#x1f40b;&#x1f40b;&#x1f40b;竹鼠通讯&#xff08;钻石&#xff1b;分治思想&#xff1b;模板题&#xff1a;就算几何平面点对问题&#xff09; 时间限制&#xff1a;3秒 占用内存&#xff1a;128M &#x1f41f;题目描述 在真空中&#xff0c;一块无限平坦光滑…

基于SSM+Jsp+Mysql的高校毕业设计管理系统

开发语言&#xff1a;Java框架&#xff1a;ssm技术&#xff1a;JSPJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包…

【三十六】【算法分析与设计】综合练习(3),39. 组合总和,784. 字母大小写全排列,526. 优美的排列

目录 39. 组合总和 对每一个位置进行枚举 枚举每一个数出现的次数 784. 字母大小写全排列 526. 优美的排列 结尾 39. 组合总和 给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target &#xff0c;找出 candidates 中可以使数字和为目标数 target 的 所有 不…