【数据结构】平衡二叉树

导语

对于二叉搜索树 而言,它的 增、 删 、 改 、 查  的时间复杂度为 O(\log_{2}n) ~ O(n) 。原因是最坏情况下,二叉搜索树会退化成 线性表  。换言之,树的高度决定了它插入、删除和查找的时间复杂度
为了对上述缺点进一步优化,设计了一种高度始终能够接近 O(\log_{2}n) 的 树形  的数据结构,它既有链表的快速插入与删除的特点,又有顺序表快速查找的优势。它就是:平衡二叉树 。


 一、平衡二叉树基本概念

1、平衡二叉树的定义

平衡二叉树(AVL树),是一种平衡(balanced)二叉搜索树(binary search tree, 简称为BST)。由两位科学家在1962年发表的论文《An algorithm for the organization of information》当中提出,作者是发明者G.M. Adelson-Velsky和E.M. Landis。它具有以下两个性质:

  • 空树是平衡二叉树
  • 任意一个结点的key,比它的左孩子key大,比它的右孩子key小;
  • 任何一个结点的左子树与右子树都是平衡二叉树,并且高度之差的绝对值不超过 1。

2、树的高度

一棵树的高度,是指从树根结点到达最远的叶子结点的路径上经过的结点数。所以,求树的高度我们可以采用递归的方式。主要有以下三种情况:

1)空树的树高为 0;

2)叶子结点的树高为 1;

3)其它结点的树高,等于左右子树树高的大者加 1;

3、平衡因子

二叉树上的结点的 左子树高度 减去 右子树高度 的值称为 平衡因子,即 BF(Balance Factor)。 根据平衡二叉树的定义,树上所有结点的平衡因子只可能是 -1、0 和 1。即只要二叉树上有一个结点的平衡因子的绝对值大于 1,则该二叉树就是不平衡的。

二、平衡二叉树存储结构

对于平衡二叉树,首先是二叉搜索树,所以会有 左右孩子指针数据域,然后特殊之处是需要平衡因子,而平衡因子可以通过节点树的高度来计算,所以需要加一个 高度。

struct TreeNode {
	int val;
	struct TreeNode* left;
	struct TreeNode* right;
	int height;
	TreeNode(int x, int h = 1) :val(x), left(nullptr), right(nullptr), height(h){}
};

三、平衡二叉树基本接口

1、获取树高

int AVLGetHeight(TreeNode* node)
{
	if (node == nullptr) {
		return 0;
	}
	return node->height;
}

获取树高期望做到 O(1) 的时间复杂度,height 字段是需要存储在结点上的,并且每次树的 插入、删除 操作都需要更新这个值

空结点的树高为 0,其它结点的树高可以直接通过获取树结点结构体的成员变量height 字段快速获取。

2、计算树高

每次对树进行插入、删除操作,对树的原结点高度会有影响,所以需要重新计算,更新这个值。

//计算树高(结点增删需要重新计算树高)
void AVLCalcHeight(TreeNode* node) {
	if (nullptr == node) {              
		return ;
	}                                
	node->height = 1 + std::max(AVLGetHeight(node->left), AVLGetHeight(node->right));
}

 3、获取平衡因子

同理,每次对树进行插入、删除操作,对树的原结点的平衡因子也会有影响,所以也需要重新计算这个值。

//获取平衡因子
int AVLGetBalanceFactor(TreeNode* node) {
	if (node == nullptr)
		return 0;                                                
	return AVLGetHeight(node->left) - AVLGetHeight(node->right); 
}

4、旋转操作

每次对树进行插入、删除操作,可能会引起树的平衡,此时就需要通过旋转操作来使树重新回到平衡状态。

假设本来这棵树是平衡的,在我们在插入一个结点以后,导致了这棵树的不平衡,那么必然是这棵树根结点的平衡因子从 +1 变成了 +2,或者从 -1 变成了 -2 。我们来分别讨论这两种情况。

实际上,总共有四种情况:

1)LL(往左子树的左子树插入一个结点),根结点的平衡因子 +2,左子树根结点平衡因子 +1;

2)RR(往右子树的右子树插入一个结点),根结点的平衡因子 -2,右子树根结点平衡因子 -1;

