二叉搜索树(查找、插入、删除的讲解实现+图文并茂)

目录

1. 二叉搜索树(BST)

  1.1 二叉搜索树概念

  1.2 二叉搜索树操作

        1.2.1 二叉搜索树的查找

        1.2.2 二叉搜索树的插入 

        1.2.3 二叉搜索树的删除

2. 二叉搜索树的实现

  2.1BST基本结构

  2.2 BST操作成员函数(非递归)

  2.3 BST操作成员函数(递归)

3. 二叉搜索树的应用

4. 二叉搜索树的性能分析


1. 二叉搜索树(BST)

  1.1 二叉搜索树概念

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

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

我举几个例子,更直观的看清楚结构:

 

  1.2 二叉搜索树操作

int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};

        1.2.1 二叉搜索树的查找

  • 从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
  • 最多查找高度次,走到空,还没找到,则这个值不存在。

        1.2.2 二叉搜索树的插入 

插入的具体过程如下:

  • 树为空,则直接新增节点,赋值给root指针
  • 树不为空,按二叉搜索树性质查找插入的位置,插入新节点(记录父节点,判断插入的节点应该在父节点的左子树还是右子树) 

        1.2.3 二叉搜索树的删除

首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情
况:
        a. 要删除的结点无孩子结点
        b. 要删除的结点只有左孩子结点
        c. 要删除的结点只有右孩子结点
        d. 要删除的结点有左、右孩子结点

看似删除节点有4种情况,但实际上a和b和c可以合并,这样就只有2种情况了:

        a:待删除的结点无孩子/只有一个孩子:删除结点并使父亲结点指向被删除结点的孩子结点(无孩子视为孩子是空结点,任意指向一个即可)

        b:待删除的结点有左右孩子:采用替换法,寻找删除结点右子树的最小结点(右子树最左结点),将最小结点的值和删除结点的值替换,然后删除最小结点(此时最小结点,要么没有孩子,要么只有一个孩子,符合a情况可以直接删除)

2. 二叉搜索树的实现

  2.1BST基本结构


结点:

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

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

BST树:

template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
    //成员函数
private:
    Node* _root=nullptr;
};

查找:

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

	return false;
}

  2.2 BST操作成员函数(非递归)


插入:

bool Insert(const K& key)
{
    //树为空,则直接新增结点,赋值给_root指针
	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->_right;
		}
		else if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
        //已有结点,不需要再插入
		else
		{
			return false;
		}
	}

	cur = new Node(key);
    //判断是插入父结点的左部还是右部
	if (parent->_key < key)
	{
		parent->_right = cur;
	}
	else
	{
		parent->_left = cur;
	}

	return true;
}

删除:

bool Erase(const K& key)
{
	Node* parent = nullptr;
	Node* cur = _root;
    //查找是否有待删除的节点
	while (cur)
	{
		if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else
		{
			// 开始删除
			// 1、左为空
			// 2、右为空
			// 3、左右都不为空
			if (cur->_left == nullptr)
			{
                //判断下当前节点是否是_root,若是,无法用parent(当前为nullptr,防止野指针错误)
				if (cur == _root)
				{
					_root = cur->_right;
				}
				else
				{
					if (cur == parent->_left)
					{
						parent->_left = cur->_right;
					}
					else
					{
						parent->_right = cur->_right;
					}
				}

				delete cur;
				cur = nullptr;
			}
			else if (cur->_right == nullptr)
			{
				if (_root == cur)
				{
					_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;
				}
				swap(cur->_key, min->_key);
				//min在父的左孩子上
				if (minParent->_left == min)
					//万一最左节点还有右孩子节点,或者是叶子也直接指右为空
					minParent->_left = min->_right;
				//min在父的右孩子上(待删除节点在根节点,最左节点为根节点的右孩子)
				else
					minParent->_right = min->_right;
				delete min;
				min == nullptr;
			}
			return true;
		}
	}
	return false;
}

其他成员函数这里就不展示了,这里再说一个小tips:

default:强制编译器生成默认的构造——C++11的用法

BSTree()=default;

  2.3 BST操作成员函数(递归)

还是递归香,看懂了上面的非递归那么就可以改造成递归了。 


查找:

bool _FindR(Node*& root, const K& key)
{
	if (root == nullptr)
		return false;
	if (root->_key < key)
	{
		return _FindR(root->_right, key);
	}
	else if (root->_key > key)
	{
		return _FindR(root->_left, key);
	}
	else
	{
		return true;
	}
}

插入:

bool _InsertR(Node*& root, const K& key)
{
	if (root == nullptr)
	{
		root = new Node(key);
		return true;
	}

	if (root->_key < key)
		return _InsertR(root->_right, key);
	else if (root->_key > key)
		return _InsertR(root->_left, key);
	else
		return false;
}

删除:

bool _EraseR(Node* root, const K& key)
{
	Node* del = root;
	if (root == nullptr)
		return false;
	if (root->_key < key)
		return _EraseR(root->_right, key);
	else if (root->_key > key)
		return _EraseR(root->_left, key);
	else
	{
		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;
			}
			swap(root->_key, min->_key);
			//交换后结构改变不是搜索二叉树了,规定范围在右树(因为是右树最左节点替换)再递归
			return _EraseR(root->_right, key); 
		}
		delete del;
		return true;
				
	}

}

3. 二叉搜索树的应用

1. K模型 :K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
2. KV模型 :每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方
式在现实生活中非常常见:
比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;
再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出 现次数就是<word, count>就构成一种键值对。

4. 二叉搜索树的性能分析

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

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

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

最优情况下:二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:log(N)

最差情况下:二叉搜索树退化为单支树(或者类似单支),其平均比较次数为 N

 如果退化为单支树,二叉搜索树的性能就失去了。那能否进行改进?无论按照什么次序插入关键码,都能达到最优?这就需要AVL树和红黑树了。

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

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

相关文章

一起学SF框架系列4.6-模块context-AbstractApplicationContext

org.springframework.context.ApplicationContext接口表示Spring IoC容器&#xff0c;负责实例化、配置和组装bean。容器通过读取配置元数据来获取关于实例化、配置和组装哪些对象的指令。配置元数据以XML、Java注释或Java代码表示。它允许您表达组成应用程序的对象以及这些对象…

在vite或者vue-cli中使用.env[mode]环境变量

在项目中总会遇到一些默认的配置,需要我们配置到静态文件中方便我们去获取,这时候就可以用到这个.env环境变量文件,在cli创建的项目中顶层的nodejs会有一个process对象,这个对象可以根据不同的环境获取不同的环境配置文件,但是vite中获取变量的方式不一样。 创建变量文件.env.…

NetApp AFF C 系列——可持续、可扩展且安全可靠的全闪存解决方案

NetApp AFF C 系列 采用全新的闪存技术&#xff0c;同时辅以智能科技加持&#xff0c;将为您带来一个更为经济实惠的全闪存解决方案&#xff0c;它重新定义了安全性、可扩展性和可持续性。 为什么选择 AFF C 系列的新一代全闪存解决方案&#xff1f; 实现现代化&#xff0c;打…

使用APIPOST 进行压力测试

使用APIPOST 进行压力测试 目录概述需求&#xff1a; 设计思路实现思路分析1.apipost 压力测试 参考资料和推荐阅读 Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c;skip hardness,make a better result,wait for c…

模拟集成电路设计-MOS器件物理基础(模集系列持续更新)

学习目的 欠缺的学习路径&#xff1a; 固体物理&#xff0c;半导体器件物理&#xff0c;器件模型&#xff0c;电路设计。所有的半导体器件都看成一个黑盒子&#xff0c;只关注端电压电流。 最佳的学习路径&#xff1a;两手抓&#xff0c;因为有些二阶效应会影响到电路本身. 本…

《Spring Guides系列学习》guide66 - guide68及小结

要想全面快速学习Spring的内容&#xff0c;最好的方法肯定是先去Spring官网去查阅文档&#xff0c;在Spring官网中找到了适合新手了解的官网Guides&#xff0c;一共68篇&#xff0c;打算全部过一遍&#xff0c;能尽量全面的了解Spring框架的每个特性和功能。 接着上篇看过的gui…

Tatuk GIS Developer Kernel for .NET 11.77 Crack

Tatuk GIS Developer Kernel for .NET 是一个变体&#xff0c;它是受控代码和 .NET GIS SDK&#xff0c;用于为用户 Windows 操作系统创建 GIS 专业软件的过程。它被认为是一个完全用于 Win Forms 的 .NET CIL&#xff0c;WPF 的框架是为 C# 以及 VB.NET、VC、oxygen 以及最终与…

netty学习第一课

技术主题 Netty是一个基于Java NIO&#xff08;非阻塞 I/O&#xff09;框架的网络编程框架。它提供了一系列的高级网络编程API&#xff0c;使得开发者可以非常容易地实现高性能、高可靠性的网络应用。Netty具有非常好的可扩展性和灵活性&#xff0c;能够很好地支持多种协议和数…

数字图像处理①基于ADMM的全变分去噪算法

