【C++进阶】二叉搜索树(BSTree)

news2024/9/23 1:35:51

在这里插入图片描述

​👻内容专栏:C/C++编程
🐨本文概括:二叉搜索树的基本操作(查找、删除、插入)、二叉搜索树的应用,KV模型。
🐼本文作者:阿四啊
🐸发布时间:2023.11.22

一、二叉搜索树

1.1 二叉搜索树的概念

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

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

在这里插入图片描述

1.2 二叉搜索树的基本操作

类的创建

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

在这里插入图片描述

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

查找操作

分为两种情况:
a、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
b、最多查找高度次,走到到空,还没找到,说明这个值不存在。

bool Find(const T& key)
{
	if (_root == nullptr)
	{
		return false;
	}

	Node* cur = _root;
	while (cur != nullptr)
	{
		if (cur->_key < key)
		{
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			cur = cur->_left;
		}
		else
		{
			return true;
		}
	}
	return false;
}

插入操作

插入的具体过程如下:
a. 树为空,则直接新增节点,赋值给root指针
b. 树不空,需要定义cur节点指针往后寻找,按二叉搜索树性质查找插入位置,插入新节点。其中我们还需定义一个parent节点指针,是为了让插入的节点在parent节点的左边还是右边。

⚠️注意:二叉搜索树中是不允许出现相等的值的,出现相等情况的值,插入操作返回false即可。

bool Insert(const T& key)
{
	//树为空
	if (_root == nullptr)
	{
		_root = new Node(key);
		return true;
	}

	//树不为空
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur != nullptr)
	{
		if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else
		{
			return false;
		}
	}
	
	if (parent->_key < key) parent->_right = new Node(key);
	else parent->_left = new Node(key);

	return true;
}

删除操作

首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:

  1. 要删除的结点无孩子结点。
  2. 要删除的结点只有左孩子结点。
  3. 要删除的结点只有右孩子结点。
  4. 要删除的结点有左、右孩子结点。

看起来有待删除节点有4中情况,无孩子结点也可以被分为只有左孩子或者只有孩子的情况,所以实际情况a可以与情况b或者c合并起来,因此真正的删除过程。如下:
情况a:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点 ==>直接删除
情况b:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点 ==>直接删除
情况c:在它的右子树中寻找中序下的最左节点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题。或者在它的左子树中寻找最右节点(关键码最大),用它的值填补到被删除节点中,再来处理该结点的删除问题 ==>替换法删除
直接删除:

在这里插入图片描述
替换法删除:

在这里插入图片描述
以上替换法两种方法都可行,作者便使用法二,寻找右子树的最左节点(最小节点)进行交换。另外一种方法友友们可以自己实现。

bool Erase(const T& key)
{
	//树为空
	if (_root == nullptr)
	{
		return false;
	}

	Node* parent = nullptr;
	Node* cur = _root;
	while (cur != nullptr)
	{
		if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else
		{
			//准备删除操作
			//情况b 要删除的结点只有右孩子结点
			if (cur->_left == nullptr)
			{
				if (cur == _root)
				{
					_root = cur->_right;
				}
				else
				{
					//左子树中
					if (cur == parent->_left)
					{
						parent->_left = cur->_right;
					}
					else
					//右子树中
					{
						parent->_right = cur->_right;
					}
				}	
			}
			else if (cur->_right == nullptr)
			//情况a 要删除的结点只有左孩子结点
			{
				if (cur == _root)
				{
					_root = cur->_left;
				}
				else
				{
					//左子树中
					if (cur == parent->_left)
					{
						parent->_left = cur->_left;
					}
					else
						//右子树中
					{
						parent->_right = cur->_left;
					}
				}
			}
			else
			//情况c 要删除的结点有左、右孩子结点
			{
				//右子树的最小节点(最左节点)
				Node* parent = cur;
				//parent为什么初始化为cur,因为在删除根节点时为特例,否则会出现循环进不去,出现空指针解引用
				Node* subLeft = cur->_right;
				while (subLeft->_left != nullptr)
				{
					parent = subLeft;
					subLeft = subLeft->_left;
				}

				swap(cur->_key, subLeft->_key);

				if (parent->_left == subLeft)
				{
					parent->_left = subLeft->_right;
				}
				else
				{
					parent->_right = subLeft->_right;
				}
			}
			return true;
		}
	}
	return false;
}

