【数据结构取经之路】二叉搜索树的实现

news2024/9/23 13:20:49

目录

前言

二叉搜索树

概念

性质

二叉搜索树的实现

结点的定义

插入

查找

删除

二叉搜索树完整代码


前言

首先,二叉搜索树是一种数据结构,了解二叉搜素树有助于理解map和set的特性。

二叉搜索树

概念

二叉搜索树又称二叉排序树,它也可能是一棵空树,走中序遍历得到的结果是有序的。

性质

二叉搜索树如果不为空,它应满足一下性质:

每一个节点都有一个键值,而且每一个键值唯一标识一个节点(即二叉搜索树所有节点中没有重复的值)

● 假设根节点的值为X,那么它的左子树上的所有节点的值都小于X,右子树上的所有的结点的值都大于X

● 根的左右子树都是二叉搜索树(递归性质)

二叉搜索树的实现

二叉搜索树的基本操作有插入、查找、删除,其中插入和查找好说,最麻烦的是它的删除操作。

结点的定义

template <class K>
struct BSTNode
{
	BSTNode<K>* _left;
	BSTNode<K>* _right;
	K _key;

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

插入

例如要插入0,0比8小,往左走,遇到3,0比3小,往左走,遇到1,0比1小,往左走,遇到空, 此时可以进行插入了。 总结起来就是:遇到空就可以插入。

直接上代码,后边再总结细节~

bool Insert(const K& key)
{
    //如果根节点为空,第一个插入的结点就充当根节点
	if (_root == nullptr)
	{
		Node* newNode = new Node(key);
		_root = newNode;
		return true;
	}

	Node* cur = _root;
	Node* parent = nullptr;
	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->_key == key,该值插入进入会破坏键值的唯一性,故插入失败
		}
	}

    //不知道上述代码是往左走到空还是往右走到空,
    //所以不确定是插入左边还是右边,需要判断
	Node* newNode = new Node(key);
	if (parent->_key > key)
		parent->_left = newNode;
	else
		parent->_right = newNode;

	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
			return true;
	}

	return false;
}

删除

上面的都好说,删除才是重头戏。

首先,查找所删除的结点是否在二叉搜索树中,如果不存在则返回false,如果存在就有以下几种情况。

1)要删除的节点为叶子结点

2)要删除的结点只有左孩子或只有右孩子

3)要删除的结点左右孩子均存在

针对第一种情况,直接删除就好,没什么好说的。

针对第二种情况,请看下面分析。

例如要删除的结点为14,根据上图,我们只需要将结点10的指向调整一下,让它指向13就可以了。当然,我们还要记录被删除结点的父节点才可以正确调整指向。仔细想想,就会返现,情况1和情况2是可以一起处理的。

第三种情况:

 

例如要删除结点3,根据上图,如果我们直接删除肯定是不对的,得用替换法来删除。用谁来替换是有讲究的,我们可以选择使用节点3的左子树的最大值或者是节点3右子树的最小值,简记:左的最大,右的最小。处理成下面这样:

 处理成这样后,删除掉rightMin节点即可。当然,rightMin节点也可能有右孩子(它不可能有左孩子,因为我们找的是右子树的最小值,必然是右子树中的最左节点),我们在删除的时候考虑进去就好了,这个并不难解决。下面,我们按照上面的逻辑来把代码写出来~


bool Erase(const K& key)
{
	Node* cur = _root;
	Node* parent = nullptr;

	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 (parent->_left == cur)
					parent->_left = cur->_right;
				else
					parent->_right = cur->_right;

				delete cur;
				return true;
			}
			else if (cur->_right == nullptr)
			{
				if (parent->_left == cur)
					parent->_left = cur->_left;
				else
					parent->_right = cur->_left;

				delete cur;
				return true;
			}
			else
			{
				Node* rightMin = cur->_right;
				Node* rightMinParent = nullptr;

				while (rightMin->_left)
				{
					rightMinParent = rightMin;
					rightMin = rightMin->_left;
				}

				//可以交换,也可以直接赋值
				cur->_key = rightMin->_key;

				rigntMinParent->_left = rightMin->_right;

				delete rightMin;
				return true;
			}
		}
	}
	return false;
}

我试着用上述代码把所有插入的结点都删掉,但是程序崩了~有以下几个原因。