3)LR(往左子树的右子树插入一个结点),根结点的平衡因子 +2,左子树根结点平衡因子 -1;

4)RL(往右子树的左子树插入一个结点),根结点的平衡因子 -2,右子树根结点平衡因子 +1;

结论:+1 变成 +2 的情况发生在 LL 和 LR,即往当前树的左子树插入一个结点的情况;-1 变成 -2 的情况发生在 RL 和 RR,即往当前树的右子树插入一个结点的情况。

(1) LL

LL,即往左子树的左子树插入一个结点。这种情况下,如果树出现了不平衡的情况,根结点的当前平衡因子一定是 +2。

如上图所示,在左子树插入T5结点后,平衡二叉树的平衡状态被打破,要想回到平衡状态需要对树进行一个右旋操作。

如图所示,以左子树根结点T1作支点右旋后,重新达到平衡。总共有以下关系发生了变化:

(1)T1变成了新的树根

(2)T和T1父子关系发生了交换

(3)T4的父节点由T1变为T

右旋源码

//右旋
TreeNode* RRotate(TreeNode* T)
{
	TreeNode* LNode = T->left;
	T->left = LNode->right;
	LNode->right = T;
	AVLCalcHeight(T);
	AVLCalcHeight(LNode);
	return LNode;
}

经过右旋后,只有T1和T的高度发生了变化,所以需要对它们重新计算高度。

LL型旋转处理

// LL 型右旋处理
TreeNode* AVLTreeLL(TreeNode* T) {
	return RRotate(T);
}
(2)RR

RR,即往右子树的右子树插入一个结点。这种情况下,如果树出现了不平衡的情况,根结点的当前平衡因子一定是 -2。

如上图所示,在右子树插入T5结点后,平衡二叉树的平衡状态被打破,要想回到平衡状态需要对树进行一个左旋操作。

如图所示,以右子树根结点T2作支点左旋后,重新达到平衡。总共有以下关系发生了变化:

(1)T2变成了新的树根

(2)T和T2父子关系发生了交换

(3)T3的父节点由T2变为T

左旋源码

//左旋
TreeNode* LRotate(TreeNode* T)
{
	TreeNode* RNode = T->right;
	T->right = RNode->left;
	RNode->left = T;
	AVLCalcHeight(T);
	AVLCalcHeight(RNode);
	return RNode;
}

RR型处理源码

//RR型左旋处理
TreeNode* AVLTreeRR(TreeNode* T) {
	return LRotate(T);
}
(3)LR

LR,即往左子树的右子树插入一个结点。这种情况下,如果树出现了不平衡的情况的话,根结点的当前平衡因子一定是 +2。

假设以左子树的右子树T4结点为支点,对左子树进行一次左旋操作,得到如下图所示:

可以看到,经过一次左旋得到新树,形状和LL型一致,所以接下来再按照型LL处理,再右旋一次即可达到平衡状态

所以对于LR型的处理主要有两步:

(1)对树T的左子树进行左旋

(2)对树T进行右旋

LR型处理源码

//LR型左旋+右旋处理
TreeNode* AVLTreeLR(TreeNode* T) {
	T->left = LRotate(T->left);   //左旋处理并修改T的左指针指向
	return RRotate(T);    //对T进行右旋处理       
}
 (4)RL

RL,即往右子树的左子树插入一个结点。这种情况下,如果树出现了不平衡的情况的话,根结点的当前平衡因子一定是 -2。

假设以右的左子树T4结点为支点,对右子树进行一次右旋操作,得到如下图所示:

可以看到,经过一次右旋得到新树,形状和RR型一致,所以接下来再按照型RR型处理,再左旋一次即可达到平衡状态

所以对于RL型的处理主要有两步:

(1)对树T的右子树进行右旋

(2)对树T进行左旋

RL型处理源码

//RL型右旋+左旋处理
TreeNode* AVLTreeRL(TreeNode* T) {
	T->right = RRotate(T->right);    // 右子树进行右旋处理并修改T的右指针指向
	return LRotate(T);               // 对T树进行左旋处理
}

四、平衡二叉树基本操作

1、查找

(1)查找定值

