【二叉搜索树】

1 二叉搜索树概念

二叉搜索树又称二叉排序树,它或者是一棵空树 ,或者是具有以下性质的二叉树 :
  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  • 它的左右子树也分别为二叉搜索树


2 二叉搜索树操作

最频繁的几个操作分别是:寻找,插入,删除。

查找与插入根据二叉搜索树的性质很容易实现,关键是删除如何操作?

这里就先不详细介绍了,在下面会给出详细解释。

3 二叉搜索树的代码实现

3.1 查询

非递归版本:

		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;
				else return true;
			}
			return false;
		}

递归版本:

为了方便使用,写了个子函数:

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


		bool FindR(const K& key)
		{
			return _FindR(_root, key);
		}

3.2 插入

非递归版本:

		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;
				}
				else
				{
					return false;
				}
			}
			if (parent->_key > key)
			{
				parent->_left = new Node(key);
			}
			else
			{
				parent->_right = new Node(key);
			}

		}

代码总的来说并不难,关键是记录好父亲结点正确链接即可。

 递归版本:

		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;
		}

递归版本巧妙之处在于用了结点指针的引用,我们来看一张图:

 假如我们想插入16,走读代码:此时root->right也就是14的右结点恰好是新插入结点的别名,当直接使用root赋值时父节点的与子节点链接关系已经完毕,并不需要像写迭代版本那样判断是父亲的左还是右,直接链接即可。但是一般我们很少写递归版本的,栈容易爆。

3.3 删除

二叉搜索树的删除主要分成下面这几种情况:

  • a. 要删除的结点无孩子结点
  • b. 要删除的结点只有左孩子结点
  • c. 要删除的结点只有右孩子结点
  • d. 要删除的结点有左、右孩子结点

 看起来有待删除节点有4中情况,实际情况a可以用情况b或者c处理,因此真正的删除过程只有3种情况,我们一个一个来看:

 要删除的结点只有右孩子结点 ,比如删除10,我们可以采用托孤来处理:

也就是将10的右子树交给10的父亲领养:

 同理,要删除的结点只有左孩子结点,比如删除14,一样可以用托孤处理:

现在关键是如何删除有两个孩子的结点,这时候托孤就不太行了,两个孩子父亲肯定是不能够领养的,所以便有了宁外一种方式:替换法删除。

比如删除8,我们应该找到哪一个结点替换才是比较合理的,通过观察,发现用7或者10来替换删除是比较合理的,也就是找到删除结点的左子树最大节点或者右子树最小结点替换删除是比较合理的。有了理论后便可以开始写代码了:

非递归版本:

		bool Erase(const K& key)
		{
			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;
				}
				else
				{
					//找到了,准备删除
					//第一种情况:删除节点没有左孩子
					if (cur->_left == nullptr)
					{
						//这里还得考虑parent为空的情况,也就是删除节点为根节点的时候
						if (parent == nullptr)
						{
							_root = cur->_right;
						}
						else
						{
							if (parent->_left == cur)
								parent->_left = cur->_right;
							else
								parent->_right = cur->_right;
						}
						delete cur;
					}

					//第二种情况:删除节点没有右孩子
					else if (cur->_right == nullptr)
					{
						//这里还得考虑parent为空的情况,也就是删除节点为根节点的时候
						if (parent == nullptr)
						{
							_root = cur->_left;
						}
						else
						{
							if (parent->_left == cur)
								parent->_left = cur->_left;
							else
								parent->_right = cur->_left;
						}
						delete cur;
					}

					//第三种情况:删除节点既有左孩子又有右孩子(替换法删除)
					//有两种替换的方法,一种是找到删除节点左子树中最大值替换
					//另外一种是找到删除节点右子树中最小值替换
					else
					{
						//找到删除节点左子树中最大值替换
						Node* maxParent = cur;
						Node* max = cur->_left;
						while (max->_right)
						{
							maxParent = max;
							max = max->_right;
						}

						cur->_key = max->_key;

						if (maxParent->_left == max)
							maxParent->_left = max->_left;
						else
							maxParent->_right = max->_left;
						delete max;
					}
					return true;
				}
			}
		}

这里面注意的细节有:

  • 1 当删除结点的孩子只有一个时,要先判断父亲是否为空(是否删除的是根节点)

2 当我们用替换法删除时,maxParent给的是cur,而不是nullptr,这是为了假如删除结点为根节点时由于没有进入循环而导致maxParent为空造成了空指针解引用的问题。

但是这样处理后会有其他变换,我们看下面这个图,假如我们想删除8:

 通过代码找左子树的最大结点我们找到了max=7,maxParent=6,由于7不可能有右子树,并且找到的7不可能在maxParent的左边(因为是一直往右找的),所以很多人直接会写出

maxParent->_right=max->_left 这样的代码,但是我们看看下面这个图:

 我们还是删除8,此时max=3,maxParent=8,但是是maxParent->_right=max->_left吗?