在这种情况下,分析删除值为8的节点。该节点的parent为空, 程序执行下面的代码:

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

	delete cur;
	return true;
}

这时候就是对空指针的解引用了,程序当然会崩掉~所以需要对parent为空的情况单独处理。把上述代码改成这样:

if (cur->_left == nullptr)
{
	  //左为空的情况习下,parent为可能空,即单支的树,所以需要判断
	  if (parent == nullptr)
	  {
		_root = cur->_right;
	  }
	  else
	  {
		if (parent->_left == cur)
			parent->_left = cur->_right;
		else
			parent->_right = cur->_right;
	  }

	  if (parent->_left == cur)
			parent->_left = cur->_right;
		else
			parent->_right = cur->_right;

		delete cur;
		return true;
}

上面分析的是只有左支的情况,只有右支的情况同样如此,需要做单独处理。

解决了只有单支的情况后,我又试着删除所有节点,程序还是崩了~原因如下:

3为要删除的结点。此时,按照代码逻辑,rightMin为6,rightMinParent为空, 后面执行这句代码:

rigntMinParent->_left = rightMin->_right;

 所以会崩掉~但此时我们可以看到,rightMin(节点6)的父亲结点为3,并不是空,所以rightMinParent初始化为空并不合理,应该初始化为cur。

完善到这步,也还有一个小问题,按照上面修改之后的逻辑,rightMin为6,rightMinParent为3,执行这句代码:

rigntMinParent->_left = rightMin->_right;

 我们可以很容易发现这是错的,应该是rightMinParent->_right = rightMin->_right; 所以我们还需要加判断,具体如下:

if (rightMinParent->_left == rightMin)
	rightMinParent->_left = rightMin->_right;
else
	rightMinParent->_right = rightMin->_right;

完整的修改后的代码如下:


bool Erase(const K& key)
{
	Node* cur = _root;
	Node* parent = nullptr;

	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)
			{
				//左为空的情况习下,parent为可能空,即单支的树,所以需要判断
				if (parent == nullptr)
				{
					_root = cur->_right;
				}
				else
				{
					if (parent->_left == cur)
						parent->_left = cur->_right;
					else
						parent->_right = cur->_right;
				}

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

				delete cur;
				return true;
			}
			else
			{
				Node* rightMin = cur->_right;
				Node* rightMinParent = cur;

				while (rightMin->_left)
				{
					rightMinParent = rightMin;
					rightMin = rightMin->_left;
				}

				//可以交换,也可以直接赋值
				cur->_key = rightMin->_key;

				//不一定是rigntMinParent的左边
				if (rightMinParent->_left == rightMin)
					rightMinParent->_left = rightMin->_right;
				else
					rightMinParent->_right = rightMin->_right;

				delete rightMin;
				return true;
			}
		}
	}
	return false;
}

二叉搜索树完整代码

#include <iostream>
using namespace std;

template <class K>
struct BSTNode
{
	BSTNode<K>* _left;
	BSTNode<K>* _right;
	K _key;

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

template <class K>
class BSTree
{
	typedef BSTNode<K> Node;
public:
bool Insert(const K& key)
{
	if (_root == nullptr)
	{
		Node* newNode = new Node(key);
		_root = newNode;
		return true;
	}

	Node* cur = _root;
	Node* parent = nullptr;
	while (cur)
	{
		if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else
		{
			return false;
		}
	}

	Node* newNode = new Node(key);
	if (parent->_key > key)
		parent->_left = newNode;
	else
		parent->_right = newNode;

	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
				return true;
		}

		return false;
	}

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

	bool Erase(const K& key)
	{
		Node* cur = _root;
		Node* parent = nullptr;

		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)
				{
					//左为空的情况习下,parent为可能空,即单支的树,所以需要判断
					if (parent == nullptr)
					{
						_root = cur->_right;
					}
					else
					{
						if (parent->_left == cur)
							parent->_left = cur->_right;
						else
							parent->_right = cur->_right;
					}

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

					delete cur;
					return true;
				}
				else
				{
					Node* rightMin = cur->_right;
					//Node* rightMinParent = nullptr;
					Node* rightMinParent = cur;

					while (rightMin->_left)
					{
						rightMinParent = rightMin;
						rightMin = rightMin->_left;
					}

					cur->_key = rightMin->_key;

					//不一定是rigntMinParent的左边
					if (rightMinParent->_left == rightMin)
						rightMinParent->_left = rightMin->_right;
					else
						rightMinParent->_right = rightMin->_right;

					delete rightMin;
					return true;
				}
			}
		}
		return false;
	}

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