对于要查找的数据 data,从根结点出发,每次选择左子树或者右子树进行查找, n 个结点的树高最多为\log {_{2}}^{n},所以查找的时间复杂度为 O(\log {_{2}}^{n}) ,总共四种情况依次进行判断:

1)若为空树,直接返回 false;

2) data 小于 树根结点的数据域,说明 data 对应的结点不在根结点,也不在右子树上,则递归返回左子树的 查找 结果;

3) data 大于 树根结点的数据域,说明 data 对应的结点不在根结点,也不在左子树上,则递归返回右子树的 查找 结果;

4) data 等于 树根结点的数据域,则直接返回 true ;

bool AVLFind(TreeNode* T, int data) {
	if (T == nullptr) {
		return false;                        // 空树
	}
	if (data < T->val) {
		return AVLFind(T->left, data);       // data<val,递归查找左子树
	}
	else if (data > T->val) {
		return AVLFind(T->right, data);      //  data>val,递归查找右子树
	}
	return true;                             //  data=val
}
(2)查找最小值结点

迭代找到树的最左结点即可。

//找最小值结点
TreeNode* AVLGetMin(TreeNode* T) {
	while (T && T->left)   
		T = T->left;       
	return T;              
}
(3)查找最大值

迭代找到树的最右结点即可。

//找最大值结点
TreeNode* AVLGetMax(TreeNode* T) {
	while (T && T->right)  
		T = T->right;      
	return T;              
}

2、平衡

每次当我们对树的结点进行插入或者删除的时候,都有可能打破树的平衡性,这时候就需要一些旋转操作来使树重新恢复平衡。 究竟是进行左旋,右旋,还是双旋,就要通过平衡因子来判断了。

令根结点为 T ,左子树的根结点为 L ,右子树的根结点为 R , \displaystyle T_{bf}代表根结点的平衡因子,\displaystyle L{_{bf}}代表左子树根的平衡因子, R_{bf}代表右子树根的平衡因子。总共分为以下四种情况:

1)  \displaystyle T_{bf} > 1 , \displaystyle L{_{bf}} > 0 ,则为 LL 型,需要进行一次右旋;

2)  \displaystyle T_{bf} > 1 , \displaystyle L{_{bf}} ≤ 0 ,则为 LR 型,需要进行一次双旋;

3)  \displaystyle T_{bf} < −1 , R_{bf} > 0 ,则为 RL 型,需要进行一次双旋;

4)  \displaystyle T_{bf} < −1 , R_{bf} ≤ 0 ,则为 RR 型,需要进行一次左旋

 平衡源码

//平衡选转
TreeNode* AVLBalance(TreeNode* T) {
	int bf = AVLGetBalanceFactor(T);

	if (bf > 1) {
		if (AVLGetBalanceFactor(T->left) > 0)
			T = AVLTreeLL(T);                 // LL型,右旋一次
		else
			T = AVLTreeLR(T);                 // LR型,左旋+右旋 
	}
	if (bf < -1) {
		if (AVLGetBalanceFactor(T->right) > 0)
			T = AVLTreeRL(T);                 // RL型,右旋+左旋 
		else
			T = AVLTreeRR(T);                 // RR型,左旋一次
	}
	AVLCalcHeight(T);                         // 重新计算根结点高度,因为之前旋转时并未完成相关操作
	return T;                                 
}

3、插入

对于要插入的数据 data ,从根结点出发,分情况依次判断:

1)若为空树,则创建一个值为 data 的结点并且返回;

2) data 的值 等于 树根结点的值,无须执行插入,直接返回根结点;

3) data 的值 小于 树根结点的值,那么插入位置一定在 左子树,递归执行插入左子树的过程,并且返回插入结果作为新的左子树

4) data 的值 大于 树根结点的值,那么插入位置一定在 右子树,递归执行插入右子树的过程,并且返回插入结果作为新的右子树

最后,在3或4情况执行完成后,需要对树执行 平衡 操作。

插入源码

TreeNode* AVLInsert(TreeNode* T, int data) {
	if (T == nullptr) {
		T = new TreeNode(data);               // 空树,创建val=data的结点
		return T;
	}

	if (data == T->val) {
		return T;                              // data已经存在
	}
	else if (data < T->val) {
		T->left = AVLInsert(T->left, data);    // 递归查找左子树适合位置,插入 
	}
	else {
		T->right = AVLInsert(T->right, data);  // 递归查找右子树适合位置,插入 
	}
	return AVLBalance(T);                      // 重新平衡 
}

