二叉树详解(进阶)

news2024/11/13 17:55:39

目录

1. 二叉搜索树

1.1 基本概念

1.2 基本操作

1.3 性能分析

1.4 键值对 

2. AVL树和红黑树

2.1 AVL树

2.2 红黑树

3. 红黑树模拟实现STL中的map与set


1. 二叉搜索树

1.1 基本概念

  二叉搜索树(BST,Binary Search Tree)一颗二叉树,可以为空;如果不为空,满足以下性质:

        若它的左子树不为空,则左子树上所有节点的值都小于根节点的值

        若它的右子树不为空,则右子树上所有节点的值都大于根节点的值

        它的左右子树也都为二叉搜索树

  如下示例: 

   中序遍历为 升序。

   由上性质:二叉搜索树也称 二叉排序树或二叉查找树。

1.2 基本操作

  示例结构:

template<class T>
class BSTreeNode
{
	typedef BSTreeNode<T> Node;
public:
	BSTreeNode(const T& data)
		:_pleft(nullptr)
		,_pright(nullptr)
		,_data(data)
	{}

	Node* _pleft;
	Node* _pright;
	T _data;
};

template<class T>
class BSTree
{
	typedef BSTreeNode<T> Node;
public:
    //增删查改
    //......
private:
	Node* _root = nullptr;
};

   查找和插入:

        从根开始比较,比根大则往右边走查找,比根小则往左边走查找;

        最多查找高度次,走到空,还没找到,这个值不存在;

        如果这个值不存在,就可以进行插入了。

   示例代码:

//-----------------循环
//查找
bool find(const T& data)
{
	Node* cur = _root;
	while (cur)
	{
		if (data < cur->_data)				cur = cur->_pleft;
		else if (data > cur->_data)		cur = cur->_pright;
		else return true;
	}
	return false;
}


//插入
bool insert(const T& data)
{
	if (_root == nullptr)
	{
		_root = new Node(data);
		return true;
	}
	Node* cur = _root, *parent = nullptr;
	while (cur)
	{
		parent = cur;
		if (data > cur->_data)		cur = cur->_pright;
		else if (data < cur->_key)		cur = cur->_pleft;
		else return false;
	}
	Node* r = new Node(data);
		if (parent->_data > data)	parent->_pleft = r;
		else	parent->_pright = r;
	return true;
}

//-----------------递归
bool findR(const T& data)
{
	return _findR(_root, data);
}

bool insertR(const T& data)
{
	return _insertR(_root, data);
}
private:
	bool _findR(Node* root, const T& data)
	{
		if (root == nullptr)		return false;

		if (root->_data == data)		return true;
		else if (root->_data > data)	return _findR(root->_pleft, data);
		else return _findR(root->_pright, data);
	}

	bool _insertR(Node*& root, const T& data)
	{
		if (root == nullptr)
		{
			root = new Node(data);
			return true;
		}
		if (data < root->_data)
		{
			return _insertR(root->_pleft, data);
		}
		else if (data > root->_data)
		{
			return _insertR(root->_pright, data);
		}
		else
		{
			return false;
		}
	}

  删除: 

         情况一:没有孩子或只有一个孩子;删除该节点后,剩下的一个孩子(可为空)直接顶替已删除节点的逻辑位置。

        情况二:有两个孩子;替换法:左子树的最大节点(最右值)或 右子树的最小节点(最左值)与要删除节点 值交换,再删除(按情况一)。

  示例代码:

