数据结构——搜索二叉树

news2024/12/23 11:12:21

文章目录

  • 一. 概念
  • 二. 二叉搜索树的操作
    • 1.查找
    • 2. 插入
    • 3. 删除(重点)
    • 4.遍历
    • 5.拷贝构造与析构
  • 三.二叉搜索树的递归实现
    • 1.递归查找
    • 2.递归插入
    • 3.递归删除
  • 四.二叉树搜索的应用
  • 五.源码

前言:
本章我们将认识一种新的二叉树——搜索二叉树。这棵树有个神奇的功能就是会对数据自动排序且有着非常高的查找效率。搜索二叉树作为set、map的基础结构,同样又是接下来将要学到的AVL树以及红黑树的实现基础非常值得我们去深入学习~

一. 概念

叉搜索树本质上也是一种二叉树,只不过多了一个约束规则——

若一棵二叉树不为空,则:

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

所以构建一个搜索二叉树,只需要在插入结点时满足约束规则即可。

二. 二叉搜索树的操作

与二叉树相同,二叉搜索树由一个个结点链接而成。每个结点包含三个成员——

template <class K>
struct BSTreeNode
{
	BSTreeNode<K>* _left;  //左孩子
	BSTreeNode<K>* _right; //右孩子
	K _key;				   //键值

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

所以再定义出BSTNode(Binary Search Tree简写)结构体——

template <class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
	// 成员函数的实现
	// 插入、删除、查找...
private:
	Node* _root = nullptr;
};

接着就是各种成员函数的实现了

1.查找

搜索二叉树的查找比较简单而且更容易帮助我们理解搜索二叉树的性质,所以先从查找入手。
在这里插入图片描述

以上图为例,倘若我们要查找 7,具体的思路是这样的——

  • 7 < 8,因此去 8 的左子树去查找
  • 7 > 3,因此去 3 的右子树去查找
  • 7 > 6,因此去 6 的右子树去查找
  • 7 = 7,找到了,返回true

于是我们试着着手实现一个Find函数

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; // 找到返回true
	}
	return false; // 未找到返回false
}

2. 插入

理解了如何查找,插入也就非常简单。

在这里插入图片描述

还是以此图为例,倘若我们要插入 9 ,具体步骤为——

  • 首先确定cur的位置,并随时更新parent
  • 最终,cur走到10的左节点的位置,即cur = nullptr,循环结束
  • 此时patent = Node*(10)
  • 最后一步,new一个结Node*(key)并赋值给parent->_left即可。
bool Insert(const K& key)
{
	// 如果是第一次插入,直接new一个新结点给_root
	if (_root == nullptr)
	{
		_root = new Node(key);
		return true;
	}
	
	Node* cur = _root; // cur用来定位插入的位置
	Node* parent = cur; // 记录parent的父亲
	
	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;
}

3. 删除(重点)

二叉搜索树的删除是最精华的部分。对与叶子节点,例如4、7、13,删除非常简单,只需将自身的位置替换为nullptr即可。

在这里插入图片描述

如果要删除14或者10,也是比较简单的,因为10的左右子树只有一方为nullptr(10的左子树为空),所以只需要载删除的时候让父结点接管自己不为空的子树即可。

倘若要删除6或者3,由于它们的左右子树都不为空,删除时无法将两个子树都交给父结点,情况就较为复杂。

所以此种情况,我们只能想办法请一个人来接替自己的位置,但是并不是谁来都能胜任这个位置的。这个接替者必须满足二叉搜索树的条件——左子树都比它小,右子树都比它大。那么这个接替者的人选只能有这两个——

  • 左子树的最大(最右)节点
  • 或右子树的最小(最左)节点

例如,倘若要删除3,此时有两种做法都可行——

  • 用1替换3
  • 用7替换3

综上所述,删除操作共分为一下几种情况——

  1. 左子树为空
  2. 右子树为空
  3. 左右子树都不为空
  4. (左右子树都为空其实可以归并到1或2的情况中)
