搜索二叉树BSTree的原理及实现

目录

一、简介

二、功能的实现

节点的实现

这里为什么模板参数采用的是K而不是T呢?

树体的实现

非递归版本

Insert函数

Find函数

Erase函数

递归版本

中序遍历

FindR

InsertR

EraseR

构造函数

析构函数

拷贝构造

赋值重载


一、简介

BSTree(Binary Search Tree),即二叉搜索树,是一种特殊的二叉树,具有以下特性:

  1. 节点的左子树上所有节点的值均小于它的根节点的值:这意味着在二叉搜索树中,任何一个节点的左子树中的元素都是小于该节点的。

  2. 节点的右子树上所有节点的值均大于它的根节点的值:同样,任何一个节点的右子树中的元素都是大于该节点的。

  3. 左右子树也分别为二叉搜索树:二叉搜索树的每一个子树也是二叉搜索树。

  4. 没有键值相等的节点:在二叉搜索树中,所有节点的值都是唯一的。

二叉搜索树具有以下优点:

  • 高效的查找、插入和删除操作:在二叉搜索树上进行查找、插入和删除操作的时间复杂度平均为O(log n),其中n是树中节点的数量。

  • 保持数据的有序性:二叉搜索树的中序遍历结果是有序的,即按照从小到大的顺序排列。

二叉搜索树的操作包括:

  • 查找:从根节点开始,比较当前节点与目标值的的大小,根据比较结果决定是向左子树还是右子树递归查找。

  • 插入:从根节点开始,比较当前节点与待插入值的大小,找到合适的位置插入新节点。

  • 删除:删除操作较为复杂,需要考虑三种情况:

    • 删除的节点是叶子节点,可以直接删除。
    • 删除的节点只有一个子节点,可以用其子节点代替该节点。
    • 删除的节点有两个子节点,通常找到该节点的中序后继(右子树中的最小节点)或中序前驱(左子树中的最大节点)来代替,然后删除该后继或前驱节点。

下图就是一棵根据数组建成的搜索二叉树

下面我们就根据搜索二叉树的特点及功能进行二叉树的实现

二、功能的实现

跟普通的树结构一样,我们需要两个自定义类型来实现BSTree的功能。首先定义一个节点,用来存放树的键位,其次另一个自定义类型进行树功能接口的实现和树的搭建。

在这里我们并没有单独的放到一个命名空间中,主要是因为跟std的命名冲突不大,所以我们直接在全局就可以进行实现。

节点的实现

template<class K>
struct BSTreeNode
{BSTreeNode<K>* _left;	//模板实例化之后才是类型BSTreeNode<K>* _right;K _key;BSTreeNode(const K& key):_left(nullptr),_right(nullptr),_key(key){}
};

由于我们希望节点部分对于整个树部分是公开的希望可以访问他的_left等内容,所以我们采用struct结构,而不是class类。

需要注意的是    BSTreeNode<K>*模板实例化之后才是类型。

这里为什么模板参数采用的是K而不是T呢?

以下是使用K作为模板参数的几个原因:

明确性:K暗示了模板参数代表的是键(Key),这在使用二叉搜索树时,键是用来比较和排序的主要元素。

一致性:在涉及键值对或键相关的数据结构中,使用K作为键的类型可以保持命名的一致性,使得代码在不同的上下文中易于理解和维护。

约定:在许多编程实践中,T通常用于表示“Type”,是一个通用的类型占位符。但在特定的情况下,使用更具体的字母可以提供更多的上下文信息。例如,V可能用于表示“Value”,E可能用于表示“Element”等。

避免混淆:如果代码中已经使用了T作为其他意义下的模板参数,为了避免混淆,可能会选择其他字母。

总的来说,选择K而不是T是为了更好地传达模板参数的意图,并且遵循了类型参数命名的通用约定。这样的命名习惯有助于其他开发者快速理解代码的结构和用途

树体的实现

成员参数:

由于树是由一个个节点组成的,所以参数用一个根节点构成。

private:Node* _root = nullptr;		//类内初始化,将一个根节点置空

为了方便使用类型,进行一次typedef

