搜索二叉树详细介绍C++

`

文章目录

  • 前言
  • 一、搜索二叉树介绍
  • 二、二叉搜索树实现
    • 1.查找
    • 2.插入
    • 3.删除
  • 三、二叉搜索树递归实现
    • 1.查找
    • 2.插入
    • 3.删除
  • 四、二叉搜索树性能分析
  • 五、二叉搜索树应用
    • 1.K模型
    • 2.KV模型
  • 总结


前言

在本篇文章中,我们将会学到数据结构中有关二叉树中一种特殊的结构-----搜索二叉树,它的主要目的就是为了搜索,我们还将会对搜索二叉树进行模拟实现。

一、搜索二叉树介绍

二叉树我们都已经学习过了,二叉搜索树就是对普通二叉树进行一些限制,有一些新的功能。
二叉搜索树又称二叉排序树,二叉查找树

如果它的左子树不为空,则左子树上所有结点的值都小于根节点的值。
如果它的右子树不为空,则右子树上所有结点的值都大于根节点的值。
左右子树也分别是一颗二叉搜索树

我们来看几个二叉树是不是二叉搜索树
在这里插入图片描述
这个就是一个二叉搜索树

在这里插入图片描述
这个就不是一个二叉搜索树,对于10这个节点,右节点的值必须大于这个10

性质:二叉搜索树中序遍历是一个递增序列

二、二叉搜索树实现

普通的数据结构就是增删查改,但是在二叉搜索树是不允许修改的。对于存储重复值也无意义。

我们先看一下二叉搜索树框架结构

template<class K>
struct TreeeNode
{
	TreeeNode(const K& val)
		:_left(nullptr)
		,_right(nullptr)
		,_val(val)
	{}
	struct TreeeNode* _left;
	struct TreeeNode* _right;
	K _val;
};

template<class K>
class BSTree
{
public:
       //功能实现
private:
	typedef TreeeNode<K> Node;
	Node* _root=nullptr;

};

1.查找

我们利用二叉搜索树的性质查找,从根开始查找,进行一次遍历。
如果这个值比根节点大,我们就去右边查找。
如果这个值比根节点小,我们就去左边查找。
如果最后走到空,还没有找到,就说明这个值不存在。

bool find(const K& val)
{
	Node* cur = _root;
	while (cur)
	{
		if (cur->_val > val)
		{
			cur = cur->_left;
		}
		else if (cur->_val < val)
		{
			cur = cur->_right;
		}
		//相等不插入
		else
		{
			return true;
		}
	}
	return false;

}

2.插入

插入的逻辑也和查找的逻辑相似,我们需要分情况判断:
⭐️如果树为空,就新增节点,赋值给root,相等时我们不插入
⭐️树不为空,就按查找逻辑,找到合适的位置进行插入

我们找到合适的位置之和,还需要与上面的树进行链接,所以我们在进行查找的时候,还需要用一个父节点记录位置,方便链接

bool insert(const K&  val)
{
	//找到合适的位置插入

	if (_root == nullptr)
	{
		Node* newnode = new Node(val);
		_root = newnode;
		return true;
	}
	Node* cur = _root;
	//记录父亲
	Node* parent = _root;
	while (cur)
	{
		if (cur->_val > val)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (cur->_val < val)
		{
			parent = cur;
			cur = cur->_right;
		}
		//相等不插入
		else
		{
			return false;
		}
	}
	Node* newnode = new Node(val);
	//判断插入哪边,用值比较
	if (parent->_val >val)
	{
		parent->_left = newnode;
		return true;
	}
	else
	{
		parent->_right = newnode;
		return true;
	}
}

3.删除

首先查找这个元素是不是存在,不存在我们直接返回,如果存在可能会出现下面四种情况:
🌟叶子节点:直接删除
在这里插入图片描述
比如删除1,找到它的父亲,删除1,再将父亲指向的这个节点制空
🌟右子树为空,左子树不为空
在这里插入图片描述
比如删除14,我们找到14的父节点,cur的左子树连接到父亲节点,在删除当前节点
🌟左子树为空,右子树不为空
这个也是同样的道理

这三种情况可以进行合并为两种:
删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点–直接删除
删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点–直接删除

🌟左右子树都存在

在这里插入图片描述

我们想要删除3,怎末删除呢??删除之后这棵树还保持原有的特性

这里我们采用替换法删除,找一个可以替换的节点,交换两个位置的值,再删除这个替换节点。

什么节点可以进行替换呢??我们有两种选择:

左子树最右节点,也就是左子树中值最大的那个节点
右子树最左节点,也就是右子树中值最小的那个节点

我们采用右子树最左节点来进行操作,

	//删除
	bool erase(const K&val)
	{
		Node* cur = _root;
		//先找到删除节点
		
		//记录父亲
		Node* parent = _root;
		while (cur)
		{
			if (cur->_val > val)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (cur->_val < val)
			{
				parent = cur;
				cur = cur->_right;
			}
			//相等找到了
			else
			{
			    //删除操作
			}
	   }
    }

🧐🧐我们先看前两种情况

//左子树为空
if (cur->_left == nullptr)
{
	if (cur == parent->_left)
	{
		parent->_left = cur->_right;
	}
	else
	{
		parent->_right = cur->_right;
	}
	delete cur;
	return true;
}
//右子树为空
else if (cur->_right == nullptr)
{
	if (cur == parent->_left)
	{
		parent->_left = cur->_left;
	}
	else
	{
		parent->_right = cur->_left;
	}

	delete cur;
	return true;
}

我们看一下这这段代码????
在这里插入图片描述
一般情况确实符合,我们来看一下面这种极端情况
在这里插入图片描述

如果我们想要删除8,我们来走一下代码
parent和cur都指向8这个节点,这个if,else语句不成立,根本就不会进去,就把这个节点给删除了,那我们也拿不到后面的节点了。
所以我们需要特殊处理。root直接指向它的孩子

 if (cur->_left == nullptr)
{
		 if (_root == cur)
		 {
			 _root = _root->_right;
		 }
		 else
		 {
			 if (cur == parent->_left)
			 {
				 parent->_left = cur->_right;
			 }
			 else
			 {
				 parent->_right = cur->_right;
			 }

		 }
		 delete cur;
		 return true;
}
//右子树为空
else if (cur->_right == nullptr)
{
		 if (_root == cur)
		 {
			 _root = _root->_left;
		 }
		 else
		 {
			 if (cur == parent->_left)
			 {
				 parent->_left = cur->_left;
			 }
			 else
			 {
				 parent->_right = cur->_left;
			 }
		 }
		delete cur;
		return true;
}

🧐🧐我们看一下两个节点都存在的情况

替换法:找到右子树的最左节点进行,找到后交换两者的值,最后删除这个替换节点,再通过这个替换节点的父亲把这个替换节点维护起来(可能存在右孩子)

我们以删除3为例子
在这里插入图片描述

首先找到替换节点4,标记为RightMin,顺表找到它的父亲节点RightParent 。交换两个节点的值
在这里插入图片描述

接下来我们就可以对3进行删除,同时维护这个节点
在这里插入图片描述

我们看一下代码实现`

