C++ 树进阶系列之平衡二叉查找树( AVL)的自平衡算法

news2024/10/10 14:24:40

1. 前言

树的深度与性能的关系。

二叉排序树上进行查找时,其时间复杂度理论上接近二分算法的时间复杂度O(logn)

但是,这里有一个问题,如果数列中的数字顺序不一样时,构建出来的二叉排序树的深度会有差异性,对最后评估时间性能会有影响。

如有数列 [36,45,67,28,20,40],其构建的二叉排序树如下图。

ts10_1.png

基于上面的树结构,查询任何一个结点的次数不会超过 3 次。

稍调整一下数列中数字的顺序 [20,28,36,40,45,67],由此构建出来的树结构会出现一边倒的现象,即增加了树的深度。此棵树的深度为6,最多查询次数是 6 次。

ts11.png

可知,二叉树上的查询时间与树的深度有关,所以,减少查找次数的最好办法,就是尽可能维护树左右子树之间的对称性,也就让其有平衡性。

什么是平衡二叉排序树?

所谓平衡二叉排序树,顾名思义,基于二叉排序树的基础之上,维护任一结点的左子树和右子树之间的深度之差不超过 1。把二叉树上任一结点的左子树深度减去右子树深度的值称为该结点的平衡因子

我们经常说的平衡树指AVL树,是Adelson-VelskiiLandis1962年提出的,它的定义如下:

  • 一颗空的二叉树就AVL树。
  • 如果T是一颗非空的二叉树,TLTR是其左子树和右子树,如果TLTRAVL树且|hL-hR|<=1,其中hL和hRTLTR的高。那么T树一定是平衡二叉树。

平衡树的平衡因子只可能是:

  • 0 :左、右子树深度一样。
  • 1:左子树深度大于右子树。
  • -1:左子树深度小于右子树。

如下图,就是平衡二叉排序树,根结点的左右子树深度相差为 0, 结点 28 的左右子树深度为 1,结点 45 的左右子树深度相差为 0

Tips: 叶结点的平衡因子为0

ts10.png

平衡树的意义何在?

平衡二叉树能保证在树上操作的时间复杂度始终为O(logn)

平衡二叉排序树本质还是二叉排序树,在此基础之上,其 API 多了维持平衡的算法。

2. 平衡算法

2.1 平衡二叉排序树的抽象数据结构

结点类:

#include <iostream>
using namespace std;
/*
*结点类
*/
template<typename T>
struct TreeNode {
	//结点上附加的值
	T value;
	//左子结点
	TreeNode<T>* leftChild;
	//右子结点
	TreeNode<T>* rightChild;
	//平衡因子,默认值为 0
	int  balance;
	//无参构造
	TreeNode() {
		this->leftChild=NULL;
		this->rightChild=NULL;
		this->balance=0;
	}
	//有参构造
	TreeNode(T value) {
		this->value=value;
		this->leftChild=NULL;
		this->rightChild=NULL;
		this->balance=0;
	}
};

二叉平衡排序树类: 主要强调维持自平衡的特征函数。

/*
*树类
*/
template<typename T>
class BalanceTree {
	private:
		//根结点
		TreeNode<T>* root;
	public:
		
		BalanceTree(T value) {
			this->root=new TreeNode<T>(value);
		}
		
		TreeNode<T>* getRoot(){
			return this->root;
		}

		/*
		*LL型调整
		*/
		TreeNode<T>* llRotate(TreeNode<T>* node);

		/*
		*RR 型调整
		*/
		TreeNode<T>* rrRotate(TreeNode<T>* node);

		/*
		*LR型调整
		*/
		TreeNode<T>* lrRotate(TreeNode<T>* node);

		/*
		*RL型调整
		*/
		TreeNode<T>* rlRotate(TreeNode<T>* node);

		/*
		*插入新结点
		*/
		void insert(T value);

		/*
		*中序遍历
		*/
		void inorderTraversal(TreeNode<T>* root);

		bool isEmpty() {
			return this->root==NULL;
		}
};

在插入或删除结点时,如果导致树结构发生了不平衡性,则需要调整让其达到平衡。这里的方案可以有 4种。

2.2 LL型调整(顺时针)

左边不平衡时,向右边旋转。

如下图所示,现在根结点 36 的平衡因子为 1

ts12.png

当插入值为 18 结点,定然是要作为结点 20 的左子结点,才能维持二叉排序树的有序性,但是破坏了根结点的平衡性。根结点的左子树深度变成 3,右子树深度为1,平衡性被打破,结点 36 的平衡因子变成了2