template<class K>
class BSTree
{
public:typedef BSTreeNode<K> Node;

功能函数的实现

功能函数重点是插入、删除、遍历、修改。由于树可以进行递归实现,因此我们实现了递归与非递归版本的实现。

非递归版本

Insert函数

用来完成插入与树的构建操作。

插入的具体过程如下:
a. 树为空,则直接新增节点,赋值给root指针
b. 树不空,按二叉搜索树性质查找插入位置,插入新节点
其性质指的是左树<键值         右树 > 键值
bool Insert(const K& key)	//不允许值重复
{Node* cur = _root;Node* prev = cur;		//定义prev用来进行节点的链接if (cur == nullptr)		//空树_root = new Node(key);		//直接对根节点进行链接,而不是新建立newnodewhile (cur)			//非空树{if (key > _root->_key){prev = cur;cur = cur->_right;}else if (key < _root->_key){prev = cur;cur = cur->_left;}elsereturn false;}//cur->_key = key;		//错误,cur目前是一个指向未知区域的野指针cur = new Node(key);	//应该连入一个新节点//链接if (prev->_key > key)prev->_left = cur;elseprev->_right = cur;return true;
}

第一部分是判断该树是不是空树。让_root指向一个新开辟的节点即可。

第二部分是找到该树的空节点。之后是借助key新建一个节点。

第三部分是判断父子的位置关系,进行数据的链接。

Find函数

用于查找数据,左树小于键值,右树大于键值。

时间复杂度:logn:接近满二叉树(完全二叉树)

                    n:最坏(最坏的才是时间复杂度)