我们可以在类中手写一个中序遍历,验证删除操作是否正确。
中序遍历

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

在main函数中进行验证

int main()
{
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	BSTree<int> bt;
	for (auto e : a)
	{
		bt.Insert(e);
	}
	bt.InOrder();

	//删除14
	bt.Erase(14);
	bt.InOrder();
	//删除3
	bt.Erase(3);
	bt.InOrder();
	//删除8
	bt.Erase(8);
	bt.InOrder();

	return 0;
}

验证结果:

1 3 4 6 7 8 10 13 14
1 3 4 6 7 8 10 13
1 4 6 7 8 10 13
1 4 6 7 10 13

1.3 二叉搜索树的基本操作(递归版本)

查找操作

public:
bool FindR(const T& key)
	{
		return _FindR(_root, key);
	}
private:
bool _FindR(Node* root, const T& 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;
	}
}

插入操作

我们发现你插入操作的难点在于如何进行链接节点,这里我们只需在形参部分给Node* root添加上&之后,这一点很巧妙,然后我们执行root = new Node(key),就能成功链接新插入的节点。不懂的友友们可以试着画一画递归展开细节图。

public:
bool InsertR(const T& key)
{
	return _InsertR(_root, key);
}
private:
bool _InsertR(Node*& root, const T& 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(const T& key)
{
	return _EraseR(_root, key);
}
private:
bool _EraseR(Node*& root,const T& key)
{
	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)
		{
			Node* del = root;
			root = root->_right;
			delete del;
		}
		else if (root->_right == nullptr)
		{
			Node* del = root;
			root = root->_left;
			delete del;
		}
		else
		{
			//在右子树中寻找最左节点(最小节点)
			Node* subLeft = root->_right;
			while (subLeft->_left != nullptr)
			{
				subLeft = subLeft->_left;
			}

			swap(root->_key, subLeft->_key);

			//转换成在子树中去递归删除
			return _EraseR(root->_right, key);
		}
	}
}

这里要着重说明一下删除左右孩子都存在的节点,该如何去递归操作,举例说明删除3,我们利用循环去找删除节点右子树中的最左节点,找到之后交换两个节点的值,那么subLeft节点如何删除?这里我们不再使用parent节点,然后条件判断,我们可以转换成在交换前的右子树中去递归删除,如下图,蓝色圆圈标记。
在这里插入图片描述

1.4 二叉搜索树的应用

  1. K模型K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
    比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
    以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
    1.2部分我们实现的就是K模型的底层基本能逻辑。

  2. KV模型每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见
    a.比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;
    b.再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。

// 改造二叉搜索树为KV结构
template<class K, class V>
struct BSTreeNode
{
	BSTreeNode<K, V>* _left;
	BSTreeNode<K, V>* _right;
	K _key;
	V _value;
	BSTreeNode(const K& key, const V& value)
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
		, _value(value)
	{ }
};
template<class K, class V>
	class BSTree
	{
	public:
		typedef BSTreeNode<K, V> Node;

		bool Insert(const K& key, const V& value);
		Node* Find(const K& key);
		void InOrder();
		bool Erase(const K& key);
	private:
		Node* _root = nullptr;
	};
//KV模型
//1.英汉词典
int main()
{
	Key_Vaule::BSTree<string, string> dictionary;
	dictionary.Insert("sort", "排序");
	dictionary.Insert("left", "左边");
	dictionary.Insert("right", "右边");
	dictionary.Insert("insert", "插入");
	dictionary.Insert("erase", "删除");

	string str;
	while (cin >> str)
	{
		Key_Vaule::BSTreeNode<string, string>* ret = dictionary.Find(str);
		if (ret)
		{
			//找到单词输出
			cout << ret->_value << endl;
		}
		else
		{
			//未找到单词
			cout << "No words found!" << endl;
		}
	}
}

