【c++】二叉搜索树(BST)

Alt

🔥个人主页Quitecoder

🔥专栏c++笔记仓

Alt

朋友们大家好,本篇文章来到二叉搜索树的内容

目录

  • `1.二叉搜索树的介绍`
  • `2.二叉搜索树的操作与实现`
    • `insert插入`
    • `Find查找`
    • `InOrder中序遍历`
    • `Erase删除`
  • `3.二叉搜索树的应用(K与KV模型)`
    • `改造二叉树为KV结构`
  • `4.二叉搜索树性能分析`

1.二叉搜索树的介绍

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

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

它在动态数据集合中维护了一定的排序顺序,以便实现快速的数据查找、插入和删除操作

在这里插入图片描述
左子树比根小,右子树比根大

比如我想查找13,就不需要暴力比较,按照大小往左边或者右边走

对于二叉搜索树,进行中序遍历为升序

2.二叉搜索树的操作与实现

首先我们构建节点

template<class K>
struct BSTreeNode
{
	K _key;
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	BSTreeNode()
		:_key(K())
		,_left(nullptr)
		,_right(nullptr)
	{}
	BSTreeNode(const K& key)
		: _key(key)
		, _left(nullptr)
		, _right(nullptr)
	{}
};

每个节点有两个指针,分别指向它的左子节点和右子节点。如果子节点不存在,则这些指针为nullptr

  1. 默认构造函数
BSTreeNode()
	:_key(K())
	,_left(nullptr)
	,_right(nullptr)
{}

默认构造函数,它初始化键值为K类型的默认值(通过调用K的默认构造函数),并将左右子节点指针都设置为nullptr,表示节点没有子节点

  1. 参数化构造函数
BSTreeNode(const K& key)
	: _key(key)
	, _left(nullptr)
	, _right(nullptr)
{}

采用键值作为参数的构造函数,它会创建一个节点,这个节点的键值为传入的key值,同时初始化左右子节点指针为nullptr

接着我们来完成主体部分:

template<class T>
class BSTree
{
public:
	typedef BSTreeNode<T> Node;
private:
	Node* _root = nullptr;
};

insert插入

在这里插入图片描述
比如插入5,我们从根节点开始,比8小,往左走,比3大,往右走…:

在这里插入图片描述

bool Insert(const T& key)
{
	Node* cur = _root;
	while (cur)
	{
		if (cur->_key < key)
		{
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			cur = cur->_left;
		}
		else
		{
			return false;
		}
	}
	return true;
}

比当前节点小,往左走,反之往右走,搜索树默认是不允许插入重复键值

所以遇到相同的直接返回false,但是最后一步插入,我们还需要父亲位置的节点来完成左边插入或者右边插入,所以我们需要一个父亲节点来记录位置:

bool Insert(const T& 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
		{
			return false;
		}
	}
	cur = new Node(key);
	if (parent->_key < key)
	{
		parent->_right = cur;
	}
	else
	{
		parent->_left = cur;
	}
	return true;
}

由于我们是从根部开始向下遍历直到达到叶节点,parent->_key必定不等于key(因为有重复检查)。如果parent的键值小于插入的键值key,新节点被设置为父节点的右子树;否则设置为左子树

注意

这里如果起始为**空树*

Node* cur = _root;
while (cur)
{
    // ...
}

由于cur是从_root开始的,如果跳过判空且_root实际上为nullptr,这个循环不会执行任何操作,因为它的条件立即不满足(cur此时为nullptr),并且会跳到循环之后的代码,如下:

cur = new Node(key);
// ...

这里将创建一个新的节点,但此时变量parent仍然是nullptr。代码会接着尝试访问parent_key成员

if (parent->_key < key)
{
    // ...
}

因为parentnullptr,这会导致未定义行为,最常见的是程序崩溃,因为你不能对nullptr解引用。另外,即使程序不崩溃,新的节点cur也没有父节点可以挂载到,这样二叉搜索树的结构就不完整了

所以完整代码如下:

bool Insert(const T& 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
		{
			return false;
		}
	}
	cur = new Node(key);
	if (parent->_key < key)
	{
		parent->_right = cur;
	}
	else
	{
		parent->_left = cur;
	}
	return true;
}

Find查找

find这里思路很简单,就按照大小关系往下遍历即可:

bool Find(const T& key)
{
	Node* cur = _root;
	while (cur)
	{
		if (cur->_key < key)
		{
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			cur = cur->_left;
		}
		else
		{
			return true;
		}
	}
	return false;
}

InOrder中序遍历

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

这里我们需要传入根节点,为类成员,单独一个函数是无法实现的,所以我们先完成上面的子函数书写,再一个主函数传入_root即可

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

测试如下:

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

for (int e : a)
{
	b1.Insert(e);
}
b1.InOrder();

在这里插入图片描述

Erase删除

二叉树的删除是这里的难点,因为它涉及到多种情况,针对不同的情况我们对应不同的方法:

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

前三种情况可以结合起来:

情况2:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点–直接删除

情况3:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点–直接删除

情况4:替换法解决

对于一个节点,它的:

  • 中序前驱是它左子树中的最大节点,它小于该节点且最接近它
  • 中序后继是它右子树中的最小节点,它大于该节点且最接近它

替换法删除的思路分为以下步骤:

  1. 找到需要被删除的节点。
  2. 检查这个节点是否有两个子节点:
    • 如果不是,处理起来比较简单,可以直接删除。如果该节点只有一个子节点,则该子节点取代被删除节点的位置。如果是叶节点,可以直接移除。
    • 如果是,执行以下步骤。
  3. 选择使用中序前驱或中序后继来替换要删除的节点。我们通常默认使用中序后继,但两者均可。
  4. 找到中序后继节点:
    • 进入待删除节点的右子树,然后一直向左走,直到找到没有左子节点的节点;这是中序后继。
  5. 替换:
    • 复制中序后继节点的值到待删除节点中,覆盖原有值
    • 此时,待删除节点的值已更新为其中序后继节点的值,原来的中序后继节点可以被移除(因为它已经被复制了)。需要注意,这个中序后继节点不会有左子节点(因为它已经是某个子树中的最左侧节点),所以它要么是一个叶节点,要么只有一个右子节点
  6. 删除中序后继节点:
    • 通过调整指针,将中序后继节点的父节点指向其可能存在的右子节点(也可能为空),完成删除操作

进行这样的替换之后,二叉搜索树的特性依然得以保持。中序后继节点保证了替换后的节点值仍然比其左子节点的所有值大,且比其右子节点(除了被移除的中序后继节点外)的所有值小

替换法删除操作需要注意的关键点是,通过中序前驱或中序后继节点替换,实际上我们把删除一个可能有两个子节点的难题转变成了删除一个有零个或一个子节点的简单问题,且这个中序后继节点一定在待删除节点的右子树中最左侧

	bool Erase(const T& key)
{
	if (_root == nullptr)
		return false;
	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
		{
			if (cur->_left == nullptr)
			{
				if (cur == _root)
				{
					_root = cur->_right;
				}
				else
				{
					if (cur == parent->_left)
					{
						parent->_left = cur->_right;
					}
					else
					{
						parent->_right = cur->_right;
					}
				}
				delete cur;
			}
			else if (cur->_right == nullptr)
			{
				if (cur == _root)
				{
					_root = cur->_left;
				}
				else {
					if (cur == parent->_left)
					{
						parent->_left = cur->_left;
					}
					else
					{
						parent->_right = cur->_left;
					}
				}
				delete cur;

			}
			else
			{
				//左右都不为空,替换法删除
				Node* rightminparent = cur;
				Node* rightmin = cur->_right;
				while (rightmin->_left)
				{
					rightminparent = rightmin;
					rightmin = rightmin->_left;
				}
				cur->_key = rightmin->_key;
				if (rightminparent->_left = rightmin)
				{
					rightminparent->_left = rightmin->_right;
				}
				else
				{
					rightminparent->_right = rightmin->_right;
				}
				delete rightmin;
			}

			return true;
		}
	}
	return false;
}

我们拆分来看这串代码:

  1. 检查树是否为空:
if (_root == nullptr)
    return false;
  1. 查找需删除的节点:
    代码通过while循环遍历树找到匹配key的节点。在循环中使用变量cur作为当前节点,变量parent作为cur的父节点

  2. 节点匹配:
    当找到与key匹配的节点后:

    • 如果该节点没有左子节点(cur->_left == nullptr), 那么它的右子节点直接替换它(也适用于它没有子节点的情况)
    • 如果该节点没有右子节点(cur->_right == nullptr), 那么它的左子节点直接替换它
