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

news2024/11/26 14:41:20

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树和红黑树的讲解

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

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

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

相关文章

链动2+1结合消费增值:破解用户留存与复购的密码

大家好&#xff0c;我是吴军&#xff0c;来自一家领先的软件开发公司&#xff0c;担任产品经理的职务。今天&#xff0c;我希望能与大家深入交流链动21模式&#xff0c;特别是它在提升用户留存和复购率方面的独特价值。 虽然链动模式在某些人眼中可能被视为传统或已被超越&…

HCIP的学习(16)

BGP的状态机 ​ OSPF的状态机是在描述整个协议的完整工作过程&#xff0c;而BGP的状态机仅描述的是对等体关系建立过程中的状态变化。-----因为BGP将邻居建立过程以及BGP路由收发过程完全隔离。 ​ IGP协议在启动后&#xff0c;需要通过network命令激活接口&#xff0c;从而使…

运筹系列92:vrp算法包VROOM

1. 介绍 VROOM is an open-source optimization engine written in C20 that aim at providing good solutions to various real-life vehicle routing problems (VRP) within a small computing time. 可以解决如下问题&#xff1a; TSP (travelling salesman problem) CVRP …

三极管 导通条件

一、三极管理解 三极管是电子行业常用的元器件之一&#xff0c;他是一种电流型控制的器件&#xff0c;他有三种工作状态&#xff1a;截止区&#xff0c;放大区、饱和区。当三极管当做开关使用时&#xff0c;他工作在饱和区。下面简短讲解三极管作为开关使用的方法&#xff0c;只…

2025考研 | 北京师范大学计算机考研考情分析

北京师范大学&#xff08;Beijing Normal University&#xff09;简称“北师大”&#xff0c;由中华人民共和国教育部直属&#xff0c;中央直管副部级建制&#xff0c;位列“211工程”、“985工程”&#xff0c;入选国家“双一流”、“珠峰计划”、“2011计划”、“111计划”、…

C--贪吃蛇