//2统计单词次数
int main()
{
	// 统计水果出现的次数
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
	Key_Vaule::BSTree<string, int> countWords;
	for (auto& e : arr)
	{
		Key_Vaule::BSTreeNode<string, int>* ret = countWords.Find(e);
		if (ret == nullptr)
		{
			countWords.Insert(e, 1);
		}
		else
		{
			ret->_value++;
		}
	}

	countWords.InOrder();

	return 0;
}

1.5 二叉搜索树的性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
在这里插入图片描述
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为: l o g 2 N log_2 N log2N
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为: N 2 \frac{N}{2} 2N

问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优?那么等待后续我们的AVL树和红黑树章节讲到再说。

二、二叉搜索树源码

👉😉 gitee ==> 二叉搜索树的基本实现及二叉搜索树的应用(K模型、KV模型)

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

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

相关文章

网络和Linux网络_4(应用层)序列化和反序列化(网络计算器)

目录 1. 重新理解协议 2. 网络版本计算器 2.1 前期封装 Log.hpp sock.hpp TcpServer.hpp 第一次测试(链接) 2.2 计算器实现 第二次测试(序列化和反序列化) 第三次测试(客户端字节流) CalServer.cc CalClient.cc 3. 守护进程 3.1 守护进程和前后台进程 3.1 变成…

String类常用方法总结

目录 一.简单认识String 二.String对象的比较 1.equals 内部实现原理&#xff1a; 2.compareTo 3.compareToIgnoreCase 三.字符串查找 示例&#xff1a; 四.字符串与其他类型转化 1.数值和字符串相互转换 2.大小写相互转化 3.字符串转数组 4.格式化转化 五.字符串…

LiteOS同步实验(实现生产者-消费者问题)

效果如下图&#xff1a; 给大家解释一下上述效果&#xff1a;在左侧&#xff08;顶格&#xff09;的是生产者&#xff08;Producer&#xff09;&#xff1b;在右侧&#xff08;空格&#xff09;的是消费者&#xff08;Consumer&#xff09;。生产者有1个&#xff0c;代号为“0”…

编译源码-【opencv3.4.16 + vs2013 x64】

编译机器&#xff1a;i5 13500HX RTX 4050 laptop win11 CMake 3.26.4 Configure&#xff0c;去掉勾选图中黄色标注的项&#xff0c;opencv_world 随意 Configure可能提示3rdparty下载timeout&#xff0c;它会下载到源码目录的.cache ├── .cache │ ├──ffmpeg │ │ …

[数据结构]—栈和队列

&#x1f493;作者简介&#x1f389;&#xff1a;在校大二迷茫大学生 &#x1f496;个人主页&#x1f389;&#xff1a;小李很执着 &#x1f497;系列专栏&#x1f389;&#xff1a;数据结构 每日分享✨&#xff1a;到头来&#xff0c;有意义的并不是结果&#xff0c;而是我们度…