//删除
bool erease(const T& data)
{
	Node* cur = _root, *parent = nullptr;
	while (cur)
	{
		if (data < cur->_data)
		{
			parent = cur;
			cur = cur->_pleft;
		}
		else if (data > cur->_data)
		{
			parent = cur;
			cur = cur->_pright;
		}
		else
		{
			//找到了
			//情况1:没有孩子或只有一个孩子
			if (cur->_pleft == nullptr)
			{
				if (parent == nullptr)
				{
					_root = cur->_pright;
				}
				else
				{
					if (parent->_pleft == cur)	parent->_pleft = cur->_pright;
					else	parent->_pright = cur->_pright;
				}
				delete cur;
			}
			else if (cur->_pright == nullptr)
			{
				if (parent == nullptr)
				{
					_root = cur->_pleft;
				}
				else
				{
					if (parent->_pleft == cur)	parent->_pleft = cur->_pleft;
					else	parent->_pright = cur->_pleft;
				}
				delete cur;
			}
			//情况2:有两个孩子
			else
			{
				Node* right_min = cur->_pright, * right_min_parent = cur;
				while (right_min->_pleft)
				{
					right_min = right_min->_pleft;
				}
				swap(cur->_data, right_min->_data);

				if (right_min_parent->_pleft == right_min)
					right_min_parent->_pleft = right_min->_pright;
				else
					right_min_parent->_pright = right_min->_pright;

				delete right_min;
			}
			return true;
		}
	}
	return false;
}

//-------------------递归
bool ereaseR(const T& data)
{
	return _ereaseR(_root, data);
}

bool _ereaseR(Node*& root, const T& data)
{
	if (root == nullptr)
	{
		return false;
	}
	if (data < root->_data)
	{
		return _ereaseR(root->_pleft, data);
	}
	else if (data > root->_data)
	{
		return _ereaseR(root->_pright, data);
	}
	else
	{
		Node* tmp = root;
		//情况1:
		if (root->_pleft == nullptr)
		{
			root = root->_pright;
			delete tmp;
		}
		else if (root->_pright == nullptr)
		{
			root = root->_pleft;
			delete tmp;
		}
		else
		{
			//情况2:
			Node* right_min = root->_pright;
			while (right_min->_pleft)
			{
				right_min = right_min->_pleft;
			}
			std::swap(root->_data, right_min->_data);
			//递归可以控制删除的起点
			_ereaseR(root->_pright, data);
		}
	}
	return true;
}

1.3 性能分析

  插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能

  对有N个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二 叉搜索树的深度的函数,即结点越深,则比较次数越多。

  但对于同一个集合,如果各值插入的次序不同,可能得到不同结构的二叉搜索树: 

   最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树,图1),其比较次数的时间复杂度为logN以2为底。 

  最差情况下,二叉搜索树退化为单支树(或者类似单支,图2),其比较次数的时间复杂度为N。

  如果退化成单支树,二叉搜索树的性能就没有了,那如何改进可以避免这种情况呢?别急,我们接着往下看。

1.4 键值对 

  用来表示具有一 一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代 表键值,value表示与key对应的信息 ,即KV模型;

  比如:商场停车场的收费系统,每次进入车辆都是一个新节点,这个节点中有两个重要变量,其中一个变量可用 车牌号的唯一性 标识每一辆车,即做key值;另一个变量,即对应的value,可填入 入场时间;当车辆出停车场时,系统可根据车牌号key值,找到对应的value,和当前时间做差得到总停留时间,进而结合收费规则进行费用收取。

  如果,key值不设对应的value,就叫K模型;

  比如:进出小区的门卫系统,只有在该系统中找到进出者的信息,才能合规开闭门禁。

  现实生活中,类似的例子还有很多,就不一 一列举了,这里的重点是: 提高 抽象现实生活为模型化,即面向对象编程的能力。 

  现在,到你试着把上述参考代码修改为KV模型,并进行一些简单测试(比如:设计本汉译英的词典,字数统计等)。做完这个,接着往下看。 

2. AVL树和红黑树

