平衡二叉搜索树--AVL详解剖析

news2024/9/22 7:21:36

目录

一、什么是AVL树

二、AVL树的作用

三、树节点的定义

四、节点的插入

五、旋转

1.左单旋

2.右单旋

左右双旋代码 :

4.右左双旋


一、什么是AVL树

AVL树就是二叉搜索树的进一步的优化,二叉搜索树虽可以缩短查找的效率,但是当数据有序或接近有序二叉搜索树变成单支树,在查找的过程中其效率会变得更低下。所以有一种可以达到自平衡二叉搜索树(AVL),它在每次插入或删除节点时会通过平衡因子(高度差)来进行旋转操作保持树的平衡。AVL树的名称来自它的发明者G. M. Adelson-Velsky和E. M. Landis。

AVL树的特点

  1. AVL树的平衡受到平衡因子(高度差)的影响,对于任意节点,其左子树和右子树的高度差不超过1。如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在O(log n),搜索时间复杂度O(log n)
  2. 当向AVL树中插入或删除节点时,就会存在树的平衡性被破坏的可能。所以,为了恢复平衡,AVL树使用四种旋转操作:左旋、右旋、左右旋和右左旋。通过这些旋转操作,AVL树可以在O(log n)时间内完成插入和删除操作,并保持树的平衡。
  3. 因为AVL树保持严格的平衡,因此其在查找、插入和删除操作上的时间复杂度都是O(log n),使得它成为一种高效的数据结构。

二、AVL树的作用

  1.  提供高效的查找操作:AVL树的平衡性保证了在最坏情况下,查找操作的时间复杂度为O(log n),其中n是树中节点的数量。这使得AVL树在需要频繁查找元素的场景下非常有用,例如数据库索引、字典等。
  2. 支持有序遍历:由于AVL树是一种二叉搜索树,其节点按照特定的顺序进行排列。因此,通过对AVL树进行中序遍历,可以按照升序或降序获取树中的所有元素。这使得AVL树在需要按顺序处理数据的场景下很有用,例如范围查询、排序等。
  3. 动态数据集的维护:AVL树的自平衡性适用于动态数据集的维护。当插入或删除节点时,AVL树会通过旋转操作来保持平衡,从而保证树的高度始终保持在O(log n)的范围内。这使得AVL树在需要频繁地插入和删除元素的场景下表现出色,例如实时的数据更新、动态排序等。
  4. 实现其他数据结构:AVL树也可以作为其他数据结构的基础,例如集合、映射、优先队列等。通过在AVL树上实现相应的操作,可以实现这些数据结构,并且保证其在插入、删除和查找等操作上的高效性和平衡性。

三、树节点的定义

template<class K, class V>
struct AVLTreeNode
{
	AVLTreeNode<K, V>* _left;  //左子节点
	AVLTreeNode<K, V>* _right;  //右子节点
	AVLTreeNode<K, V>* _parent;  //父亲节点
	pair<K, V> _kv;  // 用于存储节点值
	int _bf;  // 平衡因子

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

}

typedef AVLTreeNode<K, V> Node;

四、节点的插入

AVL树的插入主要分为两点:

1.根据二叉搜索树的性质进行插入新的节点

   二叉搜索树性质:

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  • 它的左右子树也分为二叉搜索树

2.调整节点的平衡因子

调整规则:(parent表示的是插入后的节点的父亲节点)

1、新增在右,父节点的平衡因子parent->bf++;新增在左,父节点的平衡因子parent->bf--;

2、更新后,如果parent->bf== 1 或-1,说明parent插入前的平衡因子是0,左右子树高度相等,插入后有一边高,parent高度变了,所以需要继续更新上面节点的 bf

3、更新后,如果parent->bf ==0,说明parent插入前的平衡因子是1 或 -1,左右子树一边高一边低,插入后两边一样高,插入填上了矮了那边,parent所在子树高度不变,不需要继续往上更新

4、更新后,如果parent-> bf== 2 或 -2,说明parent插入前的平衡因子是1 或 -1,是平衡临界值,插入后变成2 或 -2,打破了平衡,parent所在子树需要根据实际情况进行旋转处理。

