AVL树的原理及其实现

文章目录

  • 前言
  • 了解AVL树
  • AVL树的特点
  • AVL树的节点
  • 调整方案
    • 右单旋
      • 为什么要右单旋呢?
      • 右单旋代码
    • 左单旋
      • 为什么要左单旋?
      • 左单旋代码
    • 左右双旋
      • 左右双旋之后平衡因子的情况
      • 左右双旋代码实现
    • 右左双旋
      • 右左双旋代码:
  • 简单测试

前言

回顾我们对于二叉搜索树的了解,二叉搜索树的效率跟树的高度成反比。而且,数据插入的随机性导致普通的二叉搜索树甚至可能退化成一条“单链表”,这样的结构查找起来时间复杂度直奔O(N),这无疑是我们不想看到的。我们希望,无论数据怎么插入,二叉搜索树的结构都应该保持平衡,即看起来更加接近于完全二叉树或者满二叉树·。AVL树因此诞生。

了解AVL树

AVL树又叫平衡二叉搜索树,得名于它的发明者Georgy Adelson-Velsky和Evgenii Landis。它的出现很好的解决了二叉搜索树的效率退化问题。AVL树的特点就是任一一个节点的左右子树的高度差不超过1。这个特点可以使二叉搜索树的高度始终保持在logn的级别,从而保证了查找效率稳定在O(logn)级别。具体是怎么实现这一特点的呢?下面结合代码给您讲解。

