首页 > 其他分享 >二叉搜索树的概念与实现

二叉搜索树的概念与实现

时间:2024-09-19 09:52:28浏览次数:3  
标签:tmp right parent 二叉 概念 搜索 key root left

目录

一. 二叉搜索树的概念

 二.  二叉搜索树的实现

 1. 二叉搜索树的插入

2. 二叉搜索树的查找

3. 二叉搜索树的删除

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

1. 时间复杂度

2. 空间复杂度

四. 二叉搜索树的实现代码

五. 二叉搜索树key和key/value使用场景

1. key搜索场景

2. key/value搜索场景


一. 二叉搜索树的概念

二叉搜索树又称二叉排序树,它如果不是一棵空树,就是具有以下性质的二叉树:

若它的左子树不为空,则左子树上所有节点的值都小于等于根节点的值

若它的右子树不为空,则右子树上所有节点的值都大于等于根节点的值

它的左右子树也分别为二叉搜索树

二叉搜索树中可以支持插入相等的值,也可以不支持插入相等的值,具体看使用场景定义。map/set/multimap/multiset系列容器的底层就是二叉搜索树(红黑树),其中map/set不支持插入相等值,multimap/multiset支持插入相等值

 二.  二叉搜索树的实现

 1. 二叉搜索树的插入

插入步骤为:

1. 树为空,则直接新增节点,赋值给root指针

2. 树不空,按二叉搜索树性质,插入值比当前节点大往右走,插入值比当前节点小往左走,找到空位置,插入新节点。

3. 如果支持插入相等的值,插入值跟当前节点相等的值可以往右走,也可以往左走,找到空位置,插入新节点。

 

我们若给上图插入一个元素7,比较过程如下图所示

递归实现代码为,因为对象不能直接访问私密成员函数所以通过this指针来替换,其中K为我们使用模版定义的类型

	void Insert(STree<K> &st, K key)
	{
		Insertdfs(st._root, key);
	}
	void Insertdfs(Node*& cur,int key)
	{
		if (cur == nullptr)
		{
			cur = new Node(key);
		}
		else
		{
			if (cur->_key >= key)
			{
				if (cur->left == nullptr)
				{
					Node* child = new Node(key);
					if (cur->_key >= key)
						cur->left = child;
					else
						cur->right = child;
				}
				else
					Insertdfs(cur->left, key);
			}
			else
			{
				if (cur->right == nullptr)
				{
					Node* child = new Node(key);
					if (cur->_key >= key)
						cur->left = child;
					else
						cur->right = child;
				}
				else
					Insertdfs(cur->right, key);
			}
		}
	}

但是循环实现插入并不比递归复杂

	void Insert(K key)
	{
		if (_root == nullptr)
			_root = new Node(key);
		else
		{
			Node* parent = _root;
			Node* child = _root;
			while (child)
			{
				if (child->_key>=key)
				{
					parent = child;
					child = parent->left;
				}
				else if (child->_key < key)
				{
					parent = child;
					child = parent->right;
				}
			}
			child = new Node(key);
			if (parent->_key >= key)
				parent->left = child;
			else
				parent->right = child;

		}
	}
2. 二叉搜索树的查找

思路步骤

1. 从根开始比较,查找x,x比根的值大则往右边走查找,x比根值小则往左边走查找。

2. 最多查找高度次,走到空还没找到的话,这个值不存在。

3. 如果不支持插入相等的值,找到x返回即可,如果支持插入相等的值,意味着有多个x存在,一般要求查找中序的第一个x。

 找到最左边的x的话可以用一个值记录下来,在更左边找到值的话就更新,否则就直到循环结束返回记录的值,没找到就返回nullptr

如下图

	Node* Find(K key)
	{
		if (_root == nullptr)
			return nullptr;
		Node* parent = _root;
		Node* tmp = _root;//先找到要删除的节点

		Node* tmpNode = nullptr;
		while (tmp)
		{
			if (tmp->_key >= key)
			{
				if(tmp->_key==key)
					tmpNode = tmp;

				parent = tmp;
				tmp = tmp->left;
			}
			else if (tmp->_key < key)
			{
				parent = tmp;
				tmp = tmp->right;
			}
		}
		return tmpNode;
	}
3. 二叉搜索树的删除

首先查找元素是否在二叉搜索树中,如果不存在,则返回false。

如果查找元素存在则分一下四种情况分别处理:(假设要删除的节点为N)

1. 要删除的节点N左右节点孩子均为空

2. 要删除的节点N左孩子为空,右孩子不为空