显然不是,此时是maxParent->_left=max->_left ,所以需要我们判断处理。

  • 3 同理用右子树的最小值替换删除也会有这样的问题。

 递归版本:

		bool _EraseR(Node*& root, const K& key)
		{
			if (root == nullptr)
				return false;

			if (root->_key > key)
				return _EraseR(root->_left, key);
			else if (root->_key < key)
				return _EraseR(root->_right, key);
			else
			{
				Node* del = root;
				if (root->_left == nullptr)
					root = root->_right;//巧用了引用,root实际上是上一级的parent的孩子
				else if (root->_right == nullptr)
					root = root->_left;
				else
				{
					Node* max = root->_left;//找左子树最大值
					while (max->_right)
						max = max->_right;

					swap(max->_key, root->_key);
					//递归到左子树去删除
					_EraseR(root->_left, key);
				}
				delete del;
				return true;
			}
		}

递归版本同样是分成了3种情况,这里面同样是巧用了引用。但是其中要注意的一点是我们递归到左子树去删除时用的是root->_left,而不是max->_left,想想为什么?

我们是交换的max的_key与root的_key,由于max不可能有右节点,所以转换成子问题后删除肯定会是前面删除结点只有一个孩子的情况,所以我们必须通过root的_left来进行子问题转化而不能用max->_left,因为这样才能找到max的父亲将节点正确链接,这点一定要注意。

3.4 拷贝构造 && 赋值运算符重载 && 析构

		void destroy(Node* root)
		{
			if (root == nullptr)
				return;

			destroy(root->_left);
			destroy(root->_right);
			delete root;
		}

		Node* copy(Node* root)
		{
			if (root == nullptr)
				return nullptr;

			Node* newNode = new Node(root->_key);
			newNode->_left = copy(root->_left);
			newNode->_right = copy(root->_right);
			return newNode;
		}

		BSTree()
			:_root(nullptr)
		{}

		BSTree(const BSTree<K>& bs)
		{
			_root=copy(bs._root);
		}

		BSTree<K>& operator=(BSTree<K> bs)
		{
			swap(_root, bs._root);
			return *this;
		}

		~BSTree()
		{
			destroy(_root);
			_root = nullptr;
		}

这个很简单,就不再多说了。

3.5 二叉搜索树的应用

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

 上面的代码我们可以改造成KV模型结构。

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

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

相关文章

COCO数据集相关知识介绍

&#x1f468;‍&#x1f4bb;个人简介&#xff1a; 深度学习图像领域工作者 &#x1f389;总结链接&#xff1a; 链接中主要是个人工作的总结&#xff0c;每个链接都是一些常用demo&#xff0c;代码直接复制运行即可。包括&#xff1a; &am…

Java 8 Time 关于java.time包中你可能不知道的使用细节

目录 前言一、时区与时间1. 世界标准时&#xff1a;UTC、GMT、UT2. 地区时&#xff1a;Asia/Shanghai、UTC83. 时区&#xff1a;ZoneId、TimeZone4. 时间偏移量&#xff1a;ZoneOffset5. 时区简称&#xff1a;CTT、PRC 二、主要时间类1. 重要时间接口&#xff1a;Temporal2. 时…

腾讯云轻量16核32G28M带宽服务器CPU流量性能测评

腾讯云轻量16核32G28M服务器3468元15个月&#xff0c;折合每月231元&#xff0c;28M公网带宽下载速度峰值可达3584KB/s&#xff0c;折合3.5M/秒&#xff0c;系统盘为380GB SSD盘&#xff0c;6000GB月流量&#xff0c;折合每天200GB流量。腾讯云百科来详细说下腾讯云轻量应用服务…

13 | visual studio与Qt的结合

1 前提 Qt 5.15.2 visual studio 2019 vsaddin 2.8 2 具体操作 2.1 visual studio tool 2.1.1 下载 https://visualstudio.microsoft.com/zh-hans/downloads/2.1.2 安装 开发

10_Uboot启动流程_2

目录 _main函数详解 board_init_f函数详解 relocate_code函数详解 relocate_vectors函数详解 board_init_r 函数详解 _main函数详解 在上一章得知会执行_main函数_main函数定义在文件arch/arm/lib/crt0.S 中,函数内容如下: 第76行,设置sp指针为CONFIG_SYS_INIT_SP_ADDR,也…

信息抽取与命名实体识别:从原理到实现

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

君子签:助力高校毕业、就业协议电子签,打通就业最后一公里

据介绍&#xff0c;2023届全国普通高校毕业生规模预计达1158万人&#xff0c;同比增加82万人。毕业季即将来临&#xff0c;全国各大高校毕业、就业材料签署压力大&#xff0c;盖章需求激增&#xff0c;如何快捷、高效地处理各类毕业、就业材料签署问题呢&#xff1f; 在教育部…

2023 年第八届数维杯数学建模挑战赛 赛题浅析

为了更好地让大家本次数维杯比赛选题&#xff0c;我将对本次比赛的题目进行简要浅析。本次比赛的选题中&#xff0c;研究生、本科组请从A、B题中任选一个 完成答卷&#xff0c;专科组请从B、C题中任选一个完成答卷。这也暗示了本次比赛的难度为A>B>C 选题人数初步估计也…