2.1 AVL树

  在上面1.4所列举的例子中,系统如何对数据节点进行管理?——  数据结构,如vector, list等,都可以;

  所以,选哪个呢,或者说选择的标准是什么,也可以说管理的目的是什么?—— 数据结构只是管理的手段,目的是为用户提供 稳定,可靠,高效的使用体验,这才是选择的标准;拿着标准去衡量手段,就可以做出合理的选择和取舍了

  在1.4的例子中,查找效率决定整个系统的效率,而在目前大家清晰的数据结构中,二叉搜索树应该是最合适的了,但是依旧可能出现1.3中的单支树或近似单支树的问题;因此,两位俄罗斯的数学家G.M.Adelson-Velskii 和E.M.Landis在1962年发明了一种解决上述问题的方法:

    当向二叉搜索树中插入新结点后,如果能保证每个结点的左右 子树高度之差(平衡因子)的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。这样的二叉搜索树就叫做AVL树。

   如下示例:(本文出现的 平衡因子 均= 右树高度 - 左树高度)

  AVL树节点的定义在二叉搜索树节点定义的基础上增加了两个变量,一是节点指针变量_pparent指向父节点,另一个是整数平衡因子_bf。

  重点操作:AVL树的插入

  规则:

    左树增加节点,p的_bf--, 右树增加节点,p的_bf++;
    如果插入节点后,p的_bf == 0,说明没有影响p的整棵树的高度,也就没有影响p祖先节点的平衡因子,所以不需要往上进行调节;
    如果插入节点后,p的_bf == 1或-1,说明影响了p这棵树的高度,进而可能影响其祖先,此时循环往上调节平衡因子;
     当p的_bf == 2或-2时,需要旋转。 

   (p表示插入节点的父节点/祖先节点)

  旋转有四种情况,如下图: (h表示树的高度,红色表示插入位置,绿色表示平衡因子,蓝色表示旋转基点;选用哪种旋转的依据是:平衡因子的正负组合情况)

    1. 新节点插入较高左子树的左侧:右单旋

    2. 新节点插入较高右子树的右侧:左单旋 

    3. 新节点插入较高右子树的左侧:右左双旋(先右单旋,再左单旋) 

    4. 新节点插入较高左子树的右侧:左右双旋(先左单旋,再右单旋)  

    原理同上述点3,不再赘述。 

  至于删除操作, 也一样按照二叉搜索树的方式将节点删除,然后再更新平衡因子,旋转调整,与插入不同的是,最差情况要一直调整到根节点,较为复杂,留给大家自行思考吧!

  还有如何验证一颗树是不是AVL树等操作,上述一系列的完整参考代码可点击 AVLTree.h 前往我的Gitee仓库查看。

2.2 红黑树

   在介绍红黑树之前,我们先对AVL树的性能做简单分析:AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log_2 (N)但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时, 有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

  因此,红黑树诞生,它较AVL树,不追求绝对的平衡,其只需保证最长路径不超过最短路径的2倍,在经常进行增删的结构中有效减少了旋转调整的次数,提升了效率,结构更稳定。

  红黑树也是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或 Black,只要满足以下规则:

        1.根节点是黑色
        2. 红节点的左右孩子为黑色——>红节点不连续
        3. 每个节点到其叶子节点(这里指空节点)的黑色节点数目相等(空节点算黑色节点)

  就使:从根节点开始到叶子节点的最长路径节点个数 <= 最短路径节点个数 * 2

  如下图例: 

  和AVL树一样,这里也只深入探讨插入操作的调整旋转逻辑和参考实现代码,内容较多,请点击前往我的Gitee仓库查阅:调整旋转情况分类图示RBTree.jpg 

                                         参考代码RBTree.h 

                                         AVL树和RB树对比测试代码main.cpp 

3. 红黑树模拟实现STL中的map与set

  到这,请先熟悉STL中map与set的常见操作(点击直达查阅网页);接着强烈建议您试着自己实现几个常见操作,如插入(insert),查找(find),重载【】等;涉及到模板的使用和常见问题的解决,正向迭代器的实现,如何利用适配器的模式实现反向迭代器,仿函数的运用等一系列知识模块,是一次不可或缺的综合性测试!

  需要注意的是:STL明确规定,begin()与end()代表的是一段前闭后开的区间,而对红黑树进行中序遍历后, 可以得到一个有序的序列,因此:begin()可以放在红黑树中最小节点(即最左侧节点)的位置,end()放在最大节点(最右侧节点)的下一个位置,关键是最大节点的下一个位置在哪? 能否给成nullptr呢?答案是行不通的,因为对end()位置的迭代器进行 -- 操作,必须要能找最后一个元素,此处就不行,因此最好的方式是增加头节点并将end()放在头结点的位置,也是为了反向迭代器的实现(如下图例)。

  当然,道千遍万遍,不如你手动一遍!

  参考代码也给大家准备好了,点击查阅:my_map/set

  本篇分享到这就结束了,如果对大家有所帮助的话就是对小编最大的鼓励了。当然,您的三连也是小编坚持创作的不懈动力!

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

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