3. 要删除的节点N右孩子为空,左孩子不为空

4. 要删除的节点N左右孩子均不为空

 四种情况如下图所示

 四种情况的解决方法如下

1. 把N节点的父亲对应孩子指针指向空,直接删除N节点(情况1可以当做2或者3处理,效果一样)。

2. 把N节点的父亲对应孩子指针指向N的右孩子,直接删除N节点(由于情况1左右孩子都是空话所以将N节点的父亲指向N的左右孩子都可以)

3. 把N节点的父亲对应孩子指针指向N的左孩子,直接删除N节点

4. 无法直接删除N节点,因为N的两个孩子就无处安放了,只能用替换法删除。找N左子树的值最大的节点(左子树的最右节点)或者N右子树的值最小节点(右子树最左节点)代替N,因为这两个节点任意一个放到N的位置都满足二叉搜索树的规则。代替N的意思就是N和所找到节点的值进行交换,转而变成删除我们所找到的节点,此节点一定符合情况2和情况三可以直接删除

 利用递归实现的话先利用this指针将根节点传到实现递归的函数,如下所示

	bool Erase(STree<K>& st, K key)
	{
		return Erasedfs(st._root, nullptr, key);
	}

其中第二个参数用来记录,我们寻找节点的父亲节点,用来连接新节点。

前三种情况,我们如果找到了要删除的节点但是其是根节点,那直接改变成员变量_root即可,反之利用parent来连接节点,实现代码如下

	bool Erasedfs(Node* tmp,Node* parent,K key)
	{
		if (tmp == nullptr)
			return false;
		if (tmp->_key > key)
		{
			return Erasedfs(tmp->left,tmp, key);
		}
		else if (tmp->_key < key)
			return Erasedfs(tmp->right,tmp, key);

		//四种情况
		if (tmp->left == nullptr)//将tmp是叶子节点的情况归到这里
		{
			if (tmp == _root)//是根节点的情况
				_root = tmp->right;
			else
			{
				if (parent->left == tmp)
				{
					parent->left = tmp->right;
				}
				else
				{
					parent->right = tmp->right;
				}
			}

			delete tmp;
			return true;
		}
		else if (tmp->right == nullptr)
		{
			if (tmp == _root)
				_root = tmp->right;
			else
			{
				if (parent->left == tmp)
				{
					parent->left = tmp->left;
				}
				else
				{
					parent->right = tmp->left;
				}
			}

			delete tmp;
			return true;
		}
		else//两边都有
		{
			//寻找左子树最大值,或者右子树最小值
			parent = tmp;
			Node* rbig = tmp->left;//左子树最大值
			while (rbig->right)
			{
				parent = rbig;
				rbig = rbig->right;
			}
			tmp->_key = rbig->_key;
			if (parent->left == rbig)
				parent->left = rbig->left;
			else
				parent->right = rbig->left;

			delete rbig;
			return true;
		}

	}

不利用递归思路也是如此,如果成员变量(树的根)是空就直接返回反之先找到要删除的节点,找不到就返回false找到了就根据情况执行删除操作,但是只需要一个参数,其余自己定义即可

	bool Erase(K key)
	{
		if (_root == nullptr)
			return false;
		Node* parent = _root;
		Node* tmp=_root;//先找到要删除的节点
		while (tmp)
		{
			if (tmp->_key > key)
			{
				parent = tmp;
				tmp = tmp->left;
			}
			else if (tmp->_key < key)
			{
				parent = tmp;
				tmp = tmp->right;
			}
			else
			{
				//四种情况
				if (tmp->left == nullptr)//将tmp是叶子节点的情况归到这里
				{
					if (tmp == _root)
						_root = tmp->right;
					else
					{
						if (parent->left == tmp)
						{
							parent->left = tmp->right;
						}
						else
						{
							parent->right = tmp->right;
						}
					}

					delete tmp;
					return true;
				}
				else if (tmp->right == nullptr)
				{
					if (tmp == _root)
						_root = tmp->right;
					else
					{
						if (parent->left == tmp)
						{
							parent->left = tmp->left;
						}
						else
						{
							parent->right = tmp->left;
						}
					}

					delete tmp;
					return true;
				}
				else//两边都有
				{
					//寻找左子树最大值,或者右子树最小值
					parent = tmp;
					Node* rbig = tmp->left;//左子树最大值
					while (rbig->right)
					{
						parent = rbig;
						rbig = rbig->right;
					}
					tmp->_key = rbig->_key;
					if (parent->left == rbig)
						parent->left = rbig->left;
					else
						parent->right = rbig->left;

					delete rbig;

				}
			}
		}

		return false;
	}

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