5、更新后,如果parent->bf > 2 或 <-2的值,则说明插入前就不是AVL树,需要去检查之前操作的问题。

插入代码讲解:

bool Insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)//如果是空树,插入的节点就是根节点
		{
			_root = new Node(kv);
			return true;
		}

//parent用于记录cur节点的父亲节点,防止在插入新节点时丢失
		Node* parent = nullptr;
		Node* cur = _root;
// 遍历找到插入新节点的位置,parent记录该位置,cur节点用于探索后面的节点或存储新节点便于插入
		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作为新节点,通过parent与该树进行连接,完成节点插入
		cur = new Node(kv);
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}

		cur->_parent = parent;

		// 控制平衡
		// 更新平衡因子
        //根据上面的调整规则更新平衡因子
		while (parent)
		{
            //规则1
			if (cur == parent->_right)
			{
				parent->_bf++;
			}
			else
			{
				parent->_bf--;
			}
            //规则3
			if (parent->_bf == 0)
			{
				break;
			}
            //规则2
			else if (abs(parent->_bf) == 1)
			{
				parent = parent->_parent;
				cur = cur->_parent;
			}
            //规则4
			else if (abs(parent->_bf) == 2)
			{
				// 说明parent所在子树已经不平衡了,需要旋转处理
				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 if (parent->_bf == 2 && cur->_bf == -1)
				{
					RotateRL(parent);  // 右左双旋 
				}
				else
				{
					assert(false);
				}

				break;
			}
            //规则5不存在的可能
			else
			{
				assert(false);
			}
		}

		return true;
	}

五、旋转

1.左单旋

旋转原因:

在插入新节点后,父节点parent->bf==2,子节点cur->bf==1。也就是在插入前,该节点bf=1,插入后bf=2,是在该节点的右子树进行插入。如下图:

subR:parent的右子节点

subRL:parent的右子节点的左节点

旋转后subRL的父亲节点,要由原来的subR变为parent节点。subR节点,就变为新的parent节点

 左单旋代码:

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

        //将原来的subRL节点的父亲节点指向为parent
		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;

        //记录当前parent节点的父节点,保证旋转后,能与主树连接上
		Node* ppNode = parent->_parent;

        //将subR变为新的parent节点
		subR->_left = parent;
		parent->_parent = subR;

        //将旋转后的子树与原来的主树进行连接
		if (_root == parent)
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else
		{
            //判断该子树是原来主树的左子树还是右子树,在进行连接
			if (ppNode->_left == parent)
			{
				ppNode->_left = subR;
			}
			else
			{
				ppNode->_right = subR;
			}

			subR->_parent = ppNode;
		}
}

2.右单旋

旋转原因:

parent->bf==-2,cur->bf==-1

在parent的左子树左节点上进行插入

 右旋代码: 

	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
        
        //将subLR与parent相连接
		parent->_left = subLR;
		if (subLR)
		{
			subLR->_parent = parent;
		}

		Node* ppNode = parent->_parent;
        
        //subL变为新的parent节点
		subL->_right = parent;
		parent->_parent = subL;

        //将旋转后的子树与主树进行连接
		if (_root == parent)
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			if (ppNode->_left == parent)
			{
				ppNode->_left = subL;
			}
			else
			{
				ppNode->_right = subL;
			}

			subL->_parent = ppNode;
		}

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

 3.左右双旋

旋转原因:

parent->bf == -2 && cur->bf == 1,在parent的左子树的右子树上进行插入,破坏了平衡因子。

需要先进行左旋,再进行右旋,再进行调整平衡因子。

