C++二叉搜索树

news2024/11/26 2:45:44

C++二叉搜索树

  • 二叉搜索树概念
  • 二叉搜索树操作
    • 结点类的实现
    • 中序遍历实现
    • 二叉搜索树的插入
      • 非递归实现
      • 递归实现
    • 二叉搜索树的查找
      • 非递归实现
      • 递归实现
    • 二叉搜索树的删除
      • 非递归实现
      • 递归实现
    • 构造函数
    • 拷贝构造函数
    • 析构函数
    • 赋值运算符重载
  • 二叉搜索树的应用
  • 二叉搜索树的性能分析

二叉搜索树概念

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:

  1. 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  2. 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  3. 它的左右子树也分别为二叉搜索树

在这里插入图片描述

二叉搜索树操作

结点类的实现

为了方便二叉搜索树的实现,我们需要先实现一个节点类,它包含一个左指针,一个右指针,一个结点值。

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

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

中序遍历实现

为了方便我们随时可以检测我们我们实现的二叉搜索树是否有问题,我们需要实现一个中序遍历来将各结点值打印出来,以便保证二叉搜索树的正确性。

template<class K>
class BSTree
{
	typedef BSTreeNode Node;
public:
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}
private:
	Node* _root;
};

二叉搜索树的插入

插入的具体过程如下:

  1. 树为空,则直接新增节点,赋值给root指针
  2. 树不空,按二叉搜索树性质查找插入位置,插入新结点

树不为空,插入结点分为以下操作:

  • 待插入结点的值小于根节点的值,向左子树中插入
  • 待插入结点的值大于根节点的值,向右子树中插入
  • 待插入结点的值等于根节点的值,返回false

依次进行插入,直到找到空位置进行插入后者返回false。

非递归实现

我们需要记录根节点的位置,便于最后插入时将待插入节点与它的父节点连接起来,所以我们定义一个parent结点记录位置,在定义一个cur来遍历二叉搜索树:
在这里插入图片描述

代码实现:

bool Insert(const K& key)
{
	//如果此时插入的为根结点,直接创建新结点
	if (_root == nullptr)
	{
		_root = new Node(key);
		return true;
	}
	//定义一个父结点记录位置
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
		//插入值小于结点值,去左子树中找
		if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		//插入值大于结点值,去右子树中找
		else if(cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		//插入值等于结点值,返回false
		else
		{
			return false;
		}
	}
	//创建key所在结点
	cur = new Node(key);
	//此时如果key < parent->_key,就在左边插入
	if (parent->_key > key)
	{
		parent->_left = cur;
	}
	//反之,右边插入
	else
	{
		parent->_right = cur;
	}
	return true;
}

递归实现

插入的递归实现其实很简单,依然是小于根节点就去左边插入,大于根节点就去右边插入,但是需要注意的是结点的传递我们需要以引用的方式,因为最后我们是需要将插入结点与前面结点链接起来的:

bool _InSertR(Node*& root, const K& key)
{
	//为空树,创建一个根结点
	if (root == nullptr)
	{
		root = new Node(key);
		return true;
	}
	//key小于根结点的值,左子树中寻找
	if (root->_key > key)
	{
		return _InSertR(root->_left, key);
	}
	//key大于根结点的值,右子树中寻找
	else if(root->_key < key)
	{
		return _InSertR(root->_right, key);
	}
	//相等,返回false
	else
	{
		return false;
	}
}
//便于调用子函数进行插入
void InSertR(const K& key)
{
	_InSertR(_root, key);
}

二叉搜索树的查找

非递归实现

二叉搜索树非递归实现跟插入差不多,就是分别在左右子树中找,找到了就返回true,找不到就返回false:

//查找
bool Find(const K& key)
{
	Node* cur = _root;
	while (cur)
	{
		//查找值小于结点值,去左子树中找
		if (cur->_key > key)
		{
			cur = cur->_left;
		}
		//查找值大于结点值,去右子树中找
		else if (cur->_key < key)
		{
			cur = cur->_right;
		}
		//查找值等于结点值,返回true
		else
		{
			return true;
		}
	}
	return false;//没有找到,返回false
}

递归实现

bool _FindR(Node*& root, const K& key)
{
	//为空,就找不到,返回false
	if (root == nullptr)
	{
		return false;
	}
	//key小于根结点的值,左子树中寻找
	if (root->_key > key)
	{
		return _FindR(root->_left, key);
	}
	//key大于根结点的值,右子树中寻找
	else if (root->_key < key)
	{
		return _FindR(root->_right, key);
	}
	//相等,返回true
	else
	{
		return true;
	}

二叉搜索树的删除

删除的具体过程如下:

  1. 树为空,则不需要删除
  2. 树不空,按二叉搜索树性质查找删除位置

树不为空,存在以下三种情况:

  • 被删除结点左右都为空,此时只需要delete cur,然后让parent结点指向nullptr即可;
    在这里插入图片描述

  • 被删除的结点一边为空,此时就需要就需要判断被删除结点是parent结点的左孩子还是右孩子,
    然后在改变parent结点的指向;
    在这里插入图片描述
    上面两种情况如果我们此时删除的是根结点,我们只需要改变根节点的位置就可以了:
    在这里插入图片描述

  • 被删除的结点两边都不为空,此时就需要我们考虑用替换法来删除,我们可以考虑用被删除结点左子树的最大值或者是右子树的最小值来替换掉被删除结点,因为这样删除以后并不会破坏二叉搜索树的性质,我们以右子树的最小值来替换掉被删除结点为例,我们需要定义一个min结点来记录右子树的最小值,定义一个minParent记录min结点父节点位置。
    在这里插入图片描述
    在这里插入图片描述

非递归实现

//删除
bool Erase(const K& key)
{
	Node* parent = nullptr;
	Node* cur = _root;

	while (cur)
	{
		//key小于根结点的值,左子树中寻找
		if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		//key大于根结点的值,右子树中寻找
		else if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		//相等,进行删除
		else
		{
			//被删除结点左孩子为空
			if (cur->_left == nullptr)
			{
				//被删除结点为根节点
				if (cur == _root)
				{
					//改变根节点指向位置
					_root = cur->_left;
				}
				//被删除结点不为根节点
				else
				{
					//被删除结点为左孩子
					if (cur == parent->_left)
					{
						//父结点左指针指向被删除孩子右子树
						parent->_left = cur->_right;
					}
					//被删除结点为右孩子
					else
					{
						//父结点右指针指向被删除孩子右子树
						parent->_right = cur->_right;
					}
				}
				//删除该结点
				delete cur;
				cur = nullptr;
			}
			//被删除结点右孩子为空
			else if (cur->_right == nullptr)
			{
				//被删除结点为根节点
				if (cur == _root)
				{
					//改变根节点指向位置
					_root = cur->_left;
				}
				//被删除结点不为根节点
				else
				{
					//被删除结点为左孩子
					if (cur == parent->_left)
					{
						//父结点左指针指向被删除孩子左子树
						parent->_left = cur->_left;
					}
					//被删除结点为右孩子
					else
					{
						//父结点右指针指向被删除孩子左子树
						parent->_right = cur->_left;
					}
				}
				//删除该结点
				delete cur;
				cur = nullptr;
			}
			else
			{
				//记录待删除结点右子树当中值最小结点的父结点
				Node* minParent = cur;
				//记录待删除结点右子树当中值最小的结点
				Node* min = cur->_right;
				//寻找待删除结点右子树当中值最小的结点
				while (min->_left)
				{
					minParent = min;
					min = min->_left;
				}
				//交换待删除结点与min结点的值
				swap(cur->_key, min->_key);
				//此时minParent的左指针指向min
				if (minParent->_left == min)
				{
					//让minParent的左指针指向min的右子树
					minParent->_left = min->_right;
				}
				//此时minParent的右指针指向min
				else
				{
					//让minParent的右指针指向min的右子树
					minParent->_right = min->_right;
				}
				//删除掉min结点
				delete min;
				min = nullptr;
			}
			return true;
		}
	}
	return false;
}

递归实现

  • 若树为空树,则结点删除失败,返回false。

  • 若所给key值小于树根结点的值,则问题变为删除左子树当中值为key的结点。

  • 若所给key值大于树根结点的值,则问题变为删除右子树当中值为key的结点。

  • 若所给key值等于树根结点的值,则根据根结点左右子树的存在情况不同,进行不同的处理。

bool _EraseR(Node*& root, const K& key)
{
	//空树,找不到,返回false
	if (root == nullptr)
	{
		return false;
	}
	//key小于根结点的值,在左子树中去找
	if (root->_key > key)
	{
		return _EraseR(root->_left, key);
	}
	//key大于根结点的值,在右子树中去找
	else if (root->_key < key)
	{
		return _EraseR(root->_right, key);
	}
	//相等,进行删除操作
	else
	{
		Node* del = root;
		//待删除结点左子树为空
		if (root->_left == nullptr)
		{
			//根的右子树作为二叉树的新根节点
			root = root->_right;
		}
		//待删除结点右子树为空
		else if (root->_right == nullptr)
		{
			//根的左子树作为二叉树的新根节点
			root = root->_left;
		}
		else
		{
			Node* min = root->_right;
			//寻找待删除结点右子树当中值最小的结点
			while (min->_left)
			{
				min = min->_left;
			}
			//交换待删除结点与min结点的值
			swap(root->_key, min->_key);
			//递归进行删除
			return _EraseR(root->_right, key);
		}
		//释放掉待删除结点
		delete del;
		return true;
	}
}

构造函数

构造一个空树即可:

//构造函数
BSTree()
	:_root(nullptr)
{}

拷贝构造函数

我们需要注意点是二叉搜索树的拷贝属于深拷贝,我们需要创建一颗和被拷贝二叉搜索树相同的树:

//拷贝函数
Node* _Copy(Node* root)
{
	//为空树,直接返回空
	if (root == nullptr)
	{
		return nullptr;
	}
	//拷贝根结点
	Node* CopyRoot = new Node(root->_key);
	//向左递归拷贝左子树
	CopyRoot->_left = _Copy(root->_left);
	//向右递归拷贝右子树
	CopyRoot->_right = _Copy(root->_right);
	//返回拷贝的树
	return CopyRoot;
}
//拷贝构造函数
BSTree(const BSTree<K>& t)
{
	_root = _Copy(t._root);
}

析构函数

二叉搜索树析构函数就是将每个结点都释放掉,但需要注意的是我们这儿要后序遍历进行释放:

void _Destory(Node*& root)
{
	//为空树,返回上一级
	if (root == nullptr)
	{
		return;
	}
	//向左递归释放左子树
	_Destory(root->_left);
	//向右递归释放右子树
	_Destory(root->_right);
	//释放根节点
	delete root;
	//root置为nullptr
	root = nullptr;
}
//析构函数
~BSTree()
{
	_Destory(_root);
}

赋值运算符重载

我们使用现代写法来进行赋值运算符重载:

	//赋值运算符重载
	BSTree<K>& operator=(const BSTree<K> t)
	{
		swap(_root, t._root);
		return *this;
	}

二叉搜索树的应用

K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。

比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:

  • 以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
  • 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。

  • 比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;
  • 再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。

二叉搜索树的性能分析

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

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
在这里插入图片描述
对于有n个结点的二叉搜索树:

最优的情况下,二叉搜索树为完全二叉树,其平均比较次数为:log N;
最差的情况下,二叉搜索树退化为单支树,其平均比较次数为:N / 2;

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

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

相关文章

MySQL 日志系统

重要日志模块 日志文件bin logredo log**关于循环写入和擦除的checkpoint 规则**redo log 怎么刷入磁盘的 binlog 和 redo log 有什么区别&#xff1f;undo log 日志文件 错误日志&#xff08;error log&#xff09;&#xff1a; 错误日志文件对 MySQL 的启动、运行、关闭过程进…

【STM32】AFIO 以及重映射

在配置外部中断的时候&#xff0c;打开GPIO时钟的时候&#xff0c;也同时打开了AFIO的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD | RCC_APB2Periph_AFIO,ENABLE); AFIO 简单来说 MCU有对外管脚&#xff0c;包括CPU的管脚和内置外设&#xff08;PWM,TIM,ADC……&…

非计算机科班如何丝滑转码?(本人就是有点不丝滑)

我觉得无非三个办法可以选择(当然可能有其他方法) 自学 报班 有师傅带 但是在学习之前&#xff0c;你一定要明确你学习编程的目的是什么&#xff01; 游戏开发&#xff1f;后台研发&#xff1f;爬虫工程师&#xff1f;前端程序员?数据分析师&#xff1f; 或者 仅仅是想做一…

基于Spring Boot 的 Ext JS 应用框架之coworkee

Ext JS 官方提供了一个人员管理的完整应用框架 - coworkee。该框架的显示如下: 该框架的布局特点如下: 布局方式: 左右布局, 左侧导航栏默认收合特点:左侧导航区占用空间小, 工作区较大, 适合没有二级导航栏,工作区需要显示的内容较多的系统。如果导航栏是横向底部,就…

单值二叉树

目录 题目题目要求示例 解答方法一、实现思路时间复杂度和空间复杂度代码 方法二、实现思路时间复杂度和空间复杂度代码 题目 单值二叉树 题目要求 题目链接 示例 解答 方法一、 递归 实现思路 时间复杂度和空间复杂度 时间复杂度&#xff1a;O(N) 空间复杂度&#xf…

layui--记录

layui 行点击事件&#xff1a;点了没反应&#xff1f; //监听行工具事件layui.table.on(tool(demo), function (obj) {//alert(222) });原因&#xff1a;检查下id与lay-filter是否一致&#xff1b;id与lay-filter必须一致。 <table id"demo" lay-filter"dem…

适配器设计模式

目录 一、适配器模式1.类适配器模式2.对象适配器模式3.接口适配器 二、适配器模式应用场景三、适配器模式的优缺点 一、适配器模式 B站&#xff1a;java架构师 定义&#xff1a;适配器模式把一个类的接口变换成客户端所期待的另一种接口&#xff0c;从而使原本因接口不匹配而…

idea 打 jar 包以及运行使用

1. 在 idea 右侧点击 maven 2. 点击Lifecycle——》clean 运行 3. 点击 Lifecycle——》compile 4. 点击 Lifecycle——》package 5. 打成的 jar 包可以在 target中找到 6. jar 包的名字和版本可以在 pom.xml文件中设置 7. 注意事项&#xff1a;打 jar 包的时候 test 里的 tes…

【算法刷题-双指针篇】

目录 1.leetcode-27. 移除元素2.leetcode-344. 反转字符串3.leetcode-剑指 Offer 05. 替换空格4.leetcode-206. 反转链表5.leetcode-19. 删除链表的倒数第 N 个结点6.leetcode-面试题 02.07. 链表相交7.leetcode-142. 环形链表 II8.leetcode-15. 三数之和9.leetcode-18. 四数之…

PROFINET简介及其实现

PROFINET是一个开放式的工业以太网通讯协定&#xff0c;主要由西门子公司和PROFIBUS & PROFINET国际协会所提出。PROFINET应用TCP/IP及资讯科技的相关标准&#xff0c;是实时的工业以太网。自2003年起&#xff0c;PROFINET是IEC 61158及IEC 61784标准中的一部分。 三种通信…

Aspose导出word使用记录

背景&#xff1a;Aspose系列的控件&#xff0c;功能实现都比较强大&#xff0c;可以实现多样化的报表设计及输出。 通过这次业务机会&#xff0c;锂宝碳审核中业务功需要实现Word文档表格的动态导出功能&#xff0c;因此学习了相关内容&#xff0c;在学习和参考了官方API文档的…

解决legend数据过多,使用滚动,但进行后图形样式发生变化

前言&#xff1a; 滚动前&#xff1a; 滚动后&#xff1a; 滚动前后&#xff0c;饼状图中的内容除了“城市规划”和“城市管理部件”两个分类进行了位置的交换&#xff0c;没有其他的变化&#xff0c;数据也没有增加&#xff0c;但是&#xff0c;样式就是不知道为啥发生了变化。…

vue3实现日历日期选择(不使用任何插件,纯javaScript实现)

个人项目地址: SubTopH前端开发个人站 (自己开发的前端功能和UI组件,一些有趣的小功能,感兴趣的伙伴可以访问,欢迎提出更好的想法,私信沟通,网站属于静态页面) SubTopH前端开发个人站https://subtop.gitee.io/subtoph.github.io/#/home 以上 👆 是个人前端项目,欢…

【vue】this.$nextTick解决this.$refs undefined的问题

说明 1、发邮件页面分成两个部分&#xff1a;模态框页面&#xff08;头部和底部&#xff09;和form页面&#xff08;操作按钮&#xff09; 2、点击回复按钮&#xff0c;要将发件人信息带到模态框页面&#xff0c;给定默认值且禁止收件人下拉选择&#xff08;多个邮箱&#xff…

OpenGL精简案例一

文章目录 案例一 绘制点线面定义Renderer顶点着色器片段着色器内置的特殊变量 应用场景工具ShaderHelper工具 TextResourceReader效果图如下 结论 案例一 绘制点线面 定义Renderer import android.content.Context; import android.opengl.GLES20; import android.opengl.GLSu…

Quasi-eccentricity Error Modeling and Compensation in Vision Metrology

论文&#xff1a;Quasi-eccentricity Error Modeling and Compensation in Vision Metrology 中文&#xff1a;视觉计量中准偏心误差建模与补偿 论文地址&#xff1a;Sci-Hub | Quasi-eccentricity error modeling and compensation in vision metrology. Measurement Scienc…

ATA-L系列水声功率放大器——应用场景介绍

ATA-L系列是一款宽频带能输出较大功率的单通道放大器。最大输出1200Vrms电压&#xff0c;6500VA功率&#xff0c;可驱动0~100%的阻性或非阻性负载&#xff0c;客户可根据测试需求灵活调节。 图&#xff1a;ATA-L系列水声功率放大器 国产品牌安泰电子自主研发的ATA-L系列水声功率…

将 Llama2 中文模型接入 FastGPT,再将 FastGPT 接入任意 GPT 套壳应用,真刺激!

FastGPT 是一个基于 LLM 大语言模型的知识库问答系统&#xff0c;提供开箱即用的数据处理、模型调用等能力。同时可以通过 Flow 可视化进行工作流编排&#xff0c;从而实现复杂的问答场景&#xff01; Llama2 是Facebook 母公司 Meta 发布的开源可商用大模型&#xff0c;国内的…

安防视频监控/视频集中存储/云存储平台EasyCVR平台无法取消共享通道该如何解决?

视频汇聚/视频云存储/集中存储/视频监控管理平台EasyCVR能在复杂的网络环境中&#xff0c;将分散的各类视频资源进行统一汇聚、整合、集中管理&#xff0c;实现视频资源的鉴权管理、按需调阅、全网分发、云存储、智能分析等&#xff0c;视频智能分析平台EasyCVR融合性强、开放度…

〔019〕Stable Diffusion 之 单图中绘制多人分区域写提示词 篇

✨ 目录 &#x1f388; 下载区域绘制插件&#x1f388; 区域绘制使用&#x1f388; 参数讲解和基础使用&#x1f388; Lora 自组&#x1f388; Lora 自组的使用&#x1f388; 分区扩散&#x1f388; 分区域提示 &#x1f388; 下载区域绘制插件 在绘制图片时&#xff0c;经常绘…