bool Erase(const K& key)
{
	Node* cur = _root;
	Node* parent = cur;
	while (cur)
	{
		// 找到值为key的结点
		if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else // 找到了
		{	
			// 删除
			if (cur->_left == nullptr) // 1.左子树为空
			{
				if (cur == _root) // 根节点的删除
				{
					_root = cur->_right;
					return true;
				}
				else
				{
					if (parent->_left == cur)
						parent->_left = cur->_right;
					else
						parent->_right = cur->_right;
					delete cur;
				}
			}
			else if (cur->_right == nullptr) // 2.右子树为空
			{
				if (cur == _root) // 根节点的删除
				{
					_root = cur->_left;
					return true;
				}
				else
				{
					if (parent->_left == cur)
						parent->_left = cur->_left;
					else
						parent->_right = cur->_left;
					delete cur;
				}
			}
			else // 左右子树都不为空
			{
				// 找左子树的最大结点 或者 右子树的最小结点
				Node* minRight = cur->_right;
				Node* pminRight = cur;

				while (minRight->_left)
				{
					pminRight = minRight;
					minRight = minRight->_left;
				}

				cur->_key = minRight->_key; // 替换
				
				if (pminRight->_left == minRight)
				{
					pminRight->_left = minRight->_right;
				}
				else
				{
					pminRight->_right = minRight->_right;
				}

				delete minRight;
			}
			return true;
		}
	}
	return false;
}

4.遍历

二叉搜索树的遍历非常简单,就是之前学习过的二叉树的中序遍历

void InOrder()
{
	_InOrder(_root);
	cout << endl;
}

void _InOrder(Node* root)
{
	if (root == nullptr)
		return;
	_InOrder(root->_left);
	cout << root->_key << ' ';
	_InOrder(root->_right);
}

注:由于调用函数时C++封装的特性,需设计两个函数,InOrder接口对外提供,_InOrder不对外提供。

5.拷贝构造与析构

	//采用前序遍历拷贝构造
	BSTree(const BSTree<K>& t)
	{
		_root = _Copy(t._root);
	}

	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;
	}
	//采用后序遍历的方式来删除,从下往上删。
	void _Destroy(Node*& root)
	{
		if (root == nullptr)
		{
			return;
		}

		_Destroy(root->_left);
		_Destroy(root->_right);
		delete root;
		root = nullptr;
	}

	~BSTree()
	{
		_Destroy(_root);
	}

因为拷贝构造二叉搜索树时要保证树的结构与原来树的结构一致,因此采用前序遍历进行拷贝构造。

但如果写了拷贝构造之后编译器就不会生成默认的构造函数了,因为拷贝构造也属于构造,因此可以利用一下C++11的特性,强制编译器生成一个默认的拷贝构造

//强制编译器生成一个默认的拷贝构造
BSTree() = default;

三.二叉搜索树的递归实现

对于搜索二叉树来说,上面实现的非递归版本是比递归版本更优的。此处的递归实现完全属于多余了,但是作为拓展内容看一看也未尝不可。

1.递归查找

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

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

2.递归插入