左右双旋代码 :

	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;
        
        //先左旋、再右旋,可以套用上面已实现的方法
		RotateL(parent->_left);
		RotateR(parent);
        subLR->_bf = 0;
        
        //调整平衡因子
        //有三种情况
		if (bf == 1)
		{
			parent->_bf = 0;
			subL->_bf = -1;
		}
		else if (bf == -1)
		{
			parent->_bf = 1;
			subL->_bf = 0;
		}
		else if (bf == 0)
		{
			parent->_bf = 0;
			subL->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

4.右左双旋

旋转原因:

parent->bf == 2 && cur->bf == -1。插入节点在右子树的左子树上

需要先进行右旋,再进行左旋,再调整平衡因子

 右左旋代码:

	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		int bf = subRL->_bf;

        //先右旋,再左旋
		RotateR(parent->_right);
		RotateL(parent);

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

 基于以上就是AVL树的概念和旋转的原理和代码的实现。

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

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

相关文章

SDN系统方法 | 1. 概述

随着互联网和数据中心流量的爆炸式增长&#xff0c;SDN已经逐步取代静态路由交换设备成为构建网络的主流方式&#xff0c;本系列是免费电子书《Software-Defined Networks: A Systems Approach》的中文版&#xff0c;完整介绍了SDN的概念、原理、架构和实现方式。原文: Softwar…

SpringFactoriesLoader解析

一、SpringFactoriesLoader 介绍 1.1 SpringFactoriesLoader 简介 SpringFactoriesLoader 工厂加载机制是 Spring 内部提供的一个约定俗成的加载方式&#xff0c;与 java spi 类似&#xff0c;只需要在模块的 META-INF/spring.factories 文件中&#xff0c;以 Properties 类型…

DOT slam论文翻译

DOT:视觉SLAM的动态目标跟踪 摘要 - 在本文中&#xff0c;我们提出了DOT(动态目标跟踪)&#xff0c;这是一个添加到现有SLAM系统中的前端&#xff0c;可以显着提高其在高动态环境中的鲁棒性和准确性。DOT结合实例分割和多视图几何来生成动态对象的掩模&#xff0c;以允许基于刚…

实现 Rollup 插件alias 并使用单元测试提高开发效率

本篇文章是对 实现 Rollup 插件 alias | 使用 TypeScript 实现库的基本流程 | 使用单元测试提高开发效率 的总结。其中涉及到开发一个组件库的诸多知识点。 实现一个经常用的 rollup 插件 alias 首先执行npm init命令初始化一个package.json文件&#xff0c;因为插件使用了ty…

DevOps系列文章之Argo CD 使用

一、什么是 argo cd Argo CD 是用于 Kubernetes 的声明性 GitOps 连续交付工具。 二、为什么使用 argo cd Argo CD 可在指定的目标环境中自动部署所需的应用程序状态&#xff0c;应用程序部署可以在 Git 提交时跟踪对分支&#xff0c;标签的更新&#xff0c;或固定到清单的特…

测试开发之路 ---- 可读性,可维护性,可扩展性

目录 前言 测试框架与测试脚本的目标&#xff08;部分&#xff09; 分层 使用类似 xml 这种可扩展性强的语义存储数据 代码复用&#xff1a;抽象一切可抽象的&#xff0c;减少一切可能的代码相似与重复 活用 java 注解和反射&#xff08;python 中应该也有相关的机制&…

如何从视频中提取音频?分享三个免费的方法给大家!

在数字时代&#xff0c;视频和音频的使用越来越广泛。有时&#xff0c;您可能希望从视频中提取音频&#xff0c;以便单独使用或与他人分享。无需购买昂贵的软件或具备专业技能&#xff0c;下面将介绍三种免费的方法&#xff0c;帮助您从视频中提取音频。这些方法简单易行&#…

Unity学习笔记--siki学院保卫萝卜

生命周期&#xff1a; 在同一个脚本中的执行先后顺序&#xff1a;先左后右 Inspector 赋值 > 外部调用 > Awake > OnEnable > Start 脚本对象的失活与激活不作用于Awake方法&#xff0c;当方法中只有Awake方法时&#xff0c;控制脚本激活失活的对勾会消失掉 当…

vue3 中ref的函数用法

简介 这里说的ref不是响应式ref,是用在组件身上的ref标识&#xff0c;一般都是ref“某一个字符串”&#xff0c;本文介绍第二种用法&#xff0c;ref“()>{}”,对没错&#xff0c;ref可以等于一个回调函数 ref可以是一个回调 <el-input:ref"(vc: any) > (inputAr…

lwip-2.1.3自带的httpd网页服务器使用教程(三)使用CGI获取URL参数(GET类型表单)

上一篇&#xff1a;lwip-2.1.3自带的httpd网页服务器使用教程&#xff08;二&#xff09;使用SSI动态生成网页部分内容 认识URL参数 在上网的时候&#xff0c;我们经常会见到在网址后面带有?AB&CD这样的语法格式。例如&#xff1a;https://blog.csdn.net/ZLK1214/articl…

OpenCV的HSV颜色空间在无人车中颜色识别的应用

RGB属于三基色空间&#xff0c;是大家最为熟悉的&#xff0c;看到的任何一种颜色都可以由三基色进行混合而成。然而一般对颜色空间的图像进行有效处理都是在HSV空间进行的&#xff0c;HSV(色调Hue,饱和度Saturation,亮度Value)是根据颜色的直观特性创建的一种颜色空间, 也称六角…

如何撤销git上一次的commit(或已push)

如何撤销git上一次的commit&#xff08;或已push&#xff09; 当多人开发时&#xff0c;我们本地commit后&#xff0c;刚要push&#xff0c;发现忘记pull最新代码&#xff0c;此时会有冲突push失败&#xff0c; 我们想要撤销最近的一次commit 我们先简单介绍一下git git有三大…

GreenPlum数据库日常维护

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是【IT邦德】&#xff0c;江湖人称jeames007&#xff0c;10余年DBA工作经验 一位上进心十足的【大数据领域博主】&#xff01;&#x1f61c;&#x1f61…

使用 YOLOv8 和 Streamlit 构建实时对象检测和跟踪应用程序:第3部分:添加跟踪算法

介绍 对象跟踪是随着时间的推移识别一系列帧中的特定对象或多个对象的过程。它涉及定位对象在每个帧中的位置并跟踪其跨帧的移动。对象跟踪在各个领域都有广泛的应用,包括监控、机器人、自动驾驶、运动分析等。 跟踪算法使用各种技术(例如颜色直方图、运动分析、深度学习等)…

【多线程】(二)线程安全问题与线程同步

文章目录 一、多线程带来的风险1.1 观察线程不安全1.2 线程安全概念1.3 线程不安全的原因1.4 线程安全的解决方法 二、synchronized关键字2.1 synchronized 的特性2.2 synchronized 使用示例2.3 Java 标准库中的线程安全类 三、volatile关键字3.1 保证内存可见性3.2 禁止指令重…

Java反射的应用:动态代理

代理设计模式的原理 使用一个代理将对象包装起来, 然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。 对于静态代理&#xff0c;特征是代理类和目标对象的类都是在编译期间确定下来&#xff0c;不利于程…

基于FPGA的按键消抖

文章目录 基于FPGA的按键消抖一、按键消抖原理二、按键消抖代码三、仿真代码编写四&#xff1a;总结 基于FPGA的按键消抖 一、按键消抖原理 按键抖动&#xff1a;按键抖动通常的按键所用开关为机械弹性开关&#xff0c;当机械触点断开、闭合时&#xff0c;由于机械触点的弹性…

怎么使用Netty解码自定义通信协议

网络协议的基本要素 一个完备的网络协议需要具备哪些基本要素 魔数&#xff1a;魔数是通信双方协商的一个暗号&#xff0c;通常采用固定的几个字节表示。魔数的作用是防止任何人随便向服务器的端口上发送数据。协议版本号&#xff1a;随着业务需求的变化&#xff0c;协议可能…

SAP顾问生涯闲记:在SAP工作是什么体验

又有一段时间没更新自己的公众号了&#xff0c;为什么突然决定新开一篇SAP顾问生涯闲记系列的文章呢&#xff0c;是因为最近很荣幸地当选了SAP雇主品牌推广大使&#xff0c;作为SAP官方的推广大使在收获这份荣誉的同时&#xff0c;也承担了一些工作以及责任。 集结完毕︱SAP雇…

Flask_实现token鉴权

目录 1、安装依赖 2、实现代码 3、测试 源码等资料获取方法 1、安装依赖 pip install flask pip install pycryptodome 2、实现代码 import random import string import time import base64from functools import wrapsfrom flask import Flask, jsonify, session, req…