完~

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

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

相关文章

推荐系统之MIND用户多兴趣网络

目录 引言MIND算法原理1. 算法概述2. 模型结构3. 多兴趣提取层4. 标签感知注意力层 实践应用应用场景1. 电商平台2. 社交媒体3. 视频流媒体4. 内容分发平台 结论 引言 随着大数据和人工智能技术的快速发展&#xff0c;推荐系统已成为电商平台、社交媒体和内容分发平台的重要组成…

如何用python写接口

如何用python写接口&#xff1f;具体步骤如下&#xff1a;  1、实例化server 2、装饰器下面的函数变为一个接口 3、启动服务 开发工具和流程&#xff1a; python库&#xff1a;flask 》实例化server&#xff1a;server flask.Flask(__name__) 》server.route(/index,met…

吃空上千袋,养猫10年经验,生生不息、希喂、弗列加特谁是卷王?

身为宠物医生&#xff0c;我每天都在与猫咪和狗狗的相处中度过&#xff0c;对它们的身体变化十分敏感。当前&#xff0c;许多家养猫面临肥胖和肝脏损伤的双重困扰&#xff0c;虽然医疗手段可以介入&#xff0c;但问题的核心在于宠物主人的喂养方法是否得当。 在我职业生涯的这…

磁盘空间不足java.sql.sQLException:磁盘空间不足

java.sql.sQLException:磁盘空间不足 环境介绍1 查询表空间使用情况2 对表空间文件扩展限制进行修改(或新增表空间数据文件)3 达梦数据库学习使用列表 环境介绍 遇到此错误时,首先查看数据库服务器 , 数据库相关磁盘磁盘空间使用率;在磁盘空间充足的情况下, 业务系统操作达梦数…

React Native 自定义 Hook 获取组件位置和大小