ts13.png

怎样旋转才能让树继续保持平衡?

旋转思路是既然左边不平衡,必然是左高右低,向右旋转(顺时针)方能维持平衡。

  • 让结点 28 成为新根结点,结点36成为结点28的左子结点(降维左子树)。

ts13_1.png

  • 新根结点的右子树29成为原根结点36的新左子结点。

ts13_2.png

  • 原根结点成为新根结点的右子树。

ts14.png

旋转后,树结构即满足了有序性,也满足了平衡性。

LL 旋转算法具体实现:

/*
*LL型调整
*/
template<typename T>
TreeNode<T>* BalanceTree<T>::llRotate(TreeNode<T>* parentRoot) {
	//原父结点的左子结点成为新父结点
	TreeNode<T>*  newparentRoot =parentRoot->leftChild;
	// 新父结点的右子结点成为原父结点的左子结点
	parentRoot->leftChild = newparentRoot->rightChild;
	// 原父结点成为新父结点的右子结点
	newparentRoot->rightChild =parentRoot;
	// 重置平衡因子
	parentRoot->balance = 0;
	newparentRoot->balance = 0;
	return newparentRoot;
}

2.3 RR 型调整(逆时针旋转)

RR旋转和 LL旋转的算法差不多,只是当右边不平衡时,向左边旋转。

如下图所示,结点 50 插入后,树的平衡性被打破。

ts15.png

这里使用左旋转(逆时针)方案。

  • 结点45成为新根结点,原根结点 36向左旋转,将成为根结点 45 的左子结点。

ts15_1.png

  • 先将结点45 原来的左子结点成为结点36的右子结点。

ts15_2.png

  • 再将原根结点作为新根结点的左子结点。逆时针旋转后,结点45的平衡因子为 0,结点36的平衡因子为0,结点 48 的平衡因子为 -1。树的有序性和平衡性得到保持。

ts16.png

RR 旋转算法具体实现:

/*
*RR 型调整
*/
template<typename T>
TreeNode<T>* BalanceTree<T>::rrRotate(TreeNode<T>* parentNode) {
	// 右子结点
	TreeNode<T>* newParentNode = parentNode->rightChild;
	parentNode->rightChild = newParentNode->leftChild;
	//原父结点成为新父结点的左子树
	newParentNode->leftChild = parentNode;
	// 重置平衡因子
	parentNode->balance = 0;
	newParentNode->balance = 0;
	return newParentNode;
}

2.4 LR型调整(先逆后顺)

如下图当插入结点 28 后,结点 36 的平衡因子变成 2,则可以使用 LR 旋转算法。

ts17.png

  • 以结点 29 作为新的根结点,结点27以结点29为旋转中心,逆时针旋转。

ts18.png

  • 结点36以结点29为旋转中心向顺时针旋转。

ts19.png

最后得到的树还是一棵二叉平衡排序树

LR 旋转算法实现:

/*
*LR型调整
*/
template<typename T>
TreeNode<T>* BalanceTree<T>::lrRotate(TreeNode<T>* p_node) {
	// 原根结点的左子结点
	TreeNode<T>* b = p_node->leftChild;
	//得到新的根结点
	TreeNode<T>* new_p_node = b->rightChild;
	//更新原根结点的左子结点
	p_node->leftChild = new_p_node->rightChild;
	b->rightChild = new_p_node->leftChild;
	//更新新根结点的左子结点
	new_p_node->leftChild = b;
	// 更新新根结点的右子结点
	new_p_node->rightChild = p_node;
	//重置平衡因子
	if (new_p_node->balance == 1) {
		p_node->balance = -1;
		b->balance = 0;
	} else if (new_p_node->balance == -1) {
		p_node->balance = 0;
		b->balance = 1;
	} else {
		p_node->balance = 0;
		b->balance = 0;
	}
	new_p_node->balance = 0;
	return new_p_node;
}

2.5 RL型调整

如下图插入结点39 后,整棵树的平衡打破,这时可以使用 RL 旋转算法进行调整。

ts20.png

  • 把结点40设置为新的根结点,结点45以结点 40 为中心点顺时针旋转,结点36逆时针旋转。

ts20_1.png

ts21.png

RL 算法具体实现:

/*
*RL型调整
*/
template<typename T>
TreeNode<T>* BalanceTree<T>::rlRotate(TreeNode<T>* p_node) {
	//原根结点的右子树
	TreeNode<T>* b = p_node->rightChild;
	//新根结点
	TreeNode<T>* new_p_node = b->leftChild;
	//更新右子树
	p_node->rightChild = new_p_node->leftChild;
	b->leftChild = new_p_node->rightChild;
	new_p_node->leftChild = p_node;
	new_p_node->rightChild = b;
	if (new_p_node->balance == 1) {
		p_node->balance = 0;
		b->balance = -1;
	} else if (new_p_node->balance == -1) {
		p_node->balance = 1;
		b->balance = 0;
	} else {
		p_node->balance = 0;
		b->balance = 0;
	}
	new_p_node->balance = 0;
	return new_p_node;
}

2.6 插入算法

编写完平衡算法后,就可以编写插入算法。在插入新结点时,需要检查是否破坏二叉平衡排序树的的平衡性,否则调用平衡算法。

当插入一个结点后,为了保持平衡,需要找到最小不平衡子树。

什么是最小不平衡子树?

指离插入结点最近,且平衡因子绝对值大于 1 的结点为根结点构成的子树。

如下图所示,树结构整体上是平衡的,但根结点的平衡因子是 1,其实是一个脆弱的临界值,插入或删除操作就有可能打破这个平衡因子。

ts12.png

如插入值为 20 的结点,因为小于根结点的值,必然会导致从插入位置一路向上,一直到根结点所有直接、间接父结点的平衡因子发生变化。此时,可以把根结点到插入的新结点之间的树称为最小不平衡子树

ts12_1.png

出现了最小不平衡树,就要考虑怎么旋转,方能维持平衡。

/*
*插入新结点
*/
template<typename T>
void BalanceTree<T>::insert(T value) {
	// 创建新结点
	TreeNode<T>* new_node =new TreeNode<T>(value);
	if (BalanceTree<T>::root==NULL) {
		//如果是空树
		BalanceTree<T>::root = new_node;
		return;
	}
	//初始设定根结点为最小平衡树
	TreeNode<T>* min_b = BalanceTree<T>::root;
	//存储前驱结点
	TreeNode<T>* f_node = NULL;
	//移动指针
	TreeNode<T>* move_node = this->root;
	TreeNode<T>* f_move_node = NULL;
	//查找
	while (move_node!=NULL) {
		if (move_node->value == value)
			//结点已经存在
			return;
		if (move_node->balance != 0) {
			// 记录最小不平衡子树
			min_b = move_node;
			//记录其前驱
			f_node = f_move_node;
		}
		//移动之前,记录前驱
		f_move_node = move_node;

		if (new_node->value < move_node->value)
		   //向左边移动	
            move_node = move_node->leftChild;
		else
            //向右边移动
			move_node = move_node->rightChild;
	}

	if (new_node->value < f_move_node->value)
        //插入在左边
		f_move_node->leftChild = new_node;
	else
        //插入在右边
		f_move_node->rightChild = new_node;
    
    //开始更新最小不平衡树上各父结点的平衡因子
	move_node = min_b;
	// 修改相关结点的平衡因子
	while (move_node != new_node) {
		if (new_node->value < move_node->value) {
			move_node->balance++;
			move_node = move_node->leftChild;
		} else {
			move_node->balance--;
			move_node = move_node->rightChild;
		}
	}

	if (min_b->balance > -2 && min_b->balance < 2)
		//插入结点后没有破坏平衡性
		return;

	TreeNode<T>* b=NULL;
	if (min_b->balance == 2) {
		b = min_b->leftChild;
		if (b->balance == 1)
            //打破平衡,且左边高
			move_node = BalanceTree<T>:: llRotate(min_b);
		else
            //打破平衡,右边高
			move_node = BalanceTree<T>::lrRotate(min_b);
	} else {
		b = min_b->rightChild;
		if (b->balance == 1)
			move_node = BalanceTree<T>::rlRotate(min_b);
		else
			move_node = BalanceTree<T>::rrRotate(min_b);
	}
	if (f_node==NULL)
		BalanceTree<T>::root = move_node;
	else if (f_node->leftChild == min_b)
		f_node->leftChild = move_node;
	else
		f_node->rightChild = move_node;
}

也可以在结点类中添加一个指向父指针的成员变量,插入数据后,由下向上查找且更新平衡因子。

中序遍历: 二叉平衡排序树本质还是二树排序树,使用中序遍历输出的数字应该是有序的。

