【二叉搜索树】

news2024/12/24 2:28:15

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模型结构。

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

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

相关文章

Android“真正的”模块化

作者&#xff1a;bytebeats 模块化背后的原则概述 “如果说SOLID原则告诉我们如何将砖块排列成墙和房间, 那么组件原则则告诉我们如何将房间排列成建筑.” ~ Robert C. Martin, Clean Architecture 你应该分层打包还是分特性打包?还有其他方法吗? 如何提高项目的编译时间? 你…

将Python环境迁移到另一台设备上

本方法可以将一台电脑上的python环境迁移到另一台电脑上&#xff0c;可以省去一个一个包pip的麻烦。本文以pytorch的迁移为例。 一、从源环境备份安装包 在原来的电脑的Conda控制台中使用语句 pip freeze > c:\myrequirement.txt 后面跟的参数是文件的路径和文件名&#x…

Spring MVC自定义拦截器--Spring MVC异常处理

目录 自定义拦截器 什么是拦截器 ● 说明 自定义拦截器执行流程分析图 ● 自定义拦截器执行流程说明 自定义拦截器应用实例 ● 应用实例需求 创建MyInterceptor01 创建FurnHandler类 在 springDispatcherServlet-servlet.xml 配置拦截器 第一种配置方式 第二种配置方…

linux 互斥量pthread_mutex

专栏内容&#xff1a;linux下并发编程个人主页&#xff1a;我的主页座右铭&#xff1a;天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物&#xff0e; 目录 前言 概述 原理 初始化 进程和线程使用的不同点 死锁 接口 基本API 属性设置 …

探索机器翻译:从统计机器翻译到神经机器翻译

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

Osek网络管理及ETAS实现

OSEK/VDX&#xff08;Offene Systeme und deren Schnittstellen fr die Elektronik in Kraftfahrzeugen / Vehicle Distributed eXecutive&#xff09;是一种用于嵌入式系统&#xff08;尤其是汽车电子控制单元&#xff09;的开放标准。它旨在提供一种统一、可互操作的软件架构…

关于 《python 从入门到实践》的 matplotlib 随机漫步小项目

使用 python 生成随机漫步数据&#xff0c;再使用 matplotlib 将数据呈现。 所谓随机漫步&#xff1a; 每次行走的路径都是完全随机的&#xff0c;就像蚂蚁在晕头转向的情况下&#xff0c;每次都沿随机方向前行路径。 在自然界&#xff0c;物理学&#xff0c;生物学&#xff0…

【Linux】Job for network.service failed(网卡启动报错)

上图是Linux网卡启动报错的情况 这是由于cat/etc/sysconfig/network-scripts/ifcfg-xxx 中HWADDR的MAC地址和ifconfig中的MAC地址不一样&#xff0c;或者缺少cat/etc/sysconfig/network-scripts/ifcfg-xxx 中HWADDR的MAC地址 1.查看ifconfig中的MAC地址 图中00&#xff1a;0c…

【新星计划-2023】IP地址是什么?IP地址的主要功能是什么?

IP地址在生活中是很常见的&#xff0c;我们所使用的手机、电脑等等&#xff0c;都有一个IP地址&#xff0c;那么IP地址是什么&#xff1f;通过IP地址又能干什么&#xff1f;下文就来给大家详细的讲解一下。 一、什么是IP地址 通常我们说的IP地址多数是指互联网中联网的IP地址…

Java 基础进阶篇(十一)—— Arrays 与 Collections 工具类

文章目录 一、Arrays工具类1.1 Arrays 类常用方法1.2 对于 Comparator 比较器的支持1.3 Arrays 的综合应用1.3.1 应用一&#xff1a;数组的降序排序1.3.2 应用二&#xff1a;根据学生年龄进行排序 二、Collections工具类2.1 Collections 类常用方法2.2 Collections 排序相关 AP…

神经网络实验---梯度下降法

本次实验主要目的是掌握梯度下降法的基本原理&#xff0c;能够使用梯度下降法求解一元和多元线性回归问题。 文章目录 目录 文章目录 1. 实验目的 2. 实验内容 3. 实验过程 题目一&#xff1a; 题目二&#xff1a; 题目三&#xff1a; 实验小结&讨论题 1. 实验目的 ① 掌握…

〖Python网络爬虫实战㉓〗- Ajax数据爬取之什么是Ajax

订阅&#xff1a;新手可以订阅我的其他专栏。免费阶段订阅量1000 python项目实战 Python编程基础教程系列&#xff08;零基础小白搬砖逆袭) 说明&#xff1a;本专栏持续更新中&#xff0c;目前专栏免费订阅&#xff0c;在转为付费专栏前订阅本专栏的&#xff0c;可以免费订阅付…

23.5.7总结(学习通项目思路)

项目思路&#xff1a; 注册&#xff1a;输入邮箱&#xff08;判重&#xff09;&#xff0c;两次输入密码&#xff0c;获得的正确的验证码&#xff0c;获得不重复的用户名。 登录&#xff1a;输入用户名和密码登录。 忘记密码&#xff1a;输入邮箱&#xff08;和用户名&#…

RK3588平台开发系列讲解(进程篇)可执行文件内部结构

平台内核版本安卓版本RK3588Linux 5.10Android 12文章目录 一、 ELF 文件的两大组成部分二、文件头三、程序头和节区头四、ELF 文件的细节结构沉淀、分享、成长,让自己和他人都能有所收获!😄 📢在 Linux 中,二进制可执行文件的标准格式叫做 ELF(Executable and Linkabl…

ARP协议结构

文章目录 概念ARP协议格式ARP协议的作用ARP协议的工作流程 首先提出一个问题&#xff0c;来理解ARP解决什么问题 已知报文在数据链路层传输的过程中&#xff08;假设是主机A到主机B&#xff09;&#xff0c;是通过路由器之间的跳转&#xff0c;根据路由表&#xff0c;结合目的…

【论文】SimCLS:一个简单的框架 摘要总结的对比学习(1)

SimCLS:摘要总结的对比学习(1&#xff09; 写在最前面模型框架 摘要1 简介 写在最前面 SimCLS: A Simple Framework for Contrastive Learning of Abstractive Summarization&#xff08;2021ACL会议&#xff09; https://arxiv.org/abs/2106.01890 论文&#xff1a;https://…

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

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

postgresql insert ddl执行流程分析

专栏内容&#xff1a;postgresql内核源码分析个人主页&#xff1a;我的主页座右铭&#xff1a;天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物&#xff0e; 目录 前言 总体流程 调用堆栈 执行接口说明 详细流程分解 ExecInsert对于普通表…

Java9

Java9 &#xff08;一&#xff09;、stream流1.1 Stream流的中间方法和终结方法 &#xff08;二&#xff09;、方法引用2.1 方法引用的分类 &#xff08;三&#xff09;、异常3.1 编译时异常和运行时异常3.2 异常的作用3.3 异常的处理方式3.4 异常中的常见方法3.5 自定义异常 &…

麒麟设置分辨率

为什么要设置------额。。。。虚拟机启动的&#xff0c;直接满屏了。。。。也不能移动 命令设置法 1、可以通过xrandr命令来设置屏幕分辨率。先查询当前分辨率&#xff0c;及当前支持的分辨率。 xrandr 2、可以通过-s参数来设置为1920x1440 xrandr -s 1920x1440 这就好…