AVL树的特点

  • AVL树具有二叉搜索树的性质,即对于任意一个节点,左子树的值均小于根节点的值,右子树的值均大于根节点的值。
  • 每个节点都有一个平衡因子属性(可视为一个int变量),记录着左右子树的高度差(一般是右子树高度减去左子树高度
  • 查找、删除、插入的时间复杂度均为O(logn).

AVL树的节点

根据AVL树的特点,AVL树的每一个节点都得有一个记录左右子树高度差的平衡因子。除此之外,还需要有一个指向父节点的指针。具体如下:

		//节点信息
	template<class K, class V>
	class AVLNode {
	public:
		AVLNode(const pair<K, V>& kv)
			:_bf(0)
			, left(nullptr)
			, right(nullptr)
			,father(nullptr)
		{
			
		}
		int _bf;//平衡因子
		pair<K, V> _kv;
		AVLNode<K, V>* left;
		AVLNode<K, V>* right;
		AVLNode<K, V>* father;
	};

调整方案

下面所述代码中,平衡二叉树的平衡因子均表示右子树高度减去左子树高度。
当我们向一颗平衡二叉树里插入一个元素,插入节点的祖先节点的平衡因子都有可能会因此改变。从当前新节点的父节点开始向上遍历,重复以下几个步骤:

  1. 用一个指针指cur向新节点表示当前节点,一个指针pa指向其父节点。
  2. 如果cur节点在pa的左子树中,那么这个pa节点的平衡因子减一,否则加一。
  3. 更新平衡因子之后,观察pa平衡因子大小。如果为0,则说明当前pa节点向上的所有父节点的平衡因子都不会改变。
  4. 如果·pa·平衡因子为-1或者1,则说明当前节点符合平衡二叉树节点特征,但是其父节点还不一定,于是继续向上遍历
  5. 如果pa平衡因子为2或者-2,则说明当前节点失衡了(左右子树的高度差大于1)!我们需要调整。

调整方案具体如下:

右单旋

如果cur的平衡因子为-1,且pa的平衡因子为-2。那么我们选择让pa节点右单旋
在这里插入图片描述

上图就是这种情况**。矩形表示一颗子树**。右旋过之后就变成了这样:
在这里插入图片描述

为什么要右单旋呢?

或者说为什么右单旋之后,pacur的平衡因子会为0呢?

假设在没有旋转的时候,pa的左子树高度为N,则右子树高度为N-2。即上图黄色矩形的表示的树的高度为N-2。
由于cur的平衡因子为-1,则cur左子树的高度为N-1,右子树的高度为N-2。即上图蓝色矩形表示的树的高度为N-2。

所以,我们完成这样一个右单旋之后,对于cur来说,左子树的高度依旧是N-1,但是右子树的高度变成了N-1,平衡因子也就变成了0。
对于pa来说,它的右子树高度依旧是N-2,但是左子树高度变成了N-2,平衡因子也变成了0。

右单旋之后还需不需要继续往上调整呢呢?答案是不用了,由于curpa的平衡因子都是0了,再往上的节点的平衡因子保持不变。

继续思考,平衡因子是没问题了,但是这样旋转会不会改变二叉搜索树的性质呢?答案也是不会的。所谓的右单旋,就是把pa变成cur的右孩子,原本cur是pa的左孩子,pa节点包括pa的右子树的值均大于以cur子树的值。旋转之后cur的左子树的值依旧小于cur节点的值,cur右子树的值依旧大于cur节点的值。对于pa来说也是如此。

右单旋代码

//右单旋
void RotateR(pNode pa) {
	pNode subL = pa->left;
	pNode subLR = subL->right;

	pa->left = subLR;
	if (subLR) subLR->father = pa;

	subL->right = pa;
	pNode ppa = pa->father;
	pa->father = subL;
	
	if (pa == _root) {
		_root = subL;
		subL->father = nullptr;
	}
	else {
		if (pa == ppa->left) {
			ppa->left = subL;
		}
		else {
			ppa->right = subL;
		}
		subL->father = ppa;
	}

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

左单旋

如果cur的平衡因子为1,且pa的平衡因子为2。那么我们选择让pa节点左单旋
在理解右单旋的原理之后,对于左单旋也就容易理解了,因为两者的旋转方式都是一样的,只不过“方向”不同。
在这里插入图片描述
这种情况我们就需要对pa进行左单旋,调整之后就变成了下图所示:
在这里插入图片描述

为什么要左单旋?

参考右单旋。

假设在没有旋转的时候,pa的左子树高度为N,则右子树高度为N+2。即上图黄色矩形的表示的树的高度为N。
由于cur的平衡因子为1,则cur右子树的高度为N+1,左子树的高度为N。即上图蓝色矩形表示的树的高度为N。

所以,我们完成这样一个左单旋之后,对于cur来说,右子树的高度依旧是N+1,但是左子树的高度变成了N+1,平衡因子也就变成了0。
对于pa来说,它的左子树高度依旧是N,但是右子树高度变成了N,平衡因子也变成了0。

左单旋代码

//左单旋
void RotateL(pNode pa) {
	pNode subR = pa->right;
	pNode subRL = subR->left;

	pa->right = subRL;
	if (subRL) subRL->father = pa;

	subR->left = pa;
	pNode ppa = pa->father;
	pa->father = subR;

	if (pa == _root) {
		_root = subR;
		subR->father = nullptr;
	}
	else {
		if (pa == ppa->left) {
			ppa->left = subR;
		}
		else {
			ppa->right = subR;
		}
		subR->father = ppa;
	}

	subR->_bf = pa->_bf = 0;
}

左右双旋

如果cur的平衡因子为1,且pa的平衡因子为-2。那么我们选择先让pa节点的左孩子左单旋。然后再让pa右单旋

这种情况只对pa进行右单旋不能保证让所有节点平衡,我们自能
在这里插入图片描述
图中的h表示子树的高度
在这里插入图片描述

仔细观察上图AVL树左右双旋的过程,就能明白为什么要双旋能让每个节点再次平衡了。值得注意的是,在双旋之后,pa于subL节点的平衡因子并不是固定的,它们随着新节点插入到subLR的位置的而变化。
例如:
在这里插入图片描述

左右双旋之后平衡因子的情况

  • 情况(1)如果新节点插入到subLR的左子树中,在双旋之后,这个新节点会变成subL的右子树节点,此时subL的平衡因子为0,pa的平衡因子为1
  • 情况(2)如果新节点插入到subLR的右子树中,双旋之后,这个新结点则会变成pa的左子树节点,此时pa的平衡因子为0,subL的平衡因子为-1
  • 情况(3)如果新插入节点本身就是subLR节点,即此时subLR的平衡因子为0。也就意味着双旋之后,pa和subL的平衡因子为0.
  • 但无论是上面的哪种情况,双旋之后,subLR的平衡因子都是0.

那更新pa和subL的平衡因子时如何区分是以上哪种情况呢?看subLR的平衡因子。如果是-1,表示新节点在subLR的左子树中,即情况(1)。如果是1,说明新节点在subLR的右子树中,即情况(2)。如果是0,表示新节点就是subLR本身,即情况(3)。

左右双旋代码实现

有了上面左右单旋的代码,左右双旋就能通过使用封装它们的函数来实现了:

//左右双旋
void RotateLR(pNode pa) {
	pNode subL = pa->left;
	pNode subLR = subL->right;

	int subLR_bf = subLR->_bf;

	RotateL(subL);
	RotateR(pa);

	if (subLR_bf == -1) {
		pa->_bf = 1 ;
		subL->_bf = 0;
	}
	else if (subLR_bf == 1) {
		pa->_bf = 0;
		subL->_bf = -1;
	}
	else if (subLR_bf == 0) {
		pa->_bf = 0;
		subL->_bf = 0;
	}
	else {
		//perror("RotateLR");
	}
	return;
}

右左双旋

如果cur的平衡因子为-1,且pa的平衡因子为2。那么我们选择先让pa节点的右孩子右单旋。然后再让pa左单旋
原理和左右双旋一致,只不过旋转的方向相反:
在这里插入图片描述
右左双旋的平衡因子的情况参考左右双旋,我这里就不做过多赘述了。

同样右左双旋的代码可以使用单旋的接口来实现

右左双旋代码:

//右左双旋
void RotateRL(pNode pa) {
	pNode subR = pa->right;
	pNode subRL = subR->left;

	int subRL_bf =subRL->_bf;

	RotateR(subR);
	RotateL(pa);

	if (subRL_bf == -1) {
		pa->_bf = 0;
		subR->_bf = 1;
	}
	else if (subRL_bf == 1) {
		pa->_bf = -1;
		subR->_bf = 0;
	}
	else if (subRL_bf == 0) {
		pa->_bf = 0;
		subR->_bf = 0;
	}
	else {
		//perror("RotateRL");
	}

}

简单测试

通过上面的学习,现在我们就能基本实现AVL树的插入操作了。为了检查代码的正确性,下面给出一组测试数据插入到AVL树中,并遍历输出观察结果:

#include<iostream>
#include"myAVLTree.h"
using namespace std;
using namespace k_val;

int main() {
	int a[10] = { 1,7,8,3,4,9,10,2,6,5 };
	AVLTree<int, int> avl;
	for (int i = 0; i < 10; i++) {
		avl.Insert({ a[i],a[i] });
 	}

	avl.InOrder();

	return 0;
}

在这里插入图片描述
如果想观察·内部结构,可以自行打开调试窗口一一查看。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

HarmonyOS开发案例:【生活健康app之实现打卡功能】(2)

实现打卡功能 首页会展示当前用户已经开启的任务列表&#xff0c;每条任务会显示对应的任务名称以及任务目标、当前任务完成情况。用户只可对当天任务进行打卡操作&#xff0c;用户可以根据需要对任务列表中相应的任务进行点击打卡。如果任务列表中的每个任务都在当天完成则为…

基于 AI 的 Python 爬虫

✦ 支持 OPENAI、Gemini、Groq、本地 Ollama、Azure 等 LLM ✦ 只需传递 Prompt 和链接 注意&#xff1a; 调用 Ollama 模型&#xff0c;需要运行下方指令&#xff0c;拉取 embedding 模型&#xff1a; ollama pull nomic-embed-text 问题&#xff1a; 似乎不能换成兼容 Ope…

进程间通信 管道

前言 ubuntu系统的默认用户名不为root的解决方案&#xff08;但是不建议&#xff09;&#xff1a;轻量应用服务器 常见问题-文档中心-腾讯云 (tencent.com) 进程间通信的基本概念 进程间通信目的&#xff1a;进程间也是需要协同的&#xff0c;比如数据传输、资源共享、通知事件…

人脸图像生成(DCGAN)

一、理论基础 1.什么是深度卷积对抗网络&#xff08;Deep Convolutional Generative Adversarial Network&#xff0c;&#xff09; 深度卷积对抗网络&#xff08;Deep Convolutional Generative Adversarial Network&#xff0c;DCGAN&#xff09;是一种生成对抗网络&#xf…

计算机通信SCI期刊推荐,JCR1区,IF=6+,审稿快,无版面费!

一、期刊名称 Computer Communications 二、期刊简介概况 期刊类型&#xff1a;SCI 学科领域&#xff1a;计算机科学 影响因子&#xff1a;6 中科院分区&#xff1a;3区 出版方式&#xff1a;订阅模式/开放出版 版面费&#xff1a;选择开放出版需支付$2300 三、期刊征稿…

STM32中的ICACHE是什么有什么用?如何使用?

什么是ICACHE&#xff1f; icache是一种用于缓存指令的存储器&#xff0c;其目的是提高CPU执行指令的效率。 在计算机系统中&#xff0c;icache&#xff08;指令缓存&#xff09;是处理器核心内部的一个关键组件&#xff0c;它专门用来存储最近使用过的指令。当CPU需要执行一个…

Bean的作用域

Bean的作用域 Bean的作用域是指在Spring整个框架中的某种行为模式&#xff0c;比如singleton单例作用域&#xff0c;就表示Bean在整个spring中只有一份&#xff0c;它是全局共享的&#xff0c;那么当其他人修改了这个值时&#xff0c;那么另一个人读取到的就是被修改的值 Bea…

每日OJ题_记忆化搜索②_力扣62. 不同路径(三种解法)

目录 力扣62. 不同路径 解析代码1_暴搜递归&#xff08;超时&#xff09; 解析代码2_记忆化搜索 解析代码3_动态规划 力扣62. 不同路径 62. 不同路径 难度 中等 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器…

element-ui skeleton 组件源码分享

今日简单分享 skeleton 骨架屏组件源码&#xff0c;主要从以下四个方面来讲解&#xff1a; 1、skeleton 组件的页面结构 2、skeleton 组件的属性 3、skeleton item 组件的属性 4、skeleton 组件的 slot 一、skeleton 组件的页面结构 二、skeleton 组件的属性 2.1 animate…

BS架构 数据权限--字段级权限 设计与实现

一、需求场景 1. 销售发货场景 销售出库单上 有 商品名称、发货数量、单价、总金额 等信息。 销售人员 关注 上述所有信息&#xff0c;但 仓管人员 不需要知道 单价、总金额 信息。 2. 配方、工艺保密 场景 配方研发人员 掌握核心配方&#xff0c; 但 交给车间打样、生产时…

一款免费的PDF转换工具分享

最近在吾爱上发现一款PDF免费转换工具&#xff0c;支持多种格式转换&#xff0c;试了一下&#xff0c;还不错 最重要的是免费&#xff0c;不用开会员转换&#xff0c;也没有限制&#xff08;文末有工具地址&#xff09; ps:转换完成后看一下是否符合&#xff0c;可能会有些许…

即插即用模块:Convolutional Triplet注意力模块(论文+代码)

目录 一、摘要 二、创新点总结 三、代码详解 论文&#xff1a;https://arxiv.org/pdf/2010.03045v2 代码&#xff1a;https://github. com/LandskapeAI/triplet-attention 一、摘要 由于注意机制具有在通道或空间位置之间建立相互依赖关系的能力&#xff0c;近年来在各种计…

Web功能测试之表单、搜索测试

初入职场接触功能测试老是碰到以下情况不知道怎么写测试用例&#xff1a; 一个界面很多搜索条件怎么写用例&#xff1f; 下拉框测试如何考虑测试点&#xff1f; 上传要考虑哪些验证点&#xff1f;...... 所以这篇主要是整理关于web测试之表单、搜索测试的相关要点。 一、表…

小程序(三)

十三、自定义组件 &#xff08;二&#xff09;数据方法声明位置 在js文件中 A、数据声明位置&#xff1a;data中 B、方法声明位置methods中&#xff0c;这点和普通页面不同&#xff01; Component({/*** 组件的属性列表*/properties: {},/*** 组件的初始数据*/data: {isCh…

离线使用evaluate

一、目录 步骤demorouge-n 含义 二、实现 步骤 离线使用evaluate: 1. 下载evaluate 文件&#xff1a;https://github.com/huggingface/evaluate/tree/main2. 离线使用 路径/evaluate-main/metrics/rougedemo import evaluate离线使用evaluate: 1. 下载evaluate 文件&…

# 从浅入深 学习 SpringCloud 微服务架构(十五)

从浅入深 学习 SpringCloud 微服务架构&#xff08;十五&#xff09; 一、SpringCloudStream 的概述 在实际的企业开发中&#xff0c;消息中间件是至关重要的组件之一。消息中间件主要解决应用解耦&#xff0c;异步消息&#xff0c;流量削锋等问题&#xff0c;实现高性能&…

Java中使用RediSearch进行高效数据检索

RediSearch是一款构建在Redis上的搜索引擎&#xff0c;它为Redis数据库提供了全文搜索、排序、过滤和聚合等高级查询功能。通过RediSearch&#xff0c;开发者能够在Redis中实现复杂的数据搜索需求&#xff0c;而无需依赖外部搜索引擎。本文将介绍如何在Java应用中集成并使用Red…

3. 多层感知机算法和异或门的 Python 实现

前面介绍过感知机算法和一些简单的 Python 实践&#xff0c;这些都是单层实现&#xff0c;感知机还可以通过叠加层来构建多层感知机。 2. 感知机算法和简单 Python 实现-CSDN博客 1. 多层感知机介绍 单层感知机只能表示线性空间&#xff0c;多层感知机就可以表示非线性空间。…

切割链表 问题的讲解和实现(带哨兵位)

一&#xff1a;题目 二&#xff1a;思路讲解 先 将小于x的放进一个新的链表&#xff0c;将≥x的也放进另一个新链表。 然后 第一个新链表的尾节点的next链接到第二个新链表的哨兵节点的next&#xff0c;因为本身不存在哨兵位。 最后 链接完成后的链表的最后一个节点的next一…

python数据分析——数据的选择和运算

数据的选择和运算 前言一、数据选择NumPy的数据选择一维数组元素提取示例 多维数组行列选择、区域选择示例 花式索引与布尔值索引布尔索引示例一示例二 花式索引示例一示例二 Pandas数据选择Series数据获取DataFrame数据获取列索引取值示例一示例二 取行方式示例loc() 方法示例…
最新文章