bool InsertR(const K& key)
{
	return _EraseR(_root, 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;
}

3.递归删除

bool EraseR(const K& key)
{
	return _EraseR(_root, key);
}

bool _EraseR(Node*& root, const K& 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
	{
		Node* del = root;
		//1.右为空
		if (root->_right == nullptr)
			root = root->_left;
		//2.左为空
		else if (root->_left)
			root = root->_right;
		//3.左右都不为空
		else
		{
			//找左子树最大节点交换
			Node* maxleft = root->_left;
			while (maxleft->_right)
				maxleft = maxleft->_right;
			
			//找到后先交换要删除的值与左子树最大节点的值
			swap(root->_key, maxleft->_key);
			//再递归到左子树中去删除
			return _EraseR(root->_left, key);
		}
		delete del;

		return true;
	}
}

四.二叉树搜索的应用

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

五.源码

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

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

	template <class K>
	class BSTree
	{
		typedef BSTreeNode<K> Node;
	public:
		//强制编译器生成构造函数
		BSTree() = default;
		//拷贝构造
		BSTree(const BSTree<K>& t)
		{
			_root = Copy(t._root);
		}
		//赋值重载
		BSTree<k>& operator=(BSTree<K> t)
		{
			swap(_root, t._root);
			return *this;
		}
		~BSTree()
		{
			Destroy(_root);
		}
		//插入节点
		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->_right;
				}
				else if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else
				{
					returen false;
				}
			}

			cur = new Node(key);
			if (parent->_key < key)
			{
				parent->_right = cur;
			}
			else
			{
				parent->_left = cur;
			}
			return true;
		}

		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
				{
					returen true;
				}
			}
			return false;
		}
		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.左为空
					if (cur->_left == nullptr)
					{
						if (cur == _root)
						{
							_root = cur->_right;
						}
						else
						{
							if (parent->_left == cur)
							{
								parent->_left = cur->_right;
							}
							else
							{
								parent->_right == cur->_right;
							}
						}
						delete cur;
					}

					//2.右为空
					else if (cur->_right == nullptr)
					{
						if (cur == _root)
						{
							_root = cur->_left;
						}
						else
						{
							if (parent->_left == cur)
							{
								parent->_left = cur->_left;
							}
							else
							{
								parent->_right == cur->_left;
							}
						}
						delete cur;
					}

					//3.左右都不为空,找右树最小节点或左树最大节点替代cur
					else
					{
						Node* pminRight = cur;
						Node* minRight = cur->_right;

						while (minRight->_left)
						{
							pminRight = minRight;
							minRight = minRight->_left;
						}
						cur->_key = minRight->_key;
						if (pminRight->_left == minRight)
						{
							pminRight->_left = minRight->_right;
						}
						else
						{
							pminRight->_right = minRight->_right;

						}
						delete minRight
					}
					return true;
				}
			}
			return false;
		}
		//递归版
		bool FindR(const K& key)
		{
			return _FindR(_root, key);
		}
		bool InsertR(const K& key)
		{
			return _InsertR(_root, key);
		}
		bool Erase(const K& key)
		{
			return _Erase(_root, key);
		}
		bool Insorder()
		{
			_Insorder(_root);
			cout << endl;
		}
	protected:
		Node* Copy(Node* root)
		{
			if (root == nullptr)
				return nullptr;
			Node* newRoot = new Node(root->_key);
			newRoot->_left = Copy(root->_left);
			newRoot->_right = Copy(root->_right);
			return newRoot;
		}
		void Destroy(Node*& root)
		{
			if (root == nullptr)
				return;
			Destroy(root->_left);
			Destroy(root->_right);

			delete root;
			root = nullptr;
		}

		bool _FindR(Node* root, const K& key)
		{
			if (root == nullptr)
				return false;
			if (root->_key == key)
				return true;
			if (root->_key < key)
				return _FindR(root->_right, key);
			else 
				return _FindR(root->_left, 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;
			}
		}
		bool _EraseR(Node*& root, const K& 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
			{
				Node* del = root;
				//1.右为空
				if (root->_right == nullptr)
					root = root->_left;
				//2.左为空
				else if (root->_left)
					root = root->_right;
				//3.左右都不为空
				else
				{
					//找左子树最大节点交换
					Node* maxleft = root->_left;
					while (maxleft->_right)
						maxleft = maxleft->_right;
					
					//找到后先交换要删除的值与左子树最大节点的值
					swap(root->_key, maxleft->_key);
					//再递归到左子树中去删除
					return _EraseR(root->_left, key);
				}
				delete del;

				return true;
			}
		}
	
	private:
		Node* _root;
	};
}

本文到此结束,码文不易,还请多多支持!!!

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

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

相关文章

【2023】Redis实现消息队列的方式汇总以及代码实现

