数据结构初阶 · 二叉搜索树

news2024/11/17 14:38:22

目录

前言:

二叉搜索树的实现

二叉搜索树的基本结构

中序遍历


前言:

在最初学习二叉树的时候,就提及到过单独用树来存储数据是既不如链表也不如顺序表的,二叉树的用处可以用来排序,比如堆排序,也可以用来搜索数据,这是二叉树的用处,用来排序可以实现堆,用来搜索数据可以实现二叉搜索树,即今天实现的一种结构。

那么什么是二叉搜索树呢?

即左孩子比根小,右孩子比根大,且所有的子树都满足这个特点,这就是二叉搜索树,那么是如何实现搜索数据的呢?

搜索数据就是判断大小,最多走高度次个语句就可以找到数据了。

那么找数据的时间复杂度是不是O(logn)呢?很显然不是,万一存在只有左子树或者只有右子树有节点的树呢?那样的话时间复杂度就是O(N)了,所以时间复杂度是O(logN ~ N)。

话不多说,现在开始实现。

二叉搜索树的实现

二叉搜索树的基本结构

template <class T>
struct BSTreeNode
{
	BSTreeNode<T>* _left;
	BSTreeNode<T>* _right;
	T _key;

	BSTreeNode(const T& key)
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
	{}
};

template <class T>
class BSTree
{
public:
	typedef BSTreeNode<T> Node;

private:
	Node* _root = nullptr;

};

这是二叉搜索树的基本结构,每个节点都是一个结构体,好奇的人可能会问为什么值不是val而是key?这是因为二叉搜索树有两个模型,一个是key模型,一个是key-value模型,在key模型中,是不能修改数据的,因为一旦修改了数据整个树的结构就很容易被打乱,在key-value模型中,就可以修改数据,比如有一个数据集合,每个节点都有key和value,每存在一个key,value就++,所以key-value模型中能修改数据,但是修改的是value,即值出现的次数,总结就是能修改的数据就是对整棵树的结构没有影响的数据。

增的基本逻辑就是,如果比当前位置的值大,就走右子树,如果比当前位置的值小,就走左子树,如果该树是一个空树,那么这个值就充当根节点。当走到空了,我们就应该考虑连接的部分了,连接的时候,我们需要父节点,判断该值和父节点的大小,再使父节点的左右指针指向这个节点,既然需要父节点,我们这个时候就需要存储父节点的位置,每当走下个节点的时候,就存储一下父节点的位置,基本逻辑就这么多:

bool Insert(const T& val)
{
	if (_root == nullptr)
	{
		_root = new Node(val);
		return true;
	}
	Node* root = _root;
	Node* parent = nullptr;
	//判断部分
	while (root)
	{
		if (val > root->_key)
		{
			parent = root;
			root = root->_right;
		}
		else if (val < root->_key)
		{
			parent = root;
			root = root->_left;
		}
		else
		{
			return false;
		}
	}
	Node* newnode = new Node(val);
	//连接部分 开始判断大小关系
	if (parent->_key > val)
	{
		parent->_left = newnode;
	}
	else
	{
		parent->_right = newnode;
	}
	return true;
}

当然,为了方便,我们都写成了成员函数。

这里有个问题就是,如果存在两个相同的数据怎么办?

实际来说二叉搜索树是不允许存在相同的数据的,这样导致了数据冗余,就像字典里面,存在相同的两个单词吗?不会的是吧,所以我们就不考虑多种相同数据的情况,代码里面返回的就是false。

查就很简单了,查就是增的部分代码,遍历一遍,比较有没有这个值就行,遍历多简单,小就走左子树,大就走右子树,相等就返回true:

//查
Node* FindKey(const  T& val)
{
	Node* root = _root;
	while (root)
	{
		if (val > root->_key)
		{
			root = root->_right;
		}
		else if (val < root->_left)
		{
			root = root->_left;
		}
		else
		{
			return root;
		}
	}
	return nullptr;
}

中序遍历

数据加上了,也可以查数据了,我们现在想把数据打印出来看一下怎么办呢?这里推荐使用中序遍历,左子树根右子树这样的顺序打印,因为二叉搜索树的特性,这里打印出来就是升序,看着较为顺眼:

//中序遍历
void InOrder()
{
	_InOrder(_root);
	cout << endl;
}
private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}