else
{
	//替换法删除

	//先找到替换的位置和父亲,右子树最左节点
	Node* RightMin = cur->_right;
	Node* RightParent = cur;
	while (RightMin->_left)
	{
		RightParent = RightMin;
		RightMin = RightMin->_left;
	}
	//交换
	std::swap(cur->_val, RightMin->_val);
	RightParent->_left = RightMin->_right;


	delete RightMin;
	return true;

}

这样我们就实现了这个删除操作,但是我们看一下是不是有什么情况没有考虑呢??

我们看一下这种情况,我们要删除8
在这里插入图片描述
我们交换了之后
在这里插入图片描述

我们发现没有左子树,这句代码RightParent->_left = RightMin->_right;就会崩溃,空指针的访问。
我们先还需要特殊处理

else
{
	//替换法删除

	//先找到替换的位置和父亲,右子树最左节点
	Node* RightMin = cur->_right;
	Node* RightParent = cur;
	while (RightMin->_left)
	{
		RightParent = RightMin;
		RightMin = RightMin->_left;
	}
	//交换
	std::swap(cur->_val, RightMin->_val);
	if (RightParent->_left == RightMin)
		RightParent->_left = RightMin->_right;
	else
		RightParent->_right = RightMin->_right;

	delete RightMin;
	return true;

}