相关文章

记录一次安装Studio卸载后再次安装反复打不开的问题

先说问题表现&#xff0c;低版本的安装后点击没反应&#xff0c;高版本的报错&#xff0c;如下图&#xff0c;反复卸载安装都没有用&#xff0c;网上也找了各种彻底卸载安卓Studio的方法也不行 Error occurred during initialization of VMagent library failed Agent OnLoad:…

【C++】unordered_set 容器的最全解析(什么是unordered_set?unordered_set的常用接口有那些?)

目录 一、前言 二、预备知识 &#x1f4a2;关联式容器&#x1f4a2; &#x1f4a2;键值对&#x1f4a2; &#x1f4a2;哈希结构的关联式容器&#x1f4a2; 三、unordered_set 详解 &#x1f525;unordered_set 的介绍 &#x1f525;unordered_set 的构造 &am…

解除 Excel 表格的文档保护全攻略

在日常工作和学习中&#xff0c;我们可能会遇到 Excel 表格被保护无法编辑的情况。别担心&#xff0c;今天就为大家分享几种解除 Excel 表格文档保护的方法。 一、导入腾讯文档 可以将受保护的 Excel 表格上传到腾讯文档。在部分情况下&#xff0c;腾讯文档会尝试自动解除表…

零基础国产GD32单片机编程入门(九)低功耗模式实战含源码

文章目录 一.概要二.GD32单片机低功耗基本介绍三.GD32单片机待机模式介绍四.待机低功耗例程实验五.工程源代码下载六.小结 一.概要 在生活中通过关掉用电器可以实现省电节能的目的&#xff0c;同样的道理单片机也可以通过这种方法实现降低功耗。单片机是由许多部件组成&#x…

ruoyi-vue-plus服务端打包报错的问题

对idea不熟&#xff0c;不知道在哪里输入打包命令&#xff0c;只会用手点击进行打包&#xff0c;然后就报错了 官方文档给的打包命令是 mvn clean package -D maven.test.skiptrue -P prod 从命令中可以看到跳过了maven测试&#xff0c;那么就要设置idea打包时跳过测试&…

k8s单master多node环境搭建-k8s版本低于1.24,容器运行时为docker

k8s 1.20.6单master多node环境搭建 1.环境规划2.初始化服务器1&#xff09;配置主机名2&#xff09;设置IP为静态IP3&#xff09;关闭selinux4&#xff09;配置主机hosts文件5&#xff09;配置三台主机之间免密登录6&#xff09;关闭交换分区swap&#xff0c;提升性能7&#xf…

【Python基础】字符串类型

本文收录于 《Python编程入门》专栏&#xff0c;从零基础开始&#xff0c;分享一些Python编程基础知识&#xff0c;欢迎关注&#xff0c;谢谢&#xff01; 文章目录 一、前言二、Python 字符串类型2.1 Python访问字符串中的值2.2 Python 转义字符2.3 Python 字符串运算符2.4 Py…

Bluetooth: gatt profile

Gatt 主要是描述了attribute的排列方式&#xff1b; Attribute caching 这个机制允许client只搜索一次server即可&#xff0c;当重连后不需要再搜索直接使用之前的。如果server的服务发生了变化&#xff0c;需要通过 service change indication 告诉client&#xff1b; client…

网优学习干货:2.6G仿真操作(2)

导入仿真区域图层 建立仿真站点组 设置仿真任务-结果图层和楼宇仿真高度 仿真结果统计-结果图层渲染 仿真结果统计-结果导出 目录 导入天线文件-导入方法与覆盖仿真相同&#xff0c;但天线文件需要包含PDSCH波束文件 将Beamforming天线添加到基站 如果在步骤④中没有找到Beamfo…

Web自动化测试实战--博客系统