在 React Native 中自定义 Hook useLayout 获取 View、Pressable 等组件的位置和大小的信息 import {useState, useCallback} from react import {LayoutChangeEvent, LayoutRectangle} from react-nativeexport function useLayout() {const [layout, setLayout] useState&l…

搜维尔科技:【产品推荐】Euleria Health Riablo 运动功能训练与评估系统

Euleria Health Riablo 运动功能训练与评估系统 Riablo提供一种创新的康复解决方案&#xff0c;将康复和训练变得可激励、可衡量和可控制。Riablo通过激活本体感觉&#xff0c;并通过视听反馈促进神经肌肉的训练。 得益于其技术先进和易用性&#xff0c;Riablo是骨科、运动医…

CentOS7 虚谷数据库 单机版部署

单机版最低配置&#xff1a; 安装环境配置 1.CPU设置 关闭 CPU 超线程 查看当前CPU超线程状态&#xff1a; cat /sys/devices/system/cpu/smt/active 如果是0&#xff0c;表示超线程已关闭&#xff1b;返回值是1&#xff0c;表示超线程已开启。 切换超线程状态&#xff1a; &a…

pygame-键盘事件

pygame-官网文档:https://www.pygame.org/docs/ pygame-键盘事件文档:https://www.pygame.org/docs/ref/key.html pygame的维基文档https://www.pygame.org/wiki/ 这个网址记录了pygame的历史和pygame的解释 详细阅读pygame官网文档学会如何使用pygame后&#xff0c;阅读键盘…

DP(4) | 0-1背包 | Java | LeetCode 1049, 494, 474 做题总结

1049. 最后一块石头的重量 II 和 LC 416.分割等和子集 类似 思路&#xff08;我没有思路&#xff09;&#xff1a; 两块石头相撞&#xff0c;这里没有想到的一个点是&#xff0c;相撞的两个石头要几乎相似 以示例1为例&#xff0c;stones [2,7,4,1,8,1]&#xff0c;如果从左到…

【Linux杂货铺】期末总结篇2:文件操作命令 | 目录操作命令

&#x1f308;个人主页&#xff1a;聆风吟_ &#x1f525;系列专栏&#xff1a;Linux实践室、网络奇遇记 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 第四章4.1 ⛳️Linux与windows的文件系统差别4.2 ⛳️目录相关的常用术语4.3 ⛳️Linux文件类型…

领航Linux UDP:构建高效网络新纪元

欢迎来到 破晓的历程的 博客 ⛺️不负时光&#xff0c;不负己✈️ 文章目录 引言Udp和Tcp的异同相同点不同点总结 1.1、socket1.2、bind1.3、recvfrom1.4、sendto2.1、代码2.1、说明3.1、代码3.2、说明 引言 在前几篇博客中&#xff0c;我们学习了Linux网络编程中的一些概念。…

嵌入式人工智能(9-基于树莓派4B的PWM-LED呼吸灯)

1、PWM简介 (1)、什么是PWM 脉冲宽度调制(PWM)&#xff0c;是英文“Pulse Width Modulation”的缩写&#xff0c;简称脉宽调制&#xff0c;是在具有惯性的系统中利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术&#xff0c;广泛应用在从测量、通信到功率控制…

Linux部署禅道(无脑复制版)

目录 环境部署1、下载&#xff0c;解压2、启动3、设置开机自启 登录禅道登录数据库1、设置账号2、网页登录数据库 环境 Linux系统 Centos7 《Linux一键安装包安装禅道》视频链接&#xff1a; https://www.zentao.net/zentao-install/zentao-linux-install-80523.html 部署 …

2025考研~数据结构试卷

作者主页&#xff1a;知孤云出岫 数据结构试题 [TOC](数据结构试题)数据结构试卷一、选择题&#xff08;每题2分&#xff0c;共20分&#xff09;二、填空题&#xff08;每题3分&#xff0c;共15分&#xff09;三、简答题&#xff08;每题10分&#xff0c;共40分&#xff09;四…

c/c++ 打印调用栈

打印调用栈可以在程序出现死机的时候&#xff08;如出现 SIGABRT、SIGSEGV等一些信号错误&#xff09;是很有用的信息&#xff0c;有可能就不需要 core file 来协助排查问题了。通过 man backtrace 可以得到一个例子的源码&#xff1a; #define SIZE 100 static void backTrac…

《javeEE篇》--多线程(1)

进程 在讲线程之前我们先来简单了解一下进程 什么是进程 进程是操作系统对一个正在运行的程序的一种抽象&#xff0c;又或者说&#xff0c;可以把进程看作程序的一次运行过程(通俗的讲就是跑起来的程序)。 而且在操作系统内部&#xff0c;进程是资源分配的基本单位 PCB P…

学生基本信息界面(MFC)

本文将引用MFC常用控件&#xff0c;写一个学生基本信息界面&#xff0c;最后将统计结果显示在提示框中&#xff0c;运行效果如下&#xff1a; 1.新建基于对话框的MFC项目&#xff0c;布局对话框&#xff0c;修改相应控件ID并绑定变量 注意:第一个单选控件的group属性 3.在构造…

《算法笔记》总结No.7——二分(多例题详解版)

一.二分查找 目前有一个有序数列&#xff0c;举个例子&#xff0c;假设是1~1000&#xff0c;让我们去查找931这个数字&#xff0c;浅显且暴力的做法就是直接从头到尾遍历一遍&#xff0c;直到找到931为止。当n非常大&#xff0c;比如达到100w时&#xff0c;这是一个非常大的量级…

获取欧洲时报中国板块前新闻数据(多线程版)

这里写目录标题 一.数据获取流程二.获取主页面数据并提取出文章url三.获取文章详情页的数据并提取整体代码展示 一.数据获取流程 我们首先通过抓包就能够找到我们所需数据的api 这里一共有五个参数其中只有第一个和第五个参数是变化的第一个参数就是第几页第五个是一个由时…

论文翻译 | Successive Prompting for Decomposing Complex Questions 分解复杂问题的连续提示

摘要 回答需要做出潜在决策的复杂问题是一项具有挑战性的任务&#xff0c;尤其是在监督有限的情况下。 最近的研究利用大型语言模型&#xff08;LMs&#xff09;的能力&#xff0c;在少量样本设置中通过展示如何在单次处理复杂问题的同时输出中间推理过程&#xff0c;来执行复杂…