	bool Find(const K& key)		//给值检索{Node* cur = _root;while (cur){if (key > cur->_key)cur = cur->_right;else if (key < cur->_key)cur = cur->_left;elsereturn true;}return false;}

Erase函数

首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情
况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点
看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程
如下:
情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点--直接删除
情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点--直接删除
情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点
中,再来处理该结点的删除问题--替换法删除
情况bc是“托孤法” 形况d是替换法
替换时找左树的最大节点或者右树的最小节点
	bool Erase(const K& key){Node* cur = _root;Node* prev = cur;while (cur){if (key > cur->_key){prev = cur;cur = cur->_right;}else if (key < cur->_key){prev = cur;cur = cur->_left;}else	//找到了,进行删除{	if (cur->_left == nullptr)				//左孩子为空{	//当节点是根时(此时不存在父节点)if (cur == _root)_root = _root->_right;//判断父子关系if (prev->_key > cur->_key)prev->_left = cur->_right;	//托孤法else if (prev->_key < cur->_key)prev->_right = cur->_right;delete cur;		//最终都是删除cur,可以直接写道最后}else if (cur->_right == nullptr)		//右孩子为空{	//当节点是根时(此时不存在父节点)if (cur == _root)_root = _root->_left;//判断父子关系if (prev->_key > cur->_key)prev->_left = cur->_left;	//托孤法else if (prev->_key < cur->_key)prev->_right = cur->_left;delete cur;}else	//左右孩子都有:替换法{// 右树的最小节点(即最左节点,最左节点一定没有左孩子,但是可能有右孩子)Node* parent = cur;		//定义一个父节点去链接Node* MinRight = cur->_right;	//去寻找右树最小的节点while (MinRight->_left)		//在此处不可能出现根的左右都是空的情况(想删除时,前两种情况已经对此做出了处理){parent = MinRight;MinRight = MinRight->_left;}swap(cur->_key, MinRight->_key);	//本质是对键值交换if (parent->_left = MinRight)		parent->_left = MinRight->_right;elseparent->_right = MinRight->_right;delete MinRight;}return true;		//删除成功之后返回true}}return false;}

需要注意的是:

1.考虑root是不是需要删除的对象

2.交换时,本质是对键值的交换

3.在链接时,考虑父子的关系

递归版本

中序遍历

中序是一个递增序列(可以实现有序)

二叉树的递归都需要传入根,在外面传不合适,但是可以在内部传入。但是C++不喜欢写Get()方法,因此需要对递归的函数进行一次封装。

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

void类型不需要返回,直接及逆行递归遍历即可。

FindR

递归版本的查找

由于是bool类型,应该也有一次返回。

递归函数

bool _FindR(Node* root, const K& key)
{if (root == nullptr)return false;if (key > root->_key){_FindR(root->_right, key);}else if (key < root->_left){_FindR(root->_left, key);}elsereturn true;
}

InsertR

	bool _InsertR(Node*& root, const K& key)	//引用传参{if (root == nullptr){root = new Node(key);		//引用传参,保证_root可以链接return true;}if (key > root->_key)_InsertR(root->_right, key);else if (key < root->_key)_InsertR(root->_left, key);else	//相等return false;}

需要注意的是,我们需要传入的是指针的引用,因为我们需要修改一级指针。

在子问题函数中root就是上一级问题的root->_left   root->_right

因此这句代码:            root = new Node(key);        //引用传参,保证_root可以链接

本质就是上一层的root->_left  /  root->_right = new Node(key)

EraseR

bool _EraseR(Node*& root, const K& key)		//指针的引用可以修改一级指针{	if (root == nullptr)return false;if (key > root->_key)_EraseR(root->_right, key);else if (key < root->_key)_EraseR(root->_left, key);else	//找到了,进行删除{if (root->_left == nullptr)				//左孩子为空{//不需要判断父子关系(引用传参,知道root就是上一次的_left或者_right)因此不需要定义prev指针Node* del = root;root = root->_right;	//根节点也可以完成删除delete del;			//不能delete rootreturn true;}else if (root->_right == nullptr)		//右孩子为空{Node* del = root;root = root->_left;	//根节点也可以完成删除delete del;			//不能delete rootreturn true;}else	//左右孩子都有:替换法{Node* MinRight = root->_right;while (MinRight->_left){MinRight = MinRight->_left;}swap(MinRight->_key, root->_key);return _EraseR(root->_right, key);		//左右孩子都有这种情况要递归,必须有一次return//不能传入MinRight,因为他的父亲不能链接他的孩子(目的是为了托孤)}//return true;		不需要额外返回一次}}

核心逻辑:

if (root->_left == nullptr)				//左孩子为空
{//不需要判断父子关系(引用传参,知道root就是上一次的_left或者_right)因此不需要定义prev指针Node* del = root;root = root->_right;	//根节点也可以完成删除delete del;			//不能delete rootreturn true;
}else if (root->_right == nullptr)		//右孩子为空
{Node* del = root;root = root->_left;	//根节点也可以完成删除delete del;			//不能delete rootreturn true;
}else	//左右孩子都有:替换法
{Node* MinRight = root->_right;while (MinRight->_left){MinRight = MinRight->_left;}swap(MinRight->_key, root->_key);return _EraseR(root->_right, key);		//左右孩子都有这种情况要递归,必须有一次return//不能传入MinRight,因为他的父亲不能链接他的孩子(目的是为了托孤)
}

第一二种情况中,不需要判断root是否为根节点,即使为跟节点,也可以完成删除。

由于是引用传参,root可以作为一级指针就可以完成节点的链接。

第三种情况中,交换的本质还是交换键值,转化成去右子树删除对应的键值(找的是右子树的最小节点)

不能直接传入MinRight,前面的传参都是root->_left这种形式,保证可以直接找到父节点进行链接,直接传入一个节点无法完成爷孙(MinRight)节点的链接。这个地方的递归要右一次return,层层return回去。

return _EraseR(root->_right, key);    也可以改为 _EraseR(root->_right, key);    return true;

构造函数

	BSTree() = default;		//C++11

可以写成BST()  {}。也可以直接让BST() = default。这个是C++11才出现的语法,表示的意思是要求编译器为BST类生成默认的构造函数。

析构函数

~BSTree(){Destroy(_root);}
void Destroy(Node*& node)	//引用传参,才能让指针置空
{if (node == nullptr)return;Destroy(node->_left);Destroy(node->_right);delete node;node = nullptr;		//置空,防止出现野指针
}

引用传参,才能让指针置空。         后续结构析构,防止出现野指针。

拷贝构造

拷贝构造冲成员变量入手。

	//this(tree)BSTree(const BSTree<K>& t){_root = Copy(t._root);}

按照先序,一次new出新阶段,进行拷贝构造。 

Node* Copy(Node* root){if (root == nullptr)return nullptr;Node* newRoot = new Node(root->_key);	newRoot->_left = Copy(root->_left);newRoot->_right = Copy(root->_right);return newRoot;}

赋值重载

借助拷贝构造,完成形参的传参。内部交换跟指针,让原来的指针指向tmp二叉树,tmp二叉树跟需要拷贝构造的二叉树一致。(_root知只是指针,指向了BSTree的有效空间)

出作用域,自动调用tmp的析构,销毁原来的BSTree。

BSTree<K>& operator=(const BSTree<K> tmp)		//借助拷贝构造形成形参
{swap(tmp._root, _root);	//两个指针的指向交换return *this;		//tmp自动析构。
}

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

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

相关文章

【CS110L】Rust语言 Lecture3-4 笔记

文章目录 第三讲 所有权:移动与借用&例1例2例3 错误处理&#xff08;开头&#xff09;为什么空指针如此危险&#xff0c;我们能做什么以应对&#xff1f;— 引出Optionis_none()函数unwrap_or()函数常见用法 第四讲 代码实践:链表Box节点和链表的定义节点和链表的构造函数判…

Hack the 21LTR: Scene 1 靶机

靶机配置 kali配置 虚拟网络适配器配置 不行的时候关闭虚拟机&#xff0c;多点几次生成 主机发现和端口扫描 主机发现 arp-scan -l 端口扫描 端口扫描发现21&#xff0c;22&#xff0c;80端口开放 nmap -sV -A -T4 192.168.2.120 访问80端口 http://192.168.2.120/ 查看页…

SOMEIP_ETS_108: SD_Deregister_from_Eventgroup

测试目的&#xff1a; 验证DUT在接收到StopSubscribeEventgroup消息并取消订阅后&#xff0c;不会响应TestEventUINT8触发的事件。 描述 本测试用例旨在确保DUT在取消对事件组的订阅后&#xff0c;不会对随后的事件触发做出响应。 测试拓扑&#xff1a; 具体步骤&#xff1…

.NET内网实战:通过命令行解密Web.config

01阅读须知 此文所节选自小报童《.NET 内网实战攻防》专栏&#xff0c;主要内容有.NET在各个内网渗透阶段与Windows系统交互的方式和技巧&#xff0c;对内网和后渗透感兴趣的朋友们可以订阅该电子报刊&#xff0c;解锁更多的报刊内容。 02基本介绍 本文内容部分节选自小报童…

Spring Boot集成Akka Cluster快速入门Demo

1.什么是Akka Cluster&#xff1f; Akka Cluster将多个JVM连接整合在一起&#xff0c;实现消息地址的透明化和统一化使用管理&#xff0c;集成一体化的消息驱动系统。最终目的是将一个大型程序分割成若干子程序&#xff0c;部署到很多JVM上去实现程序的分布式并行运算&#xf…

编译原理之预处理

目录 生成预处理文件的的命令 预处理做了什么 实验 --------------------------------------------------------------------------------------------------------------------------------- 本篇文章主要是带着大家一起看看预处理阶段编译器都做了些什么 --------------…

十四,在Spring Boot当中对应“ Tomcat 服务器的相关配置”和“服务器的切换”的详细说明

十四&#xff0c;在Spring Boot当中对应“ Tomcat 服务器的相关配置”和“服务器的切换”的详细说明 文章目录 十四&#xff0c;在Spring Boot当中对应“ Tomcat 服务器的相关配置”和“服务器的切换”的详细说明1. 基本介绍2. 准备工作&#xff1a;3. 内置 Tomcat 的配置3.1 第…

Git项目管理工具

分布式版本控制系统

62. 不同路径、64. 最小路径和

思路 dp&#xff1a;代表到达当前位置的总方式 初始化&#xff1a;第一行的位置dp[0][j]&#xff1a;当前位置只能由左边的位置向右移动得到 所以只有1种方式 d[0][j]1, d[0][0]1 第一列的位置 dp[i][0]&#xff1a;当前位置只能由上一个位置向下移动得到 除此之外的位置可以由…

【Python】基本使用

目录 变量的类型 整数 int 浮点数 float 字符串 str 字符串长度 格式化字符串 布尔类型 动态类型 注释 获取输入 浮点数比较 多元赋值 for循环 函数的定义和调用 创建函数/定义函数 调用函数/使用函数 列表 创建列表 切片操作 遍历列表 新增元素 判断元…

2024最全网络安全工程师面试题(附答案),金九银十找工作必看!

《网安面试指南》http://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247484339&idx1&sn356300f169de74e7a778b04bfbbbd0ab&chksmc0e47aeff793f3f9a5f7abcfa57695e8944e52bca2de2c7a3eb1aecb3c1e6b9cb6abe509d51f&scene21#wechat_redirect 《Java代码审…

[matlab]matlab调用python的各种方法

前言 在MATLAB中&#xff0c;可以使用 py 函数来调用Python模块和函数。在此基础上&#xff0c;我们可以很轻易的调用python中的各种模块&#xff0c;方便我们在神经网络上的应用仿真。 以下是使用MATLAB调用Python模块的基本步骤&#xff1a; 确保你的系统已经正确安装了Py…

文心一言 VS 讯飞星火 VS chatgpt (347)-- 算法导论23.2 6题

六、假定一个图中所有的边权重均匀分布在半开区间[0,1)内。Prim算法和Kruskal算法哪一个可以运行得更快&#xff1f;如果要写代码&#xff0c;请用go语言。 文心一言&#xff1a; 在比较Prim算法和Kruskal算法在特定条件下的性能时&#xff0c;我们需要考虑几个因素&#xff…

复杂情感识别系统

复杂情感识别系统&#xff08;CERS&#xff09;是一种先进的技术平台&#xff0c;旨在通过分析情感的组合、相互关系及其动态变化来解读和识别复杂的情感状态。这种系统通常采用以下技术和方法&#xff1a; 机器学习与深度学习&#xff1a; 通过训练算法识别和解释大量情感数据…

[Linux]:进程间通信(上)

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ &#x1f388;&#x1f388;养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 所属专栏&#xff1a;Linux学习 贝蒂的主页&#xff1a;Betty’s blog 1. 进程间通信介绍 1.1 进程间通信的概念 进程间通信简称IPC&#xff08;In…

jdk相关介绍

JDK&#xff0c;全称Java Development Kit&#xff0c;是Java语言开发的基础工具包。它包含了Java运行时环境&#xff08;JRE&#xff09;以及用于开发Java应用程序的各种工具和库。JDK为Java程序员提供了编译、调试和运行Java应用程序所需的全部环境。 JDK的主要组成部分包括&…

离线数仓DWD层

离线数仓DWD层 DWD层设计要点&#xff1a;9.1 交易域加购事务事实表9.2 交易域下单事务事实表9.3 交易域取消订单事务事实表9.4 交易域支付成功事务事实表9.5 交易域退单事务事实表9.6 交易域退款成功事务事实表9.7 交易域购物车周期快照事实表9.8 工具域优惠券领取事务事实表9…

2024/9/15 408“回头看”之应用层小总结(下)

域名系统DNS: 本地域名服务器 本地域名服务器起着代理的作用&#xff0c;会将报文转发到根域名服务器、顶级域名服务器、权限域名服务器。 递归查询&#xff1a; 迭代查询&#xff1a; 文件传送协议FTP: FTP客户和FTP服务器之间使用的是tcp连接。 控制连接使用21端口&…

树莓派5上手

1 安装系统 Raspberry Pi OS 是基于 Debian 的免费操作系统&#xff0c;针对 Raspberry Pi 硬件进行了优化。Raspberry Pi OS 支持超过 35,000 个 Debian 软件包。树莓派 5 可以安装各种系统&#xff0c;但是如果对于系统没有特殊的要求&#xff0c;还是安装 Raspberry Pi OS …

基于SSM的二手车管理系统的设计与实现 (含源码+sql+视频导入教程)

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1 、功能描述 基于SSM的二手车管理系统4拥有三种角色 管理员&#xff1a;订单管理、在售车辆管理、下架车辆管理、品牌管理、分类管理、推荐管理、统计等 商家&#xff1a;登录注册、添加/下架/删除车辆…