&#x1f3a5; 个人主页&#xff1a;Dikz12&#x1f525;个人专栏&#xff1a;测试&#x1f4d5;格言&#xff1a;吾愚多不敏&#xff0c;而愿加学欢迎大家&#x1f44d;点赞✍评论⭐收藏 目录 1.项目效果展示 2.编写web测试用例 3.自动化测试脚本开发 3.1创建空项目 引…

构建大师:深入理解Linux下的Make和Makefile

引言 在软件开发的世界里&#xff0c;构建过程是一项繁琐而重要的任务。无论是简单的脚本还是复杂的软件项目&#xff0c;都需要一种方式来自动化编译、链接以及测试等过程。在Linux环境下&#xff0c;Make工具和它的配置文件——Makefile&#xff0c;成为了许多开发者构建项目…

计算机硬件的组成

目录 前言 计算机系统组成 计算机硬件的组成 1、控制器 2、运算器 3、主存储器 4、辅助存储器 5、输入设备 6、输出设备 最后 前言 计算机已成为不可或缺的工具。无论是个人电脑还是服务器集群&#xff0c;其背后都是由一系列硬件组件协同工作的结果。 本文讲介绍计…

ssrf攻击本地fastcgi漏洞复现

目录 环境&#xff1a;UbuntuNginxphp 代码 开始测试 查看 环境搭建 环境&#xff1a;UbuntuNginxphp 代码 <?php highlight_file(__FILE__); $url $_GET[url]; $curl curl_init($url);curl_setopt($ch,CURLOPT_URL,$url); curl_setopt($curl, CURLOPT_HEADER, 0…

滚雪球学MyBatis-Plus(02):环境准备

环境准备 本地开发环境参考如下&#xff1a; 开发工具&#xff1a;IntelliJ IDEA 2021.3.2JDK版本&#xff1a; JDK 1.8Spring Boot版本&#xff1a;2.3.1.RELEASEMaven版本&#xff1a;Apache Maven 3.8.2MySQL&#xff1a;5.6 前言 在上期内容中&#xff0c;我们系统地介绍了…

【多线程】设计模式之单例模式

&#x1f490;个人主页&#xff1a;初晴~ &#x1f4da;相关专栏&#xff1a;多线程 / javaEE初阶 一、什么是设计模式 设计模式好⽐象棋中的 "棋谱". 红⽅当头炮, ⿊⽅⻢来跳. 针对红⽅的⼀些⾛法, ⿊⽅应招的时候有⼀些固定的套路. 按照套路来⾛局势就不会吃亏. …

【微服务】接口的幂等性怎么设计?

一、什么是幂等&#xff1f; 幂等性&#xff1a;短时间内&#xff0c;对于相同输入的请求&#xff0c;无论进行多少次重复操作&#xff0c;都应该和单次调用的结果一致。 二、幂等问题产生的原因是什么&#xff1f;(或者说为什么需要实现幂等性?) 1、前端重复提交 在用户注…

高频Postman接口测试面试题

一、Postman在工作中使用流程是什么样的&#xff1f; 新建集合管理根据接口所属的模块&#xff0c;在集合中不同模块下编写接口测试用例处理接口之间的数据关联操作添加环境变量在tests tab下中增加断言调试接口&#xff0c;确保接口能被正常调用批量运行用例或者导出通过Newm…

STM32H750VBT6烧录源码无反应的问题

当烧录后出现这种情况下&#xff0c;点击魔术棒里面 Linker,勾选第一个方框后再次烧录即可。

【机器学习】聚类算法的基本概念和实例代码以及局部度量学习的概念和实例代码

引言 聚类算法在许多领域都有广泛的应用&#xff0c;例如数据挖掘、生物信息学、图像处理等。 文章目录 引言一、聚类算法1.1 K-Means算法1.2 DBSCAN算法1.3 层次聚类&#xff08;Hierarchical Clustering&#xff09;算法1.4 高斯混合模型&#xff08;Gaussian Mixture Model&…

Python系统教程02

Python 中基本运算符的使用变量基本运算符和变量编写简单的 Python 程序 一、Python 中的加法、减法、乘法、除法、 1.1 Python 中的""运算符 "" 可以用来计算两个数的和 "" 可以用来拼接 运算符可以用来计算两个数的和运算符可以连接多个字符…