目录 前言 简单的准备工作 蛇的节点 开始前 void GameStart(pSnake ps) void WelcomeToGame() void CreateMap() void InitSnake(pSnake ps) void CreateFood(pSnake ps) 游戏进行 void GameRun(pSnake ps) int NextIsFood(pSnakeNode psn, pSnake ps) void NoFood(pSnak…

whisper之初步使用记录

文章目录 前言 一、whisper是什么&#xff1f; 二、使用步骤 1.安装 2.python调用 3.识别效果评估 4.一点封装 5.参考链接 总结 前言 随着AI大模型的不断发展&#xff0c;语音识别等周边内容也再次引发关注&#xff0c;通过语音转文字再与大模型交互&#xff0c;从而…

ssm125四六级报名与成绩查询系统+jsp

四六级报名与成绩查询系统的设计与实现 摘 要 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以很好地为人们提供服务。针对四六级报名信息管理混乱&am…

外卖 点金推广实战课程,2024外卖 点金推广全流程(7节课+资料)

课程内容&#xff1a; 外卖点金推广实操课程 资料 01 1-了解外卖.mp4 02 第一节:点金推广的说明.mp4 03 第二节:如何降低点金推广的成本,mp4 04 第三节:如何计算点金推广的流速,mp4 05 第四节:如何提升点金的精准度,mp4 06 第五节:点金推广实操,mp4 07 点金推广高级教程…

UE4_照亮环境_不同雾效的动态切换

一、问题及思路&#xff1a; 我们在一个地图上&#xff0c;经常切换不同的区域&#xff0c;不同的区域可能需要不同的色调&#xff0c;例如暖色调的野外或者幽暗的山洞&#xff0c;这两种环境上&#xff0c;雾效的选用肯定不一样&#xff0c;夕阳西下的户外用的就是偏暖的色调&…

基于微信小程序+JAVA Springboot 实现的【马拉松报名系统】app+后台管理系统 (内附设计LW + PPT+ 源码+ 演示视频 下载)

项目名称 项目名称&#xff1a; 马拉松报名系统微信小程序 项目技术栈 该项目采用了以下核心技术栈&#xff1a; 后端框架/库&#xff1a; Java SSM框架数据库&#xff1a; MySQL前端技术&#xff1a; 微信开发者工具、uni-app其他技术&#xff1a; JSP开发技术 项目展示 …

CANopen总线_CANOpen开源协议栈

CANopen是自动化中使用的嵌入式系统的通信协议栈和设备配置文件规范。就OSI 模型而言&#xff0c;CANopen 实现了以上各层&#xff0c;包括网络层。 CANopen 标准由一个寻址方案、几个小型通信协议和一个由设备配置文件定义的应用层组成。通信协议支持网络管理、设备监控和节点…

防火墙远程桌面端口号修改,通过防火墙修改远程桌面的端口号详细操作步骤

使用防火墙修改远程桌面的端口号是一项涉及系统安全和网络配置的重要任务。 以下是详细的操作步骤&#xff0c;旨在确保您能够安全、有效地完成此操作&#xff1a; 一、准备阶段 1. 了解默认端口号&#xff1a;远程桌面端口号通常是3389&#xff0c;这是一个用于远程访问和控…

springboot+vue+mybatis生活废品回收系统+PPT+论文+讲解+售后

该生活废品回收系统采用B/S架构、前后端分离以及MVC模型进行设计&#xff0c;并采用java语言以及springboot框架进行开发。该系统主要设计并完成了管理过程中的用户登录、个人信息修改、义捐活动、在线咨询、订单评价、废品订单、废品、回收再利用技巧、废品回收员、用户等功能…

幻兽帕鲁(公益入库)教程

先安装“SteamtoolsSetup”&#xff0c; 安装好桌面会出来个steam图标的。然后打开“幻兽帕鲁文件夹” 把那2个脚本拖进去那个steam图标。只要显示“已编译了1个Lua脚本”“已更新了1个清单文件”将在Steam重启后生效。然后退出steam&#xff0c;然后重启steam就可以了&#xf…

霍金《时间简史 A Brief History of Time》书后索引(I--L)

A–D部分见&#xff1a;霍金《时间简史 A Brief History of Time》书后索引&#xff08;A–D&#xff09; E–H部分见&#xff1a;霍金《时间简史 A Brief History of Time》书后索引&#xff08;E–H&#xff09; 图源&#xff1a;Wikipedia INDEX I Imaginary numbers Ima…

新消息:2024中国(厦门)国际义齿加工产品展览会

DPE2024中国&#xff08;厦门&#xff09;国际义齿加工产品展览会暨学术研讨会 2024 China (Xiamen) International Denture Processing Products Exhibition 时 间&#xff1a;2024年11月1-3日 November 1-3, 2024 地 点&#xff1a;厦门国际会展中心 Xiamen Int…

强化训练:day7(字符串中找出连续最长的数字串、岛屿数量、拼三角)

文章目录 前言1. 字符串中找出连续最长的数字串1.1 题目描述1.2 解题思路1.3 代码实现 2. 岛屿数量2.1 题目描述2.2 题目描述2.3 代码实现 3. 拼三角3.1 题目描述3.2 解题思路3.3 代码实现 总结 前言 1. 字符串中找出连续最长的数字串   2. 岛屿数量   3. 拼三角 1. 字符串…

LVGL移植到ARM开发板(GEC6818)

源码下载&#xff1a;点击跳转 下载好三个文件后&#xff0c;将其解压缩&#xff0c;并合到一个文件夹里面—— 1、修改 Makefile 删除 -Wshift-negative-value 2、修改 main.c 3、修改 lv_drv_conf.h 在lv_drv_conf.h文件屏幕驱动文件刚好与开发板LCD驱动文件一致&#xff0c…

轻松掌握RAID级别

一、官方说明&#xff1a; RAID&#xff08;英文全称 Redundant Array of Independent Disks&#xff09;翻译成中文&#xff08;独立磁盘冗余阵列&#xff09;。 RAID 是一种将多块独立磁盘&#xff0c;组成一组逻辑磁盘的技术。RAID 级别分为 0、1、3、5、6等&#xff0c;可…