4、删除

(1)删除根结点

对一棵平衡二叉树,删除它的根结点,需要保证它还是一棵二叉平衡树,则有如下四种情况:

1)空树,无须执行删除,直接返回空;

2)只有左子树时,将根结点空间释放后,返回左子树;

3)只有右子树时,将根结点空间释放后,返回右子树;

4)当左右子树都有时,根据左右子树的平衡性分情况讨论:如果左子树更高,则从左子树选择最大值替换根结点,并且递归删除左子树对应结点;右子树更高,则从右子树选择最小值替换根结点,并且递归删除右子树对应结点;

5)最后,重新计算所有树高,并且返回根结点;

//删除根结点
TreeNode* AVLRemoveRoot(TreeNode* T) {
	TreeNode* delNode = nullptr;
	TreeNode* retNode = nullptr;
	if (T == nullptr) {
		return nullptr;                 // 空树,直接返回 
	}

	if (T->right == nullptr) {    // 只有左子树(包含单节点情况),释放根结点空间后,返回左子树根结点
		retNode = T->left;
		delete T;
	}
	else if (T->left == nullptr) {    // 只有右子树,释放根结点空间后,返回右子树根结点 
		retNode = T->right;
		delete T;
	}
	else {								// 左右子树都存在 
		if (AVLGetHeight(T->left) > AVLGetHeight(T->right)) {  // 左子树高于右子树
			retNode = T;
			//获取左子树最大值结点,并以它的值作为根结点的新值
			TreeNode* cur = T->left;
			TreeNode* pcur = T;
			while (cur->right)
			{
				pcur = cur;
				cur = cur->right;
			}
			delNode = cur;
			retNode->val = cur->val;

			if (pcur->right == cur) {//左子树的最大值在左子树的右子树上
				pcur->right = cur->left;
			}
			else {//左子树的最大值为左子树的根
				pcur->left = cur->left;
			}
			delete delNode;

			
			retNode = AVLBalance(T);
			AVLCalcAllHeight(retNode);
			
		}
		else {   // 右子树高于左子树
			retNode = T;
			//获取右子树最小值结点,并以它的值作为根结点的新值
			TreeNode* cur = T->right;
			TreeNode* pcur = T;
			while (cur->left)
			{
				pcur = cur;
				cur = cur->left;
			}
			delNode = cur;
			retNode->val = cur->val;

			if (pcur->left == cur) {//右子树的最小值在右子树的左子树上
				pcur->left = cur->right;
			}
			else {//右子树的最小值为右子树的根
				pcur->right = cur->right;
			}
			delete delNode;

			retNode = AVLBalance(T);
			AVLCalcAllHeight(retNode);
		}
	}
	return retNode;
}
(2)删除指定结点

删除值为 data 的结点的过程,从根结点出发,总共四种情况依次判断:

1)空树,不存在结点,直接返回空 ;

2) data 的值 等于 树根结点的值,则调用 删除根结点 的接口,这个过程下文会详细介绍;

3) data 的值 小于 树根结点的值,则需要删除的结点一定不在右子树上,递归调用删除左子树的对应结点,并且将删除结点返回的子树作为新的左子树;

4) data 的值 大于 树根结点的值,则需要删除的结点一定不在左子树上,递归调用删除右子树的对应结点,并且将删除结点返回的子树作为新的右子树;

5)最后,对于 3) 和 4) 这两步,需要对树执行 平衡 操作。

TreeNode* AVLRemove(TreeNode* root,int val)
{
	if (nullptr == root) {
		return nullptr;
	}
	if (val == root->val) {
		return AVLRemoveRoot(root);
	}
	else if (val < root->val) {
		root->left = AVLRemove(root->left, val);
	}
	else if (val > root->val) {
		root->right = AVLRemove(root->right, val);
	}
	root = AVLBalance(root);
	AVLCalcAllHeight(root);
	return root;
}

五、平衡二叉树的缺点

由于AVL树必须保证左右子树平衡,Max(最大树高-最小树高) <= 1,所以在插入的时候很容易出现不平衡的情况,一旦这样,就需要进行旋转以求达到平衡。