Redis实现消息队列的方式汇总以及代码实现 前言开始前准备1、添加依赖2、添加配置的Bean 具体实现一、从最简单的开始&#xff1a;List 队列代码实现 二、发布订阅模式&#xff1a;Pub/Sub1、使用RedisMessageListenerContainer实现订阅2、还可以使用redisTemplate实现订阅 三、…

win10 64位 vs2017 qt5.12.6 pcl1.9.1 vtk8.1.1配置安装步骤

由于我电脑中有 QT5.12.6 VS2017&#xff0c;就不介绍怎么安装了&#xff0c;只介绍cmake&#xff0c;pcl及vtk的配置步骤 为了便于后续QT的调用&#xff0c;以下所有安装路径中均不能出现中文及空格等 PCL自带VTK是不完整的&#xff0c;所以需要下载VTK源码进行重新编译使其…

激光切割机在镂空技术中的运用场景具体包括哪些部分

本文将为您呈现一些激光镂空工艺的实际应用情况。激光切割机应用在镂空工艺上的一些地方。 首先&#xff0c;纸艺激光镂空的应用&#xff1a; 纸是中国古代四大发明之一&#xff0c;激光则是20世纪以来人类的一项重大发明。当传统文化与现代科技相互碰撞时&#xff0c;使得纸雕…

分享低成本非隔离PWM控制AC-DC开关芯片 YB5011

简介&#xff1a; YB5011系列是一款高性能低成本PWM控制功率开关&#xff0c;适用于离线式小功率降 压型应用场合&#xff0c;外围电路简单、器件个数少。同时产品内置高耐压MOSFET可提高 系统浪涌耐受能力,集成有完备的带自恢复功能的保护功能&#xff1a;VDD欠压保护、逐周期…

使用傲梅 VMware 备份软件保障数据保护

VMware数据保护一直是热门话题&#xff0c;因为VMware是虚拟化的驱动力。96% 的用户至少经历过数据丢失的主要原因之一&#xff1a;人为错误、硬盘驱动器故障、断电、火灾和自然灾害。 有效的 VMware 备份解决方案可以保护您的虚拟环境&#xff0c;并能够在需要时快速保护和恢…

Kafka-Broker工作流程

kafka集群在启动时&#xff0c;会将每个broker节点注册到zookeeper中&#xff0c;每个broker节点都有一个controller&#xff0c;哪个controller先在zookeeper中注册&#xff0c;哪个controller就负责监听brokers节点变化&#xff0c;当有分区的leader挂掉时&#xff0c;contro…

在商业广告领域中,LDE透明屏有哪些应用表现?

LDE透明屏是一种新型的显示技术&#xff0c;它能够在显示内容的同时保持屏幕的透明度&#xff0c;使得用户可以透过屏幕看到背后的物体。LDE透明屏的出现&#xff0c;为我们的生活带来了许多新的可能性。 首先&#xff0c;LDE透明屏可以应用于商业广告领域。 传统的广告牌需要…

浅谈实际工程中智能照明系统的节能设计

安科瑞 华楠 摘要&#xff1a;本文介绍了智能照明系统在实际工程中的应用&#xff0c;简单介绍了智能照明控制设计系统&#xff0c;阐述当前智能照明对建筑节能的重要意义&#xff0c;合理地分析了智能照明系统的发展前景。 关键词&#xff1a;智能照明系统控制&#xff0c;…

​17款画流程图的工具全面分析,功能一览!

流程图又称框图&#xff0c;是以特定的图形符号加上说明&#xff0c;表示算法的图。流程图相对于纯文字的表达而言在视觉上更清晰&#xff0c;能帮助我们进行更加有效的沟通和分析。流程图制作软件是一种提供创建图表功能的应用程序&#xff0c;解决了手动绘制流程图比较耗费时…

Roboflow制作yolov8数据集

进入官网网页 Sign in to Roboflow 先注册&#xff0c;因为是外网&#xff0c;注册前可以选择》》fanqiangruanjian 链接&#xff1a;https://pan.baidu.com/s/1YhLxSynvtcY1_FAbhc9q0g 提取码&#xff1a;f3es Roboflow标注平台使用----小白都能看懂_李大帅哥哈哈的博客-…