if (cur->_left == nullptr)
{
	if (cur == _root)
	{
		_root = cur->_right;
	}
	else
	{
		if (cur == parent->_left)
		{
			parent->_left = cur->_right;
		}
		else
		{
			parent->_right = cur->_right;
		}
	}
	delete cur;
}

如果cur恰好是根节点,我们直接将树的根 _root 指向cur的右子节点。这个更新意味着我们在树中移除了根节点,并将右子节点(如果存在)提升为新的根节点。

如果cur不是根节点,我们需要更新它父节点的相应指针。我们检查cur是其父节点的左子还是右子,并相应地更新父节点的左指针或右指针,使其指向cur的右子节点。这样,在二叉搜索树中删除了cur节点,并保持了其右子树

    • 如果该节点既有左子节点也有右子节点, 那么需要找到该节点的中序后继节点来替代它。中序后继节点是在其右子树中值最小的节点。我们替换cur的键为中序后继节点的键,并将rightmin放在原来的位置上
else
{
	//左右都不为空,替换法删除
	Node* rightminparent = cur;
	Node* rightmin = cur->_right;
	while (rightmin->_left)
	{
		rightminparent = rightmin;
		rightmin = rightmin->_left;
	}
	cur->_key = rightmin->_key;
	if (rightminparent->_left == rightmin)
	{
		rightminparent->_left = rightmin->_right;
	}
	else
	{
		rightminparent->_right = rightmin->_right;
	}
	delete rightmin;
}

注意,在替换完成后需要删除原始的中序后继节点。这时rightmin的右子节点(如果存在)会替换rightmin

每次删除一个节点后,代码会释放该节点的内存。

  1. 维护父节点指针:
    删除过程中对父节点指针的适当维护是必须的,以确保删除节点后树的结构保持正确。比如,如果待删除节点是其父节点的左子节点,那么父节点的左指针应该指向待删除节点的相应子节点

最后,如果在树中找到并成功删除了key对应的节点,则函数返回true。如果没有找到,则函数返回false

3.二叉搜索树的应用(K与KV模型)

  1. K模型
    • K模型指的是二叉树的节点仅存储键Key)信息,而没有与键相关联的特定“值”(Value)。换句话说,节点中的数据只有一个维度,节点的排序和组织就是基于这些键
    • 在K模型的二叉树中,例如二叉搜索树(BST),节点的位置由其键的顺序决定。所有的节点操作,包括插入、查找和删除都是根据这个键来执行的。
  • 比如:给一个单词word,判断该单词是否拼写正确,具体方式如下
    • 以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
    • 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误
  1. KV模型
    • KV模型指的是二叉树的节点存储“键值对”(Key-Value Pair)。这里“键”(Key)用于确定节点的位置跟顺序,“值”(Value)则是与键关联的数据。
    • 在KV模型的二叉树中,节点依然是根据键的顺序进行排列和组织的,但是与每个键都有一个相对应的值。这种模式适用于情况更为复杂的场景,如实现映射或字典结构
    • KV模型的一个典型例子是映射(Map)或词典(Dictionary)

比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;

再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对

改造二叉树为KV结构

节点构建,加一个模版参数V

template<class K,class V>
struct BSTreeNode
{
	V _value;
	K _key;
	BSTreeNode<K,V>* _left;
	BSTreeNode<K,V>* _right;
	BSTreeNode()
		:_key(K())
		, _value(V());
		, _left(nullptr)
		, _right(nullptr)
	{}
	BSTreeNode(const K& key,const V& value)
		: _key(key)
		,_value(value)
		, _left(nullptr)
		, _right(nullptr)
	{}
};

代码主题部分只需要进行简单的修改即可:

template<class K,class V>
class BSTree
{
public:
	typedef BSTreeNode<K,V> Node;
	bool Insert(const K& key,const V& value)
	{........
	cur = new Node(key,value);
     ........
	}
	Node* 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 cur;
		}
	}
	return nullptr;
    }
	........
};

其余部分不需要改变

简单示例如下:

void TestBSTree2()
	{
		BSTree<string, string> dict;
		dict.Insert("string", "字符串");
		dict.Insert("left", "左边");
		dict.Insert("insert", "插入");
		//...

		string str;
		while (cin >> str)
		{
			BSTreeNode<string, string>* ret = dict.Find(str);
			if (ret)
			{
				cout << ret->_value << endl;
			}
			else
			{
				cout << "无此单词,请重新输入" << endl;
			}
		}
	}