完整代码

template<class K>
class BSTree
{
public:
	//强制生成默认构造
	BSTree() = default;
	//插入
	bool insert(const K&  val)
	{
		//找到合适的位置插入

		if (_root == nullptr)
		{
			Node* newnode = new Node(val);
			_root = newnode;
			return true;
		}
		Node* cur = _root;
		//记录父亲
		Node* parent = _root;
		while (cur)
		{
			if (cur->_val > val)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (cur->_val < val)
			{
				parent = cur;
				cur = cur->_right;
			}
			//相等不插入
			else
			{
				return false;
			}
		}
		Node* newnode = new Node(val);
		//判断插入哪边,用值比较
		if (parent->_val >val)
		{
			parent->_left = newnode;
			return true;
		}
		else
		{
			parent->_right = newnode;
			return true;
		}
	}
	//查找
	bool find(const K& val)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_val > val)
			{
				cur = cur->_left;
			}
			else if (cur->_val < val)
			{
				cur = cur->_right;
			}
			//相等不插入
			else
			{
				return true;
			}
		}
		return false;

	}
	//中序遍历

	//采用递归,必须要进行参数传递,封装一层;
	void Inorder()
	{
		_Inorder(_root);
	}

	//删除
	bool erase(const K&val)
	{
		Node* cur = _root;
		//先找到删除节点
		
		//记录父亲
		Node* parent = _root;
		while (cur)
		{
			if (cur->_val > val)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (cur->_val < val)
			{
				parent = cur;
				cur = cur->_right;
			}
			//相等找到了
			else
			{
				//左子树为空
				 if (cur->_left == nullptr)
				{
					 if (_root == cur)
					 {
						 _root = _root->_right;
					 }
					 else
					 {
						 if (cur == parent->_left)
						 {
							 parent->_left = cur->_right;
						 }
						 else
						 {
							 parent->_right = cur->_right;
						 }

					 }
					 delete cur;
					 return true;
				}
				//右子树为空
				else if (cur->_right == nullptr)
				{
					 if (_root == cur)
					 {
						 _root = _root->_left;
					 }
					 else
					 {
						 if (cur == parent->_left)
						 {
							 parent->_left = cur->_left;
						 }
						 else
						 {
							 parent->_right = cur->_left;
						 }
					 }
					delete cur;
					return true;
				}
				//两个孩子
				else
				{
					//替换法删除

					//先找到替换的位置和父亲,右子树最左节点
					Node* RightMin = cur->_right;
					Node* RightParent = cur;
					while (RightMin->_left)
					{
						RightParent = RightMin;
						RightMin = RightMin->_left;
					}
					//交换
					std::swap(cur->_val, RightMin->_val);
					if(RightParent->_left==RightMin)
					      RightParent->_left = RightMin->_right;
					else
						RightParent->_right = RightMin->_right;

					delete RightMin;
					return true;

				}
			}
		}
		return false;
	}
 private:
  typedef TreeeNode<K> Node;
  Node* _root=nullptr;
};

三、二叉搜索树递归实现

1.查找

我们查好就很容易完成了,这个值大,就去右边找。这个值小就去左边找。找到空了就返回。逻辑很简单。

不同的是我们需要封装一层,外边传递的参数不符合我们递归的参数。

bool Rfind(const K& val)
{
   return _Rfind(val, _root);
}


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

2.插入

找到合适的位置进行插入就可以.

void Rinsert(const K& val)
{
   _Rinsert(val,_root);
}