spring.config.location 手动指定配置文件文件

–spring.config.locationD:\javaproject\bangsun\ds-admin\ds-oper-mgr\src\main\resources\application.yml

海康视频插件VideoWebPlugin在vue中的实现

一,将js文件放在public文件下 二,在index中全局引入 三.在视频页面写方法,创建实例,初始化,我写的是1*4屏的 <template><!--视频窗口展示--><div idplayWnd classNameplayWnd refplayWnd styleleft: 0; bottom: 0;height: 902px;width: 60vw></div>&…

五分钟理解NIO与BIO

java NIO与BIO的区别&#xff1f; BIO -- Blocking IO 即阻塞式 IO。NIO -- Non-Blocking IO, 即非阻塞式 IO 或异步 IO。 BIO 基于字节流和字符流进行操作&#xff0c;数据的读取写入必须阻塞在一个线程内等待其完成。 NIO 主要有三大核心部分&#xff1a; Channel (通道)…

行业追踪,2023-08-02

自动复盘 2023-08-02 凡所有相&#xff0c;皆是虚妄。若见诸相非相&#xff0c;即见如来。 k 线图是最好的老师&#xff0c;每天持续发布板块的rps排名&#xff0c;追踪板块&#xff0c;板块来开仓&#xff0c;板块去清仓&#xff0c;丢弃自以为是的想法&#xff0c;板块去留让…

在线帮助中心 HelpLook 的致命弱点!!!

您可以使用Helplook搭建一个高效的企业知识库。利用这个知识库&#xff0c;您的团队成员将能够更好地管理、共享和获取内部知识&#xff0c;提高工作效率和协作能力。但是也得看清楚其中的有些功能需求是否满足&#xff01;&#xff01;&#xff01; HelpLook的功能欠缺还有很多…

【SQL开发实战技巧】系列(一):关于SQL不得不说的那些事

系列文章目录 【SQL开发实战技巧】系列&#xff08;一&#xff09;:关于SQL不得不说的那些事 【SQL开发实战技巧】系列&#xff08;二&#xff09;&#xff1a;简单单表查询 【SQL开发实战技巧】系列&#xff08;三&#xff09;&#xff1a;SQL排序的那些事 【SQL开发实战技巧…

这些能帮你跨越音乐边界的音频转换器推荐给你

嘿&#xff0c;朋友,你是否曾经遇到过这样的情况&#xff1a;收集了许多喜爱的音乐&#xff0c;但发现其中一些仅仅支持wma格式&#xff0c;而你的设备却只能播放mp3&#xff1f;别担心&#xff0c;因为在这个数字化时代&#xff0c;有一个神奇的工具可以帮助你解决这个问题——…

老板说把跳针改过去,什么是主板跳针

最近骑车拍了很多视频&#xff0c;把电脑磁盘堆满了&#xff0c;想着买一条固态SSD卡扩展一下。 一咬牙一跺脚&#xff0c;直接安排&#xff0c;毫不犹豫。顺带加装了无限网卡和蓝牙5.2。 收到后立马安装。安装完发现识别不到新磁盘 确认安装没问题。然后就去问固态硬盘的客服 …

Live Market做世界C端用户数据的耕耘,数据和流量的价值呈现

在数字化时代&#xff0c;数据成为了推动经济增长和商业发展的重要资源&#xff0c;而流量则是数据价值的体现和传递媒介。随着全球互联网的普及和移动设备的智能化&#xff0c;C端用户数据的收集和分析变得尤为重要。在这个领域&#xff0c;有一家专注于世界C端用户数据耕耘的…

对象的深拷贝和浅拷贝

深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。 数据类型 数据分为基本数据类型(String, Number, Boolean, Null, Undefined&#xff0c;Symbol)和对象数据类型。 基本数据类型的特点&#xff1a;直接存储在栈(stack)中的数据引用数据类型的特点&#xff1a;存…