/*
*中序遍历
*/
template<typename T>
void BalanceTree<T>::inorderTraversal(TreeNode<T>* root) {
	if (root==NULL)
		return;
	BalanceTree<T>::inorderTraversal(root->leftChild);
	cout<<root->value<<"->";
	BalanceTree<T>::inorderTraversal(root->rightChild);
}

测试代码。

int main(int argc, char** argv) {
    int	nums[] = {3, 12, 8, 10, 9, 1, 7};
    BalanceTree<int>* tree=new BalanceTree<int>(3);
    for (int i=1;i<sizeof(nums)/4;i++)
        tree->insert(nums[i]);
    // 中序遍历    
    tree->inorderTraversal(tree->getRoot());
	return 0;
}

输出结果:

ts22.png

3. 总结

利用二叉排序树的特性,可以实现动态查找。在添加、删除结点之后,理论上查找到某一个结点的时间复杂度与树的结点在树中的深度是相同的。

但是,在构建二叉排序树时,因原始数列中数字顺序的不同,则会影响二叉排序树的深度。

这里引用二叉平衡排序树,用来保持树的整体结构的平衡性,方能保证查询的时间复杂度为 Ologn(n 为结点的数量)。

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

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

相关文章

【Unity VR开发】结合VRTK4.0:抛物线

模块化开发&#xff08;抛物线&#xff09;&#xff1a; 当我们用久了一个固定的东西之后就不想去追求新的东西&#xff0c;其实这是一种懒惰&#xff0c;每个人都应该明白&#xff0c;每一天&#xff0c;这个时间都在发生着变化&#xff0c;不管是人、事、物&#xff0c;而我…

[Android开发练习3] 四季图的切换

前言 本题主要涉及到使用帧式布局&#xff0c;以及如何为组件设置点击的事件响应&#xff0c;包括获取事件源&#xff0c;设置事件监听器&#xff0c;在Activity文件中书写一般的业务逻辑代码。 文章目录 布局代码 text3_season.xml&#xff08;layout&#xff09; style.xm…

私有云OpenStack学习笔记1

私有云&#xff1a;自己组建私有云系统。 私有云系统有哪些呢&#xff1f; OpenStack、CloudStack、Eucalyptus、vCloud Director四大主流云平台。 云计算有极大的潜力提高效率&#xff0c;显著地节约成本&#xff0c;实现可升级的基础设施和高性能以及安全的数据存储。 Ope…

秋招备战——Java基础知识

垃圾回收&#xff0c;JVM常用参数 将内存中不再被使用的对象进行回收&#xff0c;GC中用于回收的方法称为收集器&#xff0c;由于GC需要消耗一些资源和时间&#xff0c;Java在对对象的生命周期特征进行分析后&#xff0c;按照新生代、老年代的方式来对对象进行收集&#xff0c…

2023情人节送另一半什么好?四款适合送女友的数码好物分享

2023年情人节快到了&#xff0c;一个关于浪漫、惊喜并且充满爱意的节日&#xff0c;不少人都在烦恼送另一半什么好。而数码产品在日常生活中也是必不可少的存在&#xff0c;在此&#xff0c;我给大家整理了四款适合送女友的数码好物&#xff0c;一起来看看吧。 一、蓝牙耳机&a…

操作系统权限提升(四)之系统错误配置-Tusted Service Paths提权

系列文章 操作系统权限提升(一)之操作系统权限介绍 操作系统权限提升(二)之常见提权的环境介绍 操作系统权限提升(三)之Windows系统内核溢出漏洞提权 注&#xff1a;阅读本编文章前&#xff0c;请先阅读系列文章&#xff0c;以免造成看不懂的情况&#xff01;&#xff01;&am…

这18张 Python 数据科学速查表,让你的代码能力飞起来

数据科学是利用科学方法、流程、算法和系统从数据中提取价值的跨学科领域。数据科学家综合利用一系列技能&#xff08;包括统计学、计算机科学和业务知识&#xff09;来分析从网络、智能手机、客户、传感器和其他来源收集的数据。 目前在主流的数据科学领域一般有三大生态&…

深入详解 二次移动平均法python

什么是 二次移动平均法 二次移动平均法&#xff0c;也称为指数加权移动平均法&#xff0c;是一种用于平滑时间序列数据的算法。 文章目录二次移动平均法逻辑Python代码实现第二种实现二次移动平均法的方式第三种卷积实现二次移动平均法二次移动平均法的应用场景二次移动平均法逻…