1. 时间复杂度

最好情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其高度为:O(㏒₂ N)

最差情况下,二叉搜索树退化为单支树(或者类似单支),其高度为:O(½N)

综合而言二叉搜索树的增删查改时间复杂度为:O(N)

这样的效率还是太慢了,所以就有了平衡搜索二叉树AVL树和红黑树才能适用于我们在内存中的存储和搜索数据。

二分查找也可以实现O(longN)级别的查找效率,但是二分查找有两大缺陷:

1. 需要存储在支持下标随机访问的结构中,并且有序。

2. 插入和删除数据效率很低,因为存储在下标随机访问的结构中,插入和删除数据一般需要挪动数据。

2. 空间复杂度

O(N)

四. 二叉搜索树的实现代码

#include<iostream>
#include<algorithm>

using namespace std;

template<class K>
class STNode
{
	//typedef STNode<K> Node;
	using Node = STNode<K>;
public:
	STNode(K key)
	{
		_key = key;
		Node* left = nullptr;
		Node* right = nullptr;
	}
	K _key;
	Node* left = nullptr;
	Node* right = nullptr;
};

template<class K>
class STree
{
	typedef STNode<K> Node;
public:
	//STree() = default;
	STree()
	{

	}
	STree(const STree<K>& st)
	{
		_root = copy(st._root);
	}
	~STree()
	{
		Destroy(_root);
	}

	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;
	}
	void Insert(STree<K> &st, K key)
	{
		Insertdfs(st._root, key);
	}
	void Insertdfs(Node*& cur,int key)
	{
		if (cur == nullptr)
		{
			cur = new Node(key);
		}
		else
		{
			if (cur->_key >= key)
			{
				if (cur->left == nullptr)
				{
					Node* child = new Node(key);
					if (cur->_key >= key)
						cur->left = child;
					else
						cur->right = child;
				}
				else
					Insertdfs(cur->left, key);
			}
			else
			{
				if (cur->right == nullptr)
				{
					Node* child = new Node(key);
					if (cur->_key >= key)
						cur->left = child;
					else
						cur->right = child;
				}
				else
					Insertdfs(cur->right, key);
			}
		}
	}
	void Insert(K key)
	{
		if (_root == nullptr)
			_root = new Node(key);
		else
		{
			Node* parent = _root;
			Node* child = _root;
			while (child)
			{
				if (child->_key>=key)
				{
					parent = child;
					child = parent->left;
				}
				else if (child->_key < key)
				{
					parent = child;
					child = parent->right;
				}
			}
			child = new Node(key);
			if (parent->_key >= key)
				parent->left = child;
			else
				parent->right = child;

		}
	}
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
	Node* Find(K key)
	{
		if (_root == nullptr)
			return nullptr;
		Node* parent = _root;
		Node* tmp = _root;//先找到要删除的节点

		Node* tmpNode = nullptr;
		while (tmp)
		{
			if (tmp->_key >= key)
			{
				if(tmp->_key==key)
					tmpNode = tmp;

				parent = tmp;
				tmp = tmp->left;
			}
			else if (tmp->_key < key)
			{
				parent = tmp;
				tmp = tmp->right;
			}
		}
		return tmpNode;
	}

	bool Erase(STree<K>& st, K key)
	{
		return Erasedfs(st._root, nullptr, key);
	}
	bool Erasedfs(Node* tmp,Node* parent,K key)
	{
		if (tmp == nullptr)
			return false;
		if (tmp->_key > key)
		{
			return Erasedfs(tmp->left,tmp, key);
		}
		else if (tmp->_key < key)
			return Erasedfs(tmp->right,tmp, key);

		//四种情况
		if (tmp->left == nullptr)//将tmp是叶子节点的情况归到这里
		{
			if (tmp == _root)//是根节点的情况
				_root = tmp->right;
			else
			{
				if (parent->left == tmp)
				{
					parent->left = tmp->right;
				}
				else
				{
					parent->right = tmp->right;
				}
			}

			delete tmp;
			return true;
		}
		else if (tmp->right == nullptr)
		{
			if (tmp == _root)
				_root = tmp->right;
			else
			{
				if (parent->left == tmp)
				{
					parent->left = tmp->left;
				}
				else
				{
					parent->right = tmp->left;
				}
			}

			delete tmp;
			return true;
		}
		else//两边都有
		{
			//寻找左子树最大值,或者右子树最小值
			parent = tmp;
			Node* rbig = tmp->left;//左子树最大值
			while (rbig->right)
			{
				parent = rbig;
				rbig = rbig->right;
			}
			tmp->_key = rbig->_key;
			if (parent->left == rbig)
				parent->left = rbig->left;
			else
				parent->right = rbig->left;

			delete rbig;
			return true;
		}

	}
	bool Erase(K key)
	{
		if (_root == nullptr)
			return false;
		Node* parent = _root;
		Node* tmp=_root;//先找到要删除的节点
		while (tmp)
		{
			if (tmp->_key > key)
			{
				parent = tmp;
				tmp = tmp->left;
			}
			else if (tmp->_key < key)
			{
				parent = tmp;
				tmp = tmp->right;
			}
			else
			{
				//四种情况
				if (tmp->left == nullptr)//将tmp是叶子节点的情况归到这里
				{
					if (tmp == _root)
						_root = tmp->right;
					else
					{
						if (parent->left == tmp)
						{
							parent->left = tmp->right;
						}
						else
						{
							parent->right = tmp->right;
						}
					}

					delete tmp;
					return true;
				}
				else if (tmp->right == nullptr)
				{
					if (tmp == _root)
						_root = tmp->right;
					else
					{
						if (parent->left == tmp)
						{
							parent->left = tmp->left;
						}
						else
						{
							parent->right = tmp->left;
						}
					}

					delete tmp;
					return true;
				}
				else//两边都有
				{
					//寻找左子树最大值,或者右子树最小值
					parent = tmp;
					Node* rbig = tmp->left;//左子树最大值
					while (rbig->right)
					{
						parent = rbig;
						rbig = rbig->right;
					}
					tmp->_key = rbig->_key;
					if (parent->left == rbig)
						parent->left = rbig->left;
					else
						parent->right = rbig->left;

					delete rbig;

				}
			}
		}

		return false;
	}