本节内容到此结束! 感谢阅读!!

4.二叉搜索树性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多

但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为O(log n)

在这里插入图片描述
最差情况下,二叉搜索树退化为单支树(或者类似单支),查找的时间复杂度为O(n)

在这里插入图片描述
如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优?

期待后续AVL树和红黑树的讲解

本节内容到此结束!!感谢阅读!!

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

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

相关文章

代理IP可靠吗?哪里可以找到可靠的代理?

需要代理来访问受限制的网站或改善您的在线隐私&#xff1f;别再犹豫了&#xff01;在这篇博文中&#xff0c;我们将探讨您可以使用的选项&#xff0c;并提供有关在哪里获取代理的指导。 首先&#xff0c;让我们了解什么是代理及其工作原理。代理充当您的设备和互联网之间的中介…

内容与图像一对多问题解决

场景复现 分析&#xff1a; 其实这是两给表&#xff0c;一个内容表&#xff0c;一个图片表&#xff0c;一对多的关系。 解决思路: 1. 先上传图片拿到图片的List集合ids&#xff0c;返回值是集合的ids&#xff0c;给到前端 2. 再添加内容表的数据生成了id&#xff0c;遍历查…

git版本控制器详解(3)本地和远端同步

为什么要使用gitee&#xff1f; gitee是基于git所搭建的网站&#xff0c;会给我们提供一个稳定的服务器保存我们的版本信息。因为github是国外网站&#xff0c;国内访问速度不够稳定&#xff0c;所以我们选择使用gitee。 前边我们讲解了如何在本地进行操作&#xff0c; 接下来进…

Ranger 面试题及答案整理,最新面试题

Ranger 的安全模型是如何设计的&#xff1f; Ranger的安全模型设计主要基于访问控制和安全策略的管理&#xff0c;它通过以下几个关键组件实现&#xff1a; 1、策略管理&#xff1a; Ranger 提供了一个中央管理平台&#xff0c;用于定义、更新和管理安全策略。这些策略根据资…

【小白入门篇6】常识|怎么计算模型需要的资源

01 背景 各个公司相继推出大模型, 有开源和不开源,有些技术爱好者也开始心痒难耐&#xff0c;萌生了私有本地模型,甚至有伙伴构建大模型并进行训练的想法, 大模型不仅比拼技术, 也是比拼爹(资源)的存在, 我个人在实战经历经常问自己,到底需要什么样配置才能跑起来这个模型, 完…

Mysql数据类型设计思考

一、Mysql数据类型设计规范 1.1 选择更小的数据类型 一般情况下&#xff0c;在满足存储要求的基础上&#xff0c;尽量选择小的存储类型。例如&#xff1a;存储0~200&#xff0c;tinyint和bigint都可以存储&#xff0c;那么选择tinyint。原因&#xff1a;越小的数据类型运算速…

【项目】Boost搜索引擎

项目相关背景 现在市面上已经出现很多搜索引擎&#xff0c;比如&#xff1a;百度、Google、Bing等等&#xff0c;它们都是全网性搜索 而我做得项目就像cplusplus网站中搜索C的相关知识一样&#xff0c;同样做的是站内搜索&#xff0c;它的搜索更垂直。 搜索引擎的宏观原理 ser…

模型 洋葱模型(组织管理方向)

系列文章 分享 模型&#xff0c;了解更多&#x1f449; 模型_思维模型目录。层层深入&#xff0c;探索核心。 1 洋葱模型的应用 1.1 洋葱模型用于职业规划 有一个名叫李明的大学生&#xff0c;他最近感到迷茫和压力&#xff0c;因为他即将毕业并面临职业选择。李明决定寻求心…

Java中55种锁,高级面试题,最新面试题

Java中乐观锁在实际应用中如何解决并发问题&#xff1f; 乐观锁通过假设并发冲突发生概率较低来解决并发问题&#xff0c;主要通过数据版本控制实现。在更新数据前&#xff0c;会检查数据版本是否发生变化&#xff0c;只有在数据版本未变时才允许更新&#xff0c;这样可以避免…

uos server 无法通过ssh工具连接

问题现象 uos server 服务器操作系统 在虚拟机中安装好之后&#xff0c;防火墙已经关闭&#xff0c;ssh服务已经启动&#xff0c;但通过finalshell等ssh工具连接报错 &#xff1a;java.net.ConnectException: Connection timed out: connect 经过确认 防火墙已关&#xff0c;s…

计算机毕业设计springboot体育馆场地预约管理系统【附源码】

计算机毕业设计springboot体育馆场地预约管理系统[附源码] &#x1f345; 作者主页 网顺技术团队 &#x1f345; 欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; &#x1f345; 文末获取源码联系方式 &#x1f4dd; &#x1f345; 查看下方微信号获取联系方式 承接各种定制…

人工智能生成图像的兴起:区分事实与虚构

人工智能生成图像的兴起&#xff1a;区分事实与虚构 概述 在人工智能 (AI) 已融入我们日常生活的时代&#xff0c;人工智能生成图像的快速发展引发了人们对数字内容真实性的担忧。最近&#xff0c;人工智能生成的图像甚至欺骗了最敏锐的眼睛&#xff0c;这引发了人们对批判性…

@游戏行业er!MongoDB广州线下沙龙邀您报名!

随着游戏和应用程序的发展&#xff0c;数据变得越来越重要。在为您的下一个游戏选择数据库时&#xff0c;数据库管理者常常会面对灵活性、可扩展性、可靠性、运营效率等问题或挑战。 MongoDB在游戏开发领域有着广泛的应用&#xff0c;灵活数据模型可以存储和处理各种类型的数据…

OPT系列极速版远距离光数据传输器|光通讯传感器安装与调试方法

OPT系列极速版远距离光数据传输器|光通讯传感器使用红外激光通信&#xff0c;满足全双工 100M 带宽&#xff0c;通讯距离可达 300 米。能够快速&#xff0c;稳地传送数据&#xff0c;支持主流的工业控制总线&#xff08;Profinet&#xff0c;Ethercat 等&#xff09;&#xff1…

媒体宣发:多元宣发方式的方式有哪些

在信息爆炸的今天&#xff0c;媒体宣发被广泛地运用在各个领域&#xff0c;对于产品宣传、企业形象塑造等都起着至关重要的作用。多样化的媒体宣发方式越来越受到企业的重视&#xff0c;那么常见的媒体宣发方式有哪些呢&#xff1f; 首先&#xff0c;新闻发布是最传统也是最直…

SQLStringInFo SQL 数据库操作语句的解析器!!!

SQLStringInFo 开源技术栏 SQLStringInFo是一个专注于sql命令语句解析的sql命令解析库&#xff0c;在库中提供了有关SQL命令语法的解析器&#xff0c;通过该库&#xff0c;可以实现快速准确的SQL语句分析处理。 介绍 SQLStringInFo是一个专注于sql命令语句解析的sql命令解析…

形位公差Overview of GDT

零件公差产生于十九世纪后期&#xff0c;其初衷是为了保证零件的互换性。起初只有尺寸公差。由于 当时的设计部门和制造部门通常都在一起或就在隔壁&#xff0c;因此交流起来非常方便。在当时&#xff0c;给 定的公差一般都很大&#xff0c;因此当时的设备刀具的能力对于保证产…

1.基本概念,半导体基础

1.电压降&#xff1a; 指电流通过阻抗负载时的电位降的大小。&#xff08;线段或部件两端的电压&#xff09;。 2.数量较多的载流子称为多子 3.二极管和稳压管 4.习题

合专家模型 (MoE) 详解

本文转载自&#xff1a;混合专家模型 (MoE) 详解 https://huggingface.co/blog/zh/moe 英文版&#xff1a;https://huggingface.co/blog/moe 文章目录 一、简短总结二、什么是混合专家模型&#xff1f;三、混合专家模型简史四、什么是稀疏性?五、混合专家模型中令牌的负载均衡…

2024中国(重庆)航空航天暨无人机低空经济展览会

2024中国&#xff08;重庆&#xff09;航空航天暨无人机低空经济展览会 邀请函 组织机构 主办单位: 中国航空学会 重庆市南岸区人民政府 招商执行单位&#xff1a; 重庆港华展览有限公司 展会概括∶ 2024中国航空航天暨无人机低空经济展览会将于2024年8月23-25日在重庆…