感觉奇怪吧?为什么InOrder的参数不用Node* root呢?因为存在this指针,this指针是在参数的第一个位置,如果我们传参,传的是根,接受到的还是this指针,就冲突了,所以这里有几个办法,第一个是用一个get set函数,这个方法java比较喜欢使用,第二个方法就是设为私有,套一层函数使用吗,私有函数就没有this指针了。

到现在是不是都感觉二叉搜索树没啥?那是因为还没有到删除部分。删除部分才是二叉搜索树的核心。

给定一个二叉搜索树,删除可以分为以下几种情况,第一种情况是删除7 和 14,第二种情况是删除3 和 8。

第一种情况是属于可以直接删除的情况。

对于直接删除的情况,我们分为左右指针都为空,左指针为空,右指针为空的三种情况,实际上,我们可以只分为两种情况,第一种是左指针为空,第二种是右指针为空,比如7,删除7就是让6指向7的任意左右指针就可以了,删除14,我们需要让10的右指针指向13,有一个点就是为什么10指向的地方一定是比10大的?因为二叉树的特性,如果是9,就一定不会在10的下面。

我们可以总结以下,删除的时候,先判断是左为空还是右为空,然后判断子节点和父节点的位置,这样好让父节点指向下一个指针,连接的主要根据就是判断子节点和父节点相对位置。

如果两个都为空怎么办?我们已知一个节点不为空,另一个节点为不为空我们都指向它,总归是没错的。这点可以反证。当我们删除的是根节点的时候,只需要让根节点指向的内容是空就可以了,所以无论我们把删除根节点的位置放在左为空还是右为空都没问题。

到这里两个都为空的问题也就顺理成章的解决了,两个都为空,来就直接走左为空的场景,判断相对位置,父节点连接子节点的右节点,连接的是空指针,解决了就。

这部分的代码如下:

Node* parent = nullptr;
Node* cur = _root;
//先找到 找到该节点才能删除
while (cur)
{
	if (cur->_key < val)
	{
		parent = cur;
		cur = cur->_right;
	}
	else if (cur->_key > val)
	{
		parent = cur;
		cur = cur->_left;
	}
	//找到了 开始删除
	else
	{
		///第一种情况:左为空 -> 都为空
		if (cur->_left == nullptr)
		{
			//删除根节点的时候
			if (cur == _root)
			{
				cur = cur->_right;
			}
			else
			{
				if (cur == parent->_left)
				{
					parent->_left = cur->_right;
				}
				else
				{
					parent->_right = cur->_right;
				}
			}
			///第二种情况:右为空
			else if (cur->_right == nullptr)
			{
				if (cur == parent->_left)
				{
					parent->_left = cur->_left;
				}
				else
				{
					parent->_right = cur->_left;
				}
			}
		}
}

第二种情况是删除3 和 8 的情况,这种就要麻烦一点,删除的话得用替换法,因为我们没有办法之直接删除它,那么是怎么个替换法呢?我们从树里面找一个数据,满足大于该节点的左节点,小于该节点的右节点,就算是替换完成了。

那么从哪里找这种适配的数据呢?当然是从该节点的左右子树去找了,我们找右子树的最小值,或者是左子树的最大值都可以满足,右子树的最小值,即比右节点的值小,但是同时比左节点大,这就满足了,找到了该值之后,我们要做的是交换数据,交换了数据之后,我们应该怎么样删除右子树的最小值的节点呢?有人提议说用递归删除,比如删除3,用4进行替换,我们删除得先找到这个数据吧,关键问题是根本找不到这个数据,因为交换了数据之后树的结构算是被轻微破坏了,所以我们想要删除就让它的父节点指向空就可以了,此时也要判断一下相对位置即可,总体删除代码如下:

	bool EraseKey(const T& val)
	{
		Node* parent = nullptr;
		Node* cur = _root;
		//先找到 找到该节点才能删除
		while (cur)
		{
			if (cur->_key < val)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_key > val)
			{
				parent = cur;
				cur = cur->_left;
			}
			//找到了 开始删除
			else
			{
				///第一种情况:左为空 -> 都为空
				if (cur->_left == nullptr)
				{
					//删除根节点的时候
					if (cur == _root)
					{
						cur = cur->_right;
					}
					else
					{
						if (cur == parent->_left)
						{
							parent->_left = cur->_right;
						}
						else
						{
							parent->_right = cur->_right;
						}
					}
				}
				///第二种情况:右为空
				else if (cur->_right == nullptr)
				{
					if (cur == parent->_left)
					{
						parent->_left = cur->_left;
					}
					else
					{
						parent->_right = cur->_left;
					}
				}
				///左右都不为空 -> 替换法
				else
				{
					Node* rightMinParent = cur;
					Node* rightMin = cur->_right;
					//找右子树的最小值
					while (rightMin->_left)
					{
						rightMinParent = rightMin;
						rightMin = rightMin->_left;
					}
					swap(rightMin->_key, cur->_key);


					if (rightMinParent->_left == rightMin)
						rightMinParent->_left = rightMin->_right;
					else
						rightMinParent->_right = rightMin->_right;
				}
				return true;
			}
		}
		return false;
	}