基于Springboot的美容院管理系统(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; 基于Springboot的美容院管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&a…

「浙江科聪新品发布」新品发布潜伏顶升式移动机器人专用控制器

聚焦专用车型 最小专用控制器 控制器只占整机5%&#xff0c;纵向出线方式&#xff0c;占比更小 更易插拔 整体解决方案 更具价格优势 提供整体解决方案&#xff0c;配套各类型产品设备及车体厂家 打造持久稳定使用 坚持工业级品质 采用车规级接口&#xff0c;不用其它类不可…

ChatGPT 使用入门

背景 ChatGPT是一个强大的聊天机器人助手&#xff0c;内置了大量的互联网知识文档&#xff0c;且具有上下文记忆&#xff0c;可以帮我们快速地查找一些资料&#xff0c;了解一个知识&#xff0c;帮我们回答问题&#xff0c;编写代码等。此外&#xff0c;在使用ChatGPT时具有一…

论文笔记:Localizing Cell Towers fromCrowdsourced Measurements (intro 部分)

2015 1 Intro 1.1 motivation opensignal.com 、cellmapper.net 和 opencellid.org 都是提供天线&#xff08;antenna&#xff09;位置的网站 他们提供的天线位置相当准确&#xff0c;但至少在大多数情况下不完全正确这个目标难以实现的原因是蜂窝网络供应商没有义务提供有…

【Redis篇】简述Java中操作Redis的方法

文章目录 &#x1f384;简述Jedis&#x1f384;Jedis优点&#x1f354;使用Jedis连接Redis⭐进行测试&#x1f388;进行测试 Redis&#xff08;Remote Dictionary Server&#xff09;是一种流行的高性能内存数据库&#xff0c;广泛应用于各种应用程序和系统中。作为Java开发人员…

汇编-pop出栈指令

32位汇编 执行动作分为两步&#xff1a; 第一步&#xff1a;读出数据 第二步&#xff1a;改变栈地址 如果操作数是16位&#xff0c; 则ESP加2&#xff1b; 如果操作数是32位&#xff0c; 则ESP加4 espesp2 或 espesp4 格式&#xff1a;

如何开发干洗店用的小程序

洗护行业现在都开始往线上的方向发展了&#xff0c;越来越多的干洗店都推出了上门取送服务&#xff0c;那么就需要开发一个干洗店专用的小程序去作为用户和商家的桥梁&#xff0c;这样的小程序该如何开发呢&#xff1f; 一、功能设计&#xff1a;根据干洗店的业务需求和小程序的…

智能座舱架构与芯片 - (3) 硬件篇 上

一、介绍 在了解智能座舱的基本架构之后&#xff0c;我们有必要针对智能座舱域的硬件平台&#xff0c;软件平台&#xff0c;SOC等进行逐一介绍。从它们的整体结构中去认识最新的智能座舱组成部件&#xff0c;以及主要功能等。 如上图&#xff0c;是中央计算-区域控制架构下的智…

集成电路生产ERP都有哪些模块?企业如何选择适用的系统

集成电路的生产管理涵盖物料、配件、车间、图纸设计、工艺流转、出入库、退补料、品质检验、成本核算、班组产能评估等环节&#xff0c;而这些环节之间信息的实时和准确传递是管理的难点也是重点。 如何集成各类资源&#xff0c;优化相关业务流程&#xff0c;提高资源利用率&a…

2014年3月13日 Go生态洞察:并发模式与管道取消技术

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

创作一款表情包生成微信小程序:功能详解与用户体验优化

一、引言 在当今社交媒体充斥着各种趣味表情包的时代&#xff0c;表情包生成工具成为许多用户创作和分享创意的热门选择。为了迎合这一趋势&#xff0c;我设计并开发了一款功能丰富、用户友好的微信小程序&#xff0c;旨在让用户能够轻松而有趣地创作个性化的表情包。这篇博客将…

Java 环境其他下载2

1 Eclipse Temurin Latest Releases | Adoptium Eclipse Temurin 是由基于 OpenJDK 的开源 Java SE 产生的构建版本。Temurin 适用于 广泛的平台 以及诸多 Java SE 版本。以下列出了推荐用于生产的最新版本&#xff0c;并且定期由 Adoptium 社区发布更新和支持。迁移帮助、容器…

C语言杨辉三角(ZZULIOJ1130:杨辉三角)

题目描述 还记得中学时候学过的杨辉三角吗&#xff1f;具体的定义这里不再描述&#xff0c;你可以参考以下的图形&#xff1a;1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 1 5 10 10 5 1 输入&#xff1a;输入只包含一个正整数n&#xff08;1 < n < 30&#xff09;&#xff0c;表示将…

linux之 服务器ping百度能通,ping其他网址不通

表症问题 linux上ping域名解析出来的ip地址不正确 linux服务器ping百度能通&#xff0c;ping其他网址不通 linux上ping域名解析出来的ip地址不正确 ping 百度可以&#xff0c;说明dns解析是没问题的 但是&#xff0c;ping 其他网址不通&#xff0c;说明是 请求的其他网址的问…

【香橙派】实战记录1——简介及烧录 Linux 镜像

文章目录 一、简介1、参数2、结构3、其他配件4、下载资料 二、基于 Windows PC 将 Linux 镜像烧写到 TF 卡的方法1、使用 balenaEtcher 烧录 Linux 镜像的方法2、效果 一、简介 Orange Pi Zero 3 香橙派是一款开源的单板卡片电脑&#xff0c; 新一代的arm64开发板&#xff0c;…