private:
	void _InOrder(Node* root)//可以在外面再弄一个通过this指针访问
	{
		if (root == nullptr)
			return;
		_InOrder(root->left);
		cout << root->_key << "   ";
		_InOrder(root->right);
	}

	Node* _root = nullptr;
};

int main()
{
	STree<int> t;
	int a[10] = { 8,3,10,1,3,6,14,4,7,13 };
	for (auto e : a)
	{
		t.Insert(t,e);
	}
	//STNode<int>* node = t.Find(3);

	//cout << node->_key << endl;
	//cout << (node->right == nullptr) << endl;
	t.InOrder();

	//for (auto e : a)
	//{
	//	t.Erase(t,e);
	//	t.InOrder();
	//}

	return 0;
}

五. 二叉搜索树key和key/value使用场景

1. key搜索场景

只有key作为关键码,结构中只需要存储key即可,关键码即为需要搜索到的值,搜索场景只需要判断key在不在。key的搜索场景支持增删差但不支持修改,因为修改key会破坏搜索树的结构。

使用场景1:

小区无人值守⻋库,小区车库买了⻋位的业主车才能进小区,那么物业会把买了车位的业主的车牌号录⼊后台系统,⻋辆进入时扫描⻋牌在不在系统中,在则抬杆,不在提示本小区⻋辆,无法进⼊。

使用场景2 :

检查⼀篇英文文章单词拼写是否正确,将词库中所有单词放⼊⼆叉搜索树,读取文章中的单 词,查找是否在⼆叉搜索树中,不在则波浪线标红提示。

2. key/value搜索场景

每一个关键码key,都有与之对应的值value,value可以是任意对象。树的节点中除了存储key还要存储与之对应的的value,增删查还是以key为关键字走二叉搜索树的规则进行比较,可以快速查找key对应的value。key/value的搜索场景实现的二叉搜索树支持修改,但是不支持修改key,修改key还是会破坏树的结构,可以修改value。

使用场景1:

简单中英互译字典,树的结构中(结点)存储key(英⽂)和vlaue(中文),搜索时输⼊英文,则同时查找到了英文对应的中⽂。

使用场景2:

商场无人值守车库,入口进场时扫描⻋牌,记录车牌和⼊场时间,出口离场时,扫描车牌,查找入场时间,用当前时间-入场时间计算出停⻋时长,计算出停车费用,缴费后抬杆,车辆离场。

使用场景3:

统计⼀篇文章中单词出现的次数,读取⼀个单词,查找单词是否存在,不存在这个说明第⼀次出现,(单词,1),单词存在,则++单词对应的次数。


这篇就到这里啦,ヾ( ̄▽ ̄)Bye~Bye~