最后父节点也可以直接指向空的,但是为了代码的美观性,这样写也不是不行。


感谢阅读!

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

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

相关文章

雷军的逆天改命与顺势而为

雷军年度演讲前&#xff0c;朋友李翔提了一个问题&#xff1a;雷军造车是属于顺势而为还是逆势而为&#xff1f;评论互动区有一个总结&#xff0c;很有意思&#xff0c;叫“顺势逆袭”。 大致意思是产业趋势下小米从手机到IOT再切入汽车&#xff0c;是战略的必然&#xff0c;不…

学习Java的日子 Day58 Servlet的生命周期,安全问题,页面跳转,中文乱码问题

Day58 1.Servlet的生命周期 创建&#xff1a;第一次发送给该Servlet请求时 ​ 调用&#xff1a;构造方法、init() 销毁&#xff1a;服务器正常关闭 ​ 调用&#xff1a;destroy() Welcome.html 没有明确写出是什么请求&#xff0c;那就是get请求 <!DOCTYPE html> <ht…

JavaWeb笔记_JSTL标签库JavaEE三层架构案例

一.JSTL标签库 1.1 JSTL概述 JSTL(jsp standard tag library):JSP标准标签库,它是针对EL表达式一个扩展,通过JSTL标签库与EL表达式结合可以完成更强大的功能 JSTL它是一种标签语言,JSTL不是JSP内置标签 JSTL标签库主要包含: ****核心标签 格式化标签 …

7月25日JavaSE学习笔记

线程的生命周期中&#xff0c;等待是主动的&#xff0c;阻塞是被动的 锁对象 创建锁对象&#xff0c;锁对象同一时间只允许一个线程进入 //创建锁对象Lock locknew ReentrantLock(true);//创建可重入锁 可重入锁&#xff1a;在嵌套代码块中&#xff0c;锁对象一样就可以直接…

分享几种电商平台商品数据的批量自动抓取方式

在当今数字化时代&#xff0c;电商平台作为商品交易的重要渠道&#xff0c;其数据对于商家、市场分析师及数据科学家来说具有极高的价值。批量自动抓取电商平台商品数据成为提升业务效率、优化市场策略的重要手段。本文将详细介绍几种主流的电商平台商品数据批量自动抓取方式&a…

PP 三 pp字段含义

单位&#xff1a;生产&#xff0c;销售&#xff0c;采购的单位&#xff0c;和基本单位会存在不一样的情况&#xff0c;所以要进行一个转换 产品组&#xff0c;普通项目类别组&#xff1a;销售来确定 跨工厂物料状态&#xff1a;如果在基本数据1里面&#xff0c;则是跨集团的&…

Kafka知识总结(分区机制+压缩机制+拦截器+副本机制)

文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 分区机制 分区策略 分区策略是决定生产者将消息发送到哪个分区的…

WPF---Prism视图传参

Prism视图传参方式。 实际应用场景 点击tabitem中的列表数据&#xff0c;同步更新到ListStatic Region对应的界面。目前用两种方式实现了传参数据同步。 第一&#xff0c;事件聚合器&#xff08;EventAggregator&#xff09; 1. 定义事件 创建一个事件类&#xff0c;用于传…

05 循环神经网络

目录 1. 基本概念 2. 简单循环网络 2.1 简单循环网络 2.2 长程依赖问题 3. 循环神经网络的模式与参数学习 3.1 循环神经网络的模式 3.2 参数学习 4. 基于门控的循环神经网络 4.1 长短期记忆网络 4.2 LSTM网络的变体网络 4.3 门控循环单元网络 5. 深层循环神经网络…