正是由于这种严格的平衡条件,导致AVL需要花大量时间在调整上,故AVL树一般使用场景在于查询场景, 而不是 增加、删除频繁的场景。

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

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

相关文章

【送书活动六期】自我摸索:高质量分文章是如何优化出来的?

自从CSDN有了文章质量分后&#xff0c;大家都力争写出高分文章&#xff0c;尤其是23年年度博客之星的评选&#xff0c;入围标准之一就是文章质量分大于80&#xff0c;更驱使大家追逐高质量分的文章了&#xff0c;这里仅以个人经验&参考他人经验&#xff0c;总结一下。 目录…

.NET 8.0 本机 AOT

在软件开发领域&#xff0c;优化性能和简化效率仍然至关重要。.NET 平台二十年来不断创新&#xff0c;为开发人员提供了构建弹性且高效的软件解决方案的基础架构。 与本机 AOT&#xff08;提前&#xff09;编译相结合&#xff0c;取得了显着的进步。本文深入研究.NET Native AO…

2.HDFS 架构

目录 概述架构HDFS副本HDFS数据写入流程NN 工作原理DN 工作原理 结束 概述 官方文档快递 环境&#xff1a;hadoop 版本 3.3.6 相关文章速递 架构 HDFS HDFS 架构总结如下&#xff1a; a master/slave architecture 一主多从架构a file is split into one or more blocks a…

无法到达所选择的在线目标(博途PLC连接不上)

第1步&#xff1a;首先需要检查的就是PLC的物理连接了&#xff0c;可以利用PING工具测试下电脑和PLC是否在同一个网段&#xff0c; 第2步就是检查下防火墙设置 1、防火墙设置 2、关闭防火墙 未完....

MySQL InnoDB引擎

1、逻辑存储结构 2、架构 a. 内存结构 Change Buffer的意义是什么? 与聚集索引不同&#xff0c;二级索引通常是非唯一的&#xff0c;并且以相对随机的顺序插入二级索引。同样&#xff0c;删除和更新可能会影响索引树中不相邻的二级索引页&#xff0c;如果每一次都操作磁盘&am…

面试官:线程池的7种创建方式,你都清楚吗?

文章目录 前言1. 固定数量的线程池a. 线程池返回结果b. ⾃定义线程池名称或优先级 2. 带缓存的线程池3. 执⾏定时任务a. 延迟执行(一次)b. 固定频率执行c. scheduleAtFixedRate VS scheduleWithFixedDelay 4. 定时任务单线程5. 单线程线程池6. 根据当前CPU⽣成线程池 前言 线程…

不同阶数的巴特沃斯低通滤波器的空间域表示——数字图像处理

原理 巴特沃斯低通滤波器&#xff08;Butterworth Low-Pass Filter&#xff09;在频率域中的定义是明确的&#xff0c;但它在空间域中的表示不是直观的。这是因为巴特沃斯滤波器的形式是基于频率的&#xff0c;并且其空间域表示涉及到一个复杂的逆傅里叶变换&#xff0c;该变换…

一文搞懂Python Web开发 Django

简介 Django是一个主流的Python Web框架&#xff0c;用于快速开发 Web 应用程序。功能强大&#xff0c;Python Web应用开发的第一选择。 特点 ORM&#xff08;对象关系映射&#xff09;&#xff1a; Django 提供了一个强大的 ORM&#xff0c;允许开发者通过 Python 代码来定义…

C#设计模式之观察者模式

前言 观察者&#xff08;Observer)模式也称发布-订阅&#xff08;Publish-Subscribe&#xff09;模式&#xff0c;定义了对象间一种一对多的依赖关系&#xff0c;当一个对象的状态发生改变时&#xff0c;所有依赖于它的对象都得到通知并被自动更新。 观察者模式的图解如下所示…

使用 Kafka 和 CDC 将数据从 MongoDB Atlas 流式传输到 SingleStore Kai

SingleStore 提供了变更数据捕获 (CDC) 解决方案&#xff0c;可将数据从 MongoDB 流式传输到 SingleStore Kai。在本文中&#xff0c;我们将了解如何将 Apache Kafka 代理连接到 MongoDB Atlas&#xff0c;然后使用 CDC 解决方案将数据从 MongoDB Atlas 流式传输到 SingleStore…