解决APP抓包问题「网络安全」

1.前言 在日常渗透过程中我们经常会遇到瓶颈无处下手&#xff0c;这个时候如果攻击者从APP进行突破&#xff0c;往往会有很多惊喜。但是目前市场上的APP都会为防止别人恶意盗取和恶意篡改进行一些保护措施&#xff0c;比如模拟器检测、root检测、APK加固、代码混淆、代码反调试…

程序员痛心流涕自述:“因为把自己代码给了别人,我亲手断送了自己的前程”

在求职的过程中&#xff0c;一般都会有投递简历、笔试、面试以及背调的环节&#xff0c;而在这几个环节中折戟沉沙的人也着实不少。 不少人觉得&#xff0c;在求职时简历需要优化&#xff0c;背调不能有瞒报、捏造的情况&#xff0c;而笔试面试则是纯纯的要靠硬实力。 虽然说…

Springboot +Flowable,服务任务ServiceTask执行的三种方式(二)

一.简介 ServiceTask 从名字上看就是服务任务&#xff0c;它的图标是像下面这样&#xff0c;截图如下&#xff1a; ServiceTask 一般由系统自动完成&#xff0c;当流程走到这一步的时候&#xff0c;不会自动停下来&#xff0c;而是会去执行我们提前在 ServiceTask 中配置好的…

Linux - 第12节 - 网络编程套接字

1.预备知识 1.1.理解源IP地址和目的IP地址 因特网上的每台计算机都有一个唯一的IP地址&#xff0c;如果一台主机上的数据要传输到另一台主机&#xff0c;那么对端主机的IP地址就应该作为该数据传输时的目的IP地址。但仅仅知道目的IP地址是不够的&#xff0c;当对端主机收到该数…

【新星计划-2023】什么是ARP?详解它的“解析过程”与“ARP表”。

一、什么是ARP ARP&#xff08;地址解析协议&#xff09;英文全称“Address Resolution Protocol”&#xff0c;是根据IP地址获取物理地址的一个TCP/IP协议。主机发送信息时将包含目标IP地址的ARP请求广播到局域网络上的所有主机&#xff0c;并接收返回消息&#xff0c;以此确…

Linux 中的文件锁定命令:flock、fcntl、lockfile、flockfile

在 Linux 系统中&#xff0c;文件锁定是一种对文件进行保护的方法&#xff0c;可以防止多个进程同时访问同一个文件&#xff0c;从而导致数据损坏或者冲突。文件锁定命令是一组用于在 Linux 系统中实现文件锁定操作的命令&#xff0c;它们可以用于对文件进行加锁或解锁&#xf…

Android UI深度理解:Activity UI视图结构

Activity UI视图结构 每个Activity都会获得一个窗口&#xff0c;那就是Window&#xff0c;它用于绘制用户的UI界面 Window是一个抽象类&#xff0c;提供了绘制窗口的一组通用API&#xff0c;PhoneWindow是它的唯一实现类 DecorView是所有应用窗口的根节点。是FrameLayout的子类…

windows下python下载及安装

下载python安装包 进入python官网&#xff1a;https://www.python.org/ 鼠标移动到“Downloads”->"Windows"上&#xff0c;可以看到最新版本是3.11.3版本 点击“Windows”按钮&#xff0c;可以去下载其他版本 标记为embeddable package的表示嵌入式版本&#x…

【C语言】通讯录(文件版)

前言 前面我们完成了通讯录的静态版本和动态版本&#xff0c;虽然功能已经比较完善了&#xff0c;但是前面的通讯录缺少了存储联系人的能力&#xff0c;所以我们学习了文件的操作管理&#xff0c;这里我们就用上一篇文章的知识来完成这次的文章吧。 关于通讯录的前两篇文章我放…

Windows无法完成格式化怎么办?正确的3个解决方法!

案例&#xff1a;Windows无法完成格式化怎么办 【由于我的U盘使用时间过长&#xff0c;很多文件都是不需要的&#xff0c;我想将其格式化&#xff0c;但插入电脑后&#xff0c;Windows根本无法完成格式化&#xff0c;这是为什么呢&#xff1f;我应该怎么做呢&#xff1f;求答案…

java版工程项目管理系统源码+系统管理+系统设置+项目管理+合同管理+二次开发

工程项目各模块及其功能点清单 一、系统管理 1、数据字典&#xff1a;实现对数据字典标签的增删改查操作 2、编码管理&#xff1a;实现对系统编码的增删改查操作 3、用户管理&#xff1a;管理和查看用户角色 4、菜单管理&#xff1a;实现对系统菜单的增删改查操…

【c语言小demo】登录demo | 账号密码验证功能

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; 给大家跳段街舞感谢支持&#xff01;ጿ ኈ ቼ ዽ ጿ ኈ ቼ ዽ ጿ ኈ ቼ …