算法第十五天:leetcode19.删除链表的倒数第N个节点

一、删除链表的倒数第N个节点的题目描述与链接 19.删除链表的倒数第N个节点的链接如下表所示&#xff0c;您可直接复制下面网址进入力扣学习&#xff0c;在观看下面的内容之前您一定要先做一遍哦&#xff0c;以便让我印象更深刻&#xff01;&#xff01;!https://leetcode.cn/p…

数据结构和算法入门

1.了解数据结构和算法 1.1 二分查找 二分查找&#xff08;Binary Search&#xff09;是一种在有序数组中查找特定元素的搜索算法。它的基本思想是将数组分成两半&#xff0c;然后比较目标值与中间元素的大小关系&#xff0c;从而确定应该在左半部分还是右半部分继续查找。这个…

电离层——科普

电离层的发现 图1 电离层区域示意图 在地球上空大约60km至1000km范围内有一个特殊的区域。因为它的存在,使无线电通信成为现实,同时它又是GPS定位的捣乱鬼,它就是电离层。 电离层的发现 1901年,扎营守候在加拿大信号山的意大利科学家马可尼用风筝价高接收天线,接收到了从英格…

【Android】碎片—动态添加、创建Fragment生命周期、通信

简单用法 在一个活动中添加两个碎片&#xff0c;并让这两个碎片平分活动空间 先新建一个左侧碎片布局和一个右侧碎片布局 左侧碎片 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/…

智慧工地视频汇聚管理平台:打造现代化工程管理的全新视界

一、方案背景 科技高速发展的今天&#xff0c;工地施工已发生翻天覆地的变化&#xff0c;传统工地管理模式很容易造成工地管理混乱、安全事故、数据延迟等问题&#xff0c;人力资源的不足也进一步加剧了监管不到位的局面&#xff0c;严重影响了施工进度质量和安全。 视频监控…

LLM及GPT知识点

工欲善其事必先利其器&#xff0c;在了解大语言模型和GPT之前先要了解基本概念。 LLM Large Language Model (LLM) 即大型语言模型&#xff0c;也叫大语言模型&#xff0c;是一种基于深度学习的自然语言处理&#xff08;NLP&#xff09;模型&#xff0c;它能够学习自然语言的语…

【Django】django模板与前端技术(html模板)

文章目录 “python包html”还是“html包python”?1.新建模板2.模板语法3.views.py测试 “python包html”还是“html包python”? 在前端页面中html代码比python多得多&#xff0c;所以一定是html包python最优&#xff01;于是引出今天的模板。 大体分为三个步骤&#xff1a;…

【Python面试题收录】Python编程基础练习题②(数据类型+文件操作+时间操作)

本文所有代码打包在Gitee仓库中https://gitee.com/wx114/Python-Interview-Questions 一、数据类型 第一题 编写一个函数&#xff0c;实现&#xff1a;先去除左右空白符&#xff0c;自动检测输入的数据类型&#xff0c;如果是整数就转换成二进制形式并返回出结果&#xff1b…

什么是数据标注?

什么是数据标注&#xff1f; 数据标注是在原始数据上添加结构化信息的过程&#xff0c;这些信息通常以标签或元数据的形式存在&#xff0c;目的是让机器能够理解和“学习”数据的特征&#xff0c;从而提高算法的准确性和效率。 数据标注是机器学习和人工智能开发中不可或缺的一…

网络地址转换技术

一、实验日期与地址 1、实验日期&#xff1a;2024年xx月xx日 2、实验地址&#xff1a;xxx 二、实验目的 1、理解源NAT应用场景及原理&#xff1b; 2、掌握NAT Server的配置方法&#xff1b; 3、掌握NAT双出口的配置方法&#xff1b; 4、掌握域内NAT的配置方法。 三、实…

【C++】标准库类型vector

&#x1f984;个人主页:修修修也 &#x1f38f;所属专栏:C ⚙️操作环境:Visual Studio 2022 目录 vector对象集合简介 vector对象集合常用接口(成员函数) &#x1f4cc;vector对象集合模板默认成员函数 &#x1f38f;vector对象集合模板构造函数 &#x1f38f;vector对象…