文章目录 1. Problem2. 仿真结果3. MATLAB算法4. 源码地址参考文献 1. Problem 在图像处理中&#xff0c;图像信号总会因为各种原因受到噪声的干扰&#xff0c;其中高斯噪声就是典型的干扰类型之一。 针对图像去噪的模型有很多种&#xff0c;其中全变分模型被认为是最有效的模…

linux中断

一 Linux中断原理 Linux中断&#xff08;Interrupt&#xff09;是指在计算机执行过程中&#xff0c;由于某些事件发生&#xff08;例如硬件请求、错误、异常等&#xff09;&#xff0c;CPU暂停当前正在执行的程序&#xff0c;转而执行相应的处理程序的过程。中断是计算机多任务…

Flask+表格静态展示

Python网页开发&#xff08;持续更新ing…&#xff09; 诸神缄默不语-个人CSDN博文目录 本文的需求场景是&#xff1a;我现在有一个JSON格式的表格&#xff0c;这个具体格式不重要相信你们能看懂其他格式的表格怎么改。总之我想用PythonFlask提取这个表格&#xff0c;并展示在…

【网络编程一】初识网络:IP与端口号 网络模型

目录 &#x1f31f;需要知道 一、基础概念 &#x1f308;1、IP地址与端口号 &#x1f308;2、五元组 二、协议分层 &#x1f308;1、OSI七层网络网络模型 &#x1f308;2、TCP/IP五层(四层)模型 &#x1f308;3、封装和分用&#xff08;重点&#xff01;&#xff09; &…

软件测试基础篇

文章目录 一、软件测试的生命周期二、BUGBUG的描述BUG的级别BUG生命周期产生争执怎么办?如何开始第一次测试测试的执行和BUG的管理 一、软件测试的生命周期 软件测试的生命周期&#xff1a; 1.需求分析&#xff1a;需求是否完整&#xff0c;需求是否正确 2.测试计划&#xff…

串口屏-迪文10寸T5串口屏数据交互

效果演示 为了便于理解 建议先看上篇博客 点击跳转到上一篇博客 正式开始 1 打开DGUS 2 如图点击文本显示 数据变量 3 填写数据地址 按步骤操作 3-1 先点击框选1处 3-2 再点击框选2处改地址 我改的1000 3-3 设置完直接导出 插入U盘替换DWSET文件夹文件(这一步不理解去看上一…

C++ set类成员函数介绍 (set和multiset)

目录 &#x1f914;set模板介绍&#xff1a; &#x1f914;特点&#xff1a; &#x1f914;set的成员函数&#xff1a; &#x1f60a;set构造函数&#xff1a; &#x1f50d;代码实例&#xff1a; &#x1f50d;运行结果&#xff1a; &#x1f60a; set赋值函数&#xf…

Linux——线程的同步与互斥

目录 模拟抢火车票的过程 代码示例 thread.cc Thread.hpp 运行结果 分析原因 tickets减到-2的本质 解决抢票出错的方案 临界资源的概念 原子性的概念 加锁 定义 初始化 销毁 代码形式如下 代码示例1&#xff1a; 代码示例2&#xff1a; 总结 如何看待锁 申…

【C++】STL中stack的用法及模拟实现

目录 一、stack的简介二、stack的使用三、stack的模拟实现 一、stack的简介 stack是一种容器适配器&#xff0c;专门用在后进先出操作的上下文中环境中&#xff0c;其中的元素只允许从容器固定的一端进行插入和删除操作。stack是作为容器适配器来实现的&#xff0c;容器适配器…

信息安全实践1.3(HTTPS)

前言 做这个实验对Tomcat的版本有要求&#xff0c;最好是使用Tomcat8。因为我之前使用Tomcat10&#xff0c;然后一直做不出来。 要求 部署Web服务器端HTTPS功能&#xff0c;通过网络嗅探分析HTTPS通过SSL实施安全保护的效果 关键步骤 首先要给tomcat配置https&#xff0c;也…

Unity3D安装:离线安装 Unity

推荐&#xff1a;将 NSDT场景编辑器 加入你的3D工具链 3D工具集&#xff1a; NSDT简石数字孪生 在没有 Hub 的情况下离线安装 Unity Unity 下载助手 (Download Assistant) 支持离线部署。在这种部署方式中&#xff0c;可下载用于安装 Unity 的所有文件&#xff0c;然后生成脚本…

采购申请审批测试

采购申请审批的配置并不难&#xff0c;但是总会有原因导致业务无审批策略&#xff0c;而且这个配置也比较脆弱&#xff0c;有时同步也会出现问题&#xff0c;小编利用这篇操作记录下测试结果。 1、项目类别的审批策略分类 下图是审批策略分类-项目类别不给值&#xff0c;测试…