JAVA基础学习笔记-day13-数据结构与集合源1

JAVA基础学习笔记-day13-数据结构与集合源1 1. 数据结构剖析1.1 研究对象一&#xff1a;数据间逻辑关系1.2 研究对象二&#xff1a;数据的存储结构&#xff08;或物理结构&#xff09;1.3 研究对象三&#xff1a;运算结构1.4 小结 2. 一维数组2.1 数组的特点 3. 链表3.1 链表的…

Linux之IP地址、主机名、域名解析

一、IP地址 可以通过ifconfig命令查看本机的ip地址&#xff0c;如果无法使用ifconfig命令&#xff0c;可以安装 安装&#xff1a;yum -y install net-tools ens33&#xff1a;主网卡&#xff0c;里面的inet就是ip地址 lo&#xff1a;本地回环网卡&#xff0c;127.0.0.1&…

Pytorch从零开始实战15

Pytorch从零开始实战——ResNeXt-50算法实战 本系列来源于365天深度学习训练营 原作者K同学 文章目录 Pytorch从零开始实战——ResNeXt-50算法实战环境准备数据集模型选择开始训练可视化总结 环境准备 本文基于Jupyter notebook&#xff0c;使用Python3.8&#xff0c;Pytor…

【计算机毕业设计】SSM医药信息管理系统

项目介绍 该系统共七个功能模块&#xff1a;查询模块、录入模块、删除模块、修改模块、浏览模块、打印模块和用户管理模块。 系统只有一个超级管理员&#xff0c;可以创建系统用户并进行权限管理&#xff0c;其他用户没有用户管理权限&#xff0c;只有其他权限。 不同的用户…

Jvm垃圾收集器系列之Parallel Scavenge收集器(个人见解仅供参考)

问&#xff1a;什么是Parallel Scavenge&#xff1f; 答&#xff1a;Parallel Scavenge是Java HotSpot虚拟机中的一种垃圾收集器&#xff0c;它主要用于提高应用程序的吞吐量。 问&#xff1a;Parallel Scavenge的主要目标是什么&#xff1f; 答&#xff1a;Parallel Scavenge的…

Debian12使用Xshell连接失败解决办法详细

1、Debian开启ssh服务 sudo apt update -y sudo apt install ssh2、编辑配置文件 # 安装vim sudo apt install vimvim /etc/ssh/sshd_config3、将#PermitRootLogin prohibit-password的注释去掉&#xff0c;设置为yes 4、将#PasswordAuthentication no的注释去掉&#xff0c;…

什么是DigiCert证书?

DigiCert作为全球知名的证书颁发机构&#xff0c;以其卓越的品质和全面的服务&#xff0c;为用户的数据安全保驾护航。 一、为何选择DigiCert证书&#xff1f; 权威认证&#xff1a;DigiCert与全球众多知名企业和政府机构合作&#xff0c;拥有广泛的认可度。高安全性&#xff…

太阳能杀虫灯的优点是什么

太阳能杀虫灯的优点主要包括以下几点&#xff1a; 环保节能&#xff1a;太阳能杀虫灯利用太阳能进行供电&#xff0c;无需接通市电&#xff0c;既节约能源又避免了排放污染物。适用范围广&#xff1a;只要有阳光照射的地区都可以使用太阳能杀虫灯&#xff0c;特别适合在电力资…

62.状态机实践(活动管理系统:二)

文章目录 一、简介二、状态机实践&#xff08;活动元信息管理&#xff09;1、dal/db.go2、dal/activity.go3、constdef/activity.go4、service/activity.go5、routes/routes.go6、main.go 代码地址&#xff1a;https://gitee.com/lymgoforIT/golang-trick/tree/master/37-load-…

详细解读QLC SSD无效编程问题-4

对于这些全部页面被无效化的WL&#xff0c;执行第二次编程实际上是不必要的&#xff0c;但当前的策略并未注意到这一问题。而对于那些既有有效页面又有无效页面&#xff08;图11中显示为1到3个&#xff09;的WL&#xff0c;应当被编程&#xff0c;但可以利用这些无效信息来改进…