我们需要注意的是如何进行链接并且返回。
在这里插入图片描述
我们想要插入15,我们如何通过递归找到这个父节点???

bool _Rinsert(const K& val,Node*& root)这就可以完成

在这里插入图片描述
传参传递的是引用,这里的root是上一层root->right的别名。
所以实际上就是让14指向了15。这个引用只在最后一层起作用。
在这里插入图片描述

bool _Rinsert(const K& val,Node*& root)
{
	if (root == nullptr)
	{
		//开节点插入
		root= new Node(val);
		return true;
	}
	if (root->_val > val)
	{
		return _Rinsert(val, root->_left);
	}
	else if(root->_val < val)
	{
		return _Rinsert(val, root->_right);
	}
	else
	{
		return false;
	}
}

3.删除

找到合适的位置删除,但是这里的删除要分情况删除。

一个孩子的情况很简单,我们主要看一下两个孩子的情况

我们还是需要用到替换法删除,但是我们不需要找父亲了;我们这里有引用。
我们这里转换成去这棵树进行删除。直接将要删除的节点有两个孩子,变成只有一个孩子或是没有孩子的情况

void Rerase(const K& val)
{
   _Rerase(val, _root);
}

	bool _Rerase(const K& val, Node*& root)
	{
	//这里所有情况都包含,我们不用找父亲了。
		if (root == nullptr)
		{
			return false;
		}
		if (root->_val > val)
		{
			return _Rerase(val, root->_left);
		}
		else if (root->_val < val)
		{
			return _Rerase(val, root->_right);
		}
		//找到了
		else
		{
			Node* del = root;
			//判断一个孩子情况
			if (root->_left == nullptr)
			{
				root = root->_right;
			}
			else if (root->_right == nullptr)
			{
				root = root->_left;
			}
			//两个孩子
			else
			{
				//替换法删除.右子树最左节点
				Node* RightMin = root->_right;
				while (RightMin->_left)
				{
					RightMin = RightMin->_left;
				}
				//交换
				std::swap(RightMin->_val, root->_val);
				//转换成去这棵树删除
				return _Rerase(val, root->_right);
			}
			delete del;
		}
		return true;
	}

四、二叉搜索树性能分析

插入和删除之前都必须先进行查找,那么查找的效率就是二叉搜索树中操作的性能。

我们可以发现,最差就是查找高度次,有人说,这不就是logn吗!!

但是对于同一个关键码集合,插入的顺序不同,极有可能得到不同的·二叉搜索树。
在这里插入图片描述
💗最优的情况:二叉搜索树为完全二叉树,平均比较次数就是logn
💗最差的情况:就像右图所示,退化为类似单只,平均比较次数就是n

如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插
入关键码,二叉搜索树的性能都能达到最优?那么我们后续章节学习的AVL树和红黑树就可以上
场了

五、二叉搜索树应用

1.K模型

k模型只有key作为关键字,在结构中只需要存储key就可以。

使用:
🌟日常生活中我们开车进入小区中,会进行车牌的扫描,快速查找,看你是否是这个小区的车型,若果是抬杆,进去。如果不存在,就进不去
🌟查看一个单词是否拼写错误,在词库中所有单词构建一颗二叉搜索树,在二叉搜索树中查找该单词是否存在。

2.KV模型

每一个对应的key都有一个与之对应的value,即<K,V>键值对

使用:
🌟高铁刷身份进站,每一个乘客都有一个身份证号,如果在平台上买票了,就会更新这个身份信息。
🌟查找某个单词的意思,<中文,英文>也构成了键值对
🌟统计单词出现的个数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出
现次数就是<word, count>就构成一种键值对。

总结

以上就是今天要讲的内容,本文仅仅详细介绍了二叉搜索树的相关内容。希望对大家的学习有所帮助,仅供参考 如有错误请大佬指点我会尽快去改正 欢迎大家来评论~~ 😘 😘 😘

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

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

相关文章

如何在多个地理位置的企业中部署SD-WAN?