Linux监控之prometheus学习

目录 需要学习以及掌握的知识 第一步&#xff1a;安装准备 1、安装nginx 2、安装prometheus 2_1、解压&#xff1a;prometheus-2.0.0.linux-amd64.tar.gz 2_2、移动到安装目录 2_3、将Prometheus配置为系统服务 2_4、重新加载系统文件&#xff0c;使prometheus.service文…

Linux字符设备、块设备的区别

一、字符设备 字符设备就是在对某设备进行操作时&#xff0c;该设备的读取以字节为单位进行。字符设备的操作是通过linux系统直接调用驱动程序完成的&#xff0c;在驱动程序的上一层并没用文件系统。因此字符设备的特点如下&#xff1a; 1、以字节流的方式进行读写、一个字节一…

亚马逊上线优惠券推荐功能,如何选品成为重中之重?

随着全球数字经济的进一步发展以及中国数字化基础设施的完善&#xff0c;众多中国品牌选择出海掘金道路。虽然全球经济格局的不确定因素在增多&#xff0c;但是总体上各国消费潜力在逐步释放&#xff0c;我国外贸行业也在不断迭代优化&#xff0c;尤其是出口跨境电商行业&#…

Python 基础语法介绍(二)

文章目录一、概述二、函数1&#xff09;函数定义2&#xff09;函数调用3&#xff09;函数传参1、形式参数和实际参数2、关键字参数3、参数默认值4、可变参数4&#xff09;函数返回值1、语法结构2、多值返回5&#xff09;变量作用域1、局部变量2、全局变量【1】在主程序中定义全…

金融风控12

社交网络分析与金融反欺诈 设备指纹 一般是基于某些设备信息&#xff0c;通过一些设备指纹算法将这些信息组合&#xff0c;通过特定hsah算法得到一个ID值&#xff0c;作为该设备唯一标识符 常见元素有&#xff1a; - sim卡信息 - wifi信息 - 硬盘信息 - 内存信息 - 屏幕…

【Qt】6.QTableWidget控件、其他控件、自定义控件封装、鼠标事件

目录 QTableWidget控件 代码 widget.cpp 结果 其他控件 stackWidget栈控件 下拉框 代码 widget.cpp 结果 自定义控件封装 代码 smallwidget.h smallwidget.cpp widget.cpp 结果 鼠标事件 代码 mylabel.h mylabel.cpp 结果 QTableWidget控件 设置列数setC…

oAuth2的入门

目录一、OAuth2流程演示示例第一步第二步二、流程2.1 资源所有者2.2 客户2.3 客户2.4 认证服务器2.5 客户2.6 资源服务器三、测试一、OAuth2流程演示示例 第一步 先到gitee下载oAuth2官方提供的代码&#xff0c;然后导入项目 https://gitee.com/lisenaq/oauth2-example.git第…

SpringMVC(十四):SpringMVC异常处理

文章目录 SpringMVC异常处理 一、异常简介 二、异常处理具体实现

Charles 的简单使用

1.下载并安装charles官方下载地址&#xff1a;https://www.charlesproxy.com/download/当前最新版本是4.6.3 选择合适的安装包进行下载&#xff08;windows.msi、macos.dmg、linux.tar.gz&#xff09;例windows.msi&#xff1a;执行msi文件&#xff0c;选择合适的下载地址&…

【机器学习】聚类算法(理论)

聚类算法&#xff08;理论&#xff09; 目录一、概论1、聚类算法的分类2、欧氏空间的引入二、K-Means算法1、算法思路2、算法总结三、DBSCAN算法1、相关概念2、算法思路3、算法总结四、实战部分...一、概论 聚类分析&#xff0c;即聚类&#xff08;Clustering&#xff09;&…

js继承的6种方式

// 1 原型链继承function Per() {this.name "key";}Per.prototype new Person(); // 主要var per1 new Per();console.log(per1.age);// instanceof 判断元素是否在其他元素的原型链上// per1继承了Person的属性&#xff0c;返回trueconsole.log(Per1 instanceof …

java常用类:BigInteger类和BigDecimal类

java常用类型: Ineteger等包装类 String类&#xff0c;StringBuffer类和StringBuilder类 Math类及常用方法 System类及常用方法 Arrays类及常用方法 BigInteger类和BigDecimal类及常用方法 日期类Date类,Calender类和LocalDateTime类 文章目录引言BigInteger (大整数)常用方法B…