标签:tmp,right,parent,二叉,概念,搜索,key,root,left
From: https://blog.csdn.net/m0_68142120/article/details/142319042

相关文章

  • SEO专家们齐聚一堂,畅谈搜索 [播客]
    Wix刚刚庆祝了他们的第100期播客!恭喜,Wix。正如Wix的SEO品牌主管MordyOberstein所说:“我们谈了很多。”确实如此!你们确实有很多有趣的事情要说。第100期“SERPsUp”节目邀请了许多精彩的嘉宾。以下是节目的摘要。除了我们熟悉的面孔,Oberstein和SEO通讯主管CrystalCarter,......
  • C/C++语言基础--C++面向对象、类、对象概念讲解
    本专栏目的更新C/C++的基础语法,包括C++的一些新特性前言今天更新的比较晚了,主要一直用是谷歌Colab训练模型,访问国内csdn反而不好使了,请大家见谅;C++是面向对象的语言,本文将介绍什么是面向对象、什么是类、什么是对象、类和对象的关系是什么?欢迎大家点赞+收藏+关注;C语......
  • Day18 二叉树part08| LeetCode 669. 修剪二叉搜索树 , 108.将有序数组转换为二叉搜索树
    669.修剪二叉搜索树669.修剪二叉搜索树classSolution{publicTreeNodetrimBST(TreeNoderoot,intlow,inthigh){if(root==null)returnnull;//处理节点值<low的情况:当前节点及其左子树的所有节点都不在范围内,继续在其右子树上修......
  • 吴恩达机器学习课程 笔记1 概念
    主要的人工智能分支人工智能(AI)是一个广泛的领域,包含了多个子领域或分支,每个分支都专注于解决特定类型的问题或执行特定的任务。以下是一些主要的人工智能分支:机器学习(MachineLearning):这是AI的一个核心部分,专注于构建可以从数据中学习并作出决策或预测的系统。深度学习(D......
  • 【数据结构】二叉树的遍历
    堆的应用堆排列堆排序即利用堆的思想来进行排序,总共分为两个步骤:建堆升序:建大堆降序:建小堆利用堆删除思想来进行排序建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。voidHeapSort(int*a,intn){ //a数组直接建堆O(N) for(inti=......
  • 大数据-123 - Flink 并行度 相关概念 全局、作业、算子、Slot并行度 Flink并行度设置
    点一下关注吧!!!非常感谢!!持续更新!!!目前已经更新到了:Hadoop(已更完)HDFS(已更完)MapReduce(已更完)Hive(已更完)Flume(已更完)Sqoop(已更完)Zookeeper(已更完)HBase(已更完)Redis(已更完)Kafka(已更完)Spark(已更完)Flink(正在更新!)章节内容上节我们完成了如下的内容:FlinkTimeWatermarkJava代码实例测试简单介......
  • 概念:光学中的Tanalov变换
    1.Tanalov变换的背景在经典光学中,傅里叶光学提供了一种有效的方法来描述光束的传播。傅里叶变换将空间中的光场转化为频域(波矢域)表示,可以简化光场的分析,特别是在自由空间或均匀介质中的传播。然而,在非均匀或非对称系统中,傅里叶变换有时无法准确处理复杂的相位变化和空间分布......
  • 【DFS深度优先搜索】纵火犯
    【问题描述】给你一块n*m的草坪,问如果只点一次火,最多能烧多少块草坪。可以从n*m的草地中任意一个地方开始点火,火只能往上下左右传递,没有草的地方不能燃烧。【输入格式】输入由多个测试例组成。每个测试例的第一行含两个整数n和m,(1<=n,m<=100),分别表示01矩阵的行数与列......
  • 源码包和 RPM 包是两种常见的 Linux 软件包形式,它们各有特点和适用场景。下面是这两种
    概念源码包:定义:源码包包含了软件的源代码,用户需要自己下载源码包,然后进行编译和安装。优点:用户可以根据自己的需求定制编译选项,选择安装哪些功能模块,还可以查看和修改源代码。缺点:安装过程较为复杂,需要一定的技术知识,而且安装速度相对较慢。RPM包:定义:RPM(RedHatPackageManager)是......
  • 详细的解释Rust语言中所增加的新概念
    Rust是一门注重性能和安全性的系统级编程语言,其设计目标之一是避免传统系统编程语言(如C和C++)中常见的内存管理错误。为实现这些目标,Rust引入了一些新的编程概念,这些概念是Rust的核心,帮助开发者编写出高效、安全且易于维护的代码。以下是Rust中一些重要的新概念及其详细解......