企业业务的全球化和分布式办公模式的普及&#xff0c;跨地域的网络连接变得至关重要。SD-WAN&#xff08;软件定义广域网&#xff09;技术为企业提供了一种灵活、高效、安全的网络解决方案。本文将介绍如何在多个地理位置的企业中部署SD-WAN&#xff0c;以提高网络性能和管理效…

HTML基本元素

文章目录 如何制作标题如何制作文字如何做粗体字检查我们程序码给输出文字添加属性 HTML 一个HTML标签包含着&#xff1a; 起始标签&#xff1a;它包含了元素的名字&#xff0c;夹在一对 <、>&#xff08;尖括号&#xff09;之间。它指明元素从何处开始生效。结束标签&am…

Transformer - 注意⼒机制

Transformer - 注意⼒机制 flyfish 计算过程 flyfish # -*- coding: utf-8 -*-import torch import torch.nn as nn import torch.nn.functional as F import os import mathdef attention(query, key, value, maskNone, dropoutNone):# query的最后⼀维的⼤⼩, ⼀般情况下就…

工业项目能耗管理可以看这个开源项目

软件介绍 Scaphandre是一个专注于电力和能源消耗指标的计量代理&#xff0c;旨在为公司和个人提供测量技术服务功耗的便捷工具&#xff0c;并以便于理解的方式获取数据。其名字来源于法语中的潜水服或潜水器&#xff0c;象征着深入测量和揭示技术服务耗能量的意图。 功能特点 …

2024轨道交通、运输工程、供应链管理国际会议(RTTESCM2024)

2024轨道交通、运输工程、供应链管理国际会(RTTESCM2024) 会议简介 2024轨道交通、运输工程、供应链管理国际会议(RTTESCM2024)组委会诚挚地邀请您参加这次将在厦门举行的会议。 RTTESCM2024会议旨在为轨道交通、运输工程和供应链管理领域的专家学者提供一个平台&#xff0c;…

观察和配置MAC地址表

目录 原理概述 实验目的 实验内容 实验拓扑 ​编辑1&#xff0e;基本配置 2.观察正常状态时的MAC地址表 4.配置静态MAC地址表项 原理概述 MAC 地址表是交换机的一个核心组成部分&#xff0c;交换机主要是根据 MAC 地址表来进行帧的转发的。交换机对帧的转发操作行为一共有…

Mac上怎么合并多张图片?

Mac上怎么合并多张图片&#xff1f;上班过的小伙伴都应该知道&#xff0c;合并拼接图片是一件非常重要且经常需要使用到的图片处理技术&#xff0c;将多张图片合并拼成一张之后能够展现出更多的图片内容。在Mac电脑上&#xff0c;合并多张图片是一项常见的任务&#xff0c;无论…

智慧InSAR专题———模拟数据实现现实场景异常形变点识别(论文解读)

文章目录 &#xff08;近期想静下心回顾近期看的佳作&#xff0c;会写一下自己的总结&#xff0c;大家如果对此系列感兴趣&#xff0c;每周踢一下我&#xff0c;周更&#xff0c;持续更新&#xff09;0 前言1 Automated deformation detection and interpretation using InSAR …

财报解读:首次全年盈利的奈雪的茶,正越来越“接地气”

从2021年6月到2023年底&#xff0c;上市的奈雪的茶用一年半的时间&#xff0c;终于进入了自己的“盈利时代”。 根据奈雪的茶近日披露的财报&#xff0c;2023年&#xff0c;公司营收51.64亿元&#xff0c;同比增长20.3%&#xff1b;经调整净利润2090万元&#xff0c;上年同期亏…

HackTheBox-Machines--Builder

文章目录 1 端口扫描2 测试思路3 漏洞测试 Builder测试过程 1 端口扫描 nmap -sC -sV 10.129.230.2202 测试思路 系统开启了22和8080端口&#xff0c;22端口无账号密码&#xff0c;测试方向主要从8080的jenkins服务开始测试。 在测试开源系统时&#xff0c;可以下载源代码或本地…

C++ 哈希思想应用:位图,布隆过滤器,哈希切分

C 哈希思想应用:位图,布隆过滤器,哈希切分 一.位图1.位图的概念1.问题2.分析3.位图的概念4.演示 2.位图的操作3.位图的实现1.char类型的数组2.int类型的数组3.解决一开始的问题位图开多大呢?小小补充验证 4.位图的应用1.给定100亿个整数&#xff0c;设计算法找到只出现一次的整…

aardio plus滑尺滑块垂直

在滑尺调色工具中&#xff0c;将“控件宽度”调到小于“控件高度”时&#xff0c;就看到滑块滑条变成垂直的了。 一年前下载了aardio&#xff0c;刚接触&#xff0c;看到plus&#xff0c;滑尺配色工具&#xff0c;然后就试了一下&#xff0c;结果一直没找到怎么把plus滑尺调节…

Flash选型确认

3.1 NOR Flash选型 容量&#xff1a;容量大小一般是我们首先要考虑的因素&#xff0c;Flash的大小一般用bit表示&#xff0c;容量范围涵盖512Kb~512Mb。例如型号GD25Q64C容量就为64Mb8MB。 供电电压&#xff1a;Nor Flash的供电电压一般分为四种&#xff1a;2.7V~3.6V&#x…

盘点AI编程效率神器合集,代码助手工具大模型、Agent智能体

关注wx公众号:aigc247 进社群加wx号&#xff1a;aigc365 程序员是最擅长革自己命的职业&#xff0c;让我们借助AI的力量一起摸鱼一起卷&#xff01; 据说好用的AI代码助手工具、大模型、Agent智能体 微软的compoliot&#xff1a;AI神器之微软的编码助手Copilot-CSDN博客 阿…

Stream流,线程

文章目录 Stream流思想作用三类方法获取方法单列集合(Collection[List,Set双列集合Map(不能直接获取)数组同一类型元素(Stream中的静态方法) 常见的中间方法终结方法收集方法 Optional类 线程相关概念多线程概念实现方式继承Thread类实现Runnable接口比较 常用方法线程安全产生…

电机控制器电路板布局布线参考指导(二)

电机控制器电路板布局布线参考指导&#xff08;二&#xff09;热特性 1.概述2.PCB传导与对流3.连续顶层散热焊盘4.覆铜厚度5.散热过孔连接6.散热过孔宽度7.电机控制器电路板热设计总结 1.概述 电机驱动器并不是理想的器件&#xff0c;在实际应用中&#xff0c;它们的一些功率会…

Go 源码之 Chan

Go 源码之 chan go源码之chan - Jxy 博客 目录 Go 源码之 chan一、总结二、源码&#xff08;一&#xff09;hchan&#xff08;二&#xff09;创建&#xff08;三&#xff09;发送&#xff08;四&#xff09;接收&#xff08;五&#xff09;关闭 三、常见问题1.为什么要使用环形…

SV学习笔记(一)

SV&#xff1a;SystemVerilog 开启SV之路 数据类型 內建数据类型 四状态与双状态 &#xff1a; 四状态指0、1、X、Z&#xff0c;包括logic、integer、 reg、 wire。双状态指0、1&#xff0c;包括bit、byte、 shortint、int、longint。 有符号与无符号 &#xff1a; 有符号&am…

12313124

c语言中的小小白-CSDN博客c语言中的小小白关注算法,c,c语言,贪心算法,链表,mysql,动态规划,后端,线性回归,数据结构,排序算法领域.https://blog.csdn.net/bhbcdxb123?spm1001.2014.3001.5343 给大家分享一句我很喜欢我话&#xff1a; 知不足而奋进&#xff0c;望远山而前行&am…

【Linux】进程管理(2):进程控制

一、进程创建&#xff1a;fork函数 我们在命令行中输入man fork 即可得到fork函数的函数接口的函数的使用方法。 我们可以看到&#xff0c;fork函数位于man手册的第2部分&#xff0c;由于第2部分通常是用于描述系统调用和库函数&#xff0c;所以我们可以了解到fork函数实际是一…