【C++】详解二叉搜索树

news2024/12/25 0:14:45

目录

树概述

二叉搜索树概述

概念

特性

元素操作

插入

删除

模拟实现

框架

查找

插入

删除


树概述

——在计算机中是一种很常见的数据结构

树是一种很强大的数据结构,数据库,linux操作系统管理和windows操作系统管理所有文件的结构就是一颗树形结构

每个树有且只有一个根节点——根节点个数只有一个,节点可存储数据。

根节点往下可以有多个叶子节点,每个叶子节点也可以有多个叶子节点

如下图

每一个节点都可以是父亲节点,每个父亲节点的叶子节点都是孩子节点

根节点的深度为0,最后一层节点的深度为lgN

如果每一层深度的孩子节点的个数小于等于2,便是二叉树

左边的节点称为左孩子节点,右边的节点称为右孩子节点

如下图

如何正确的看一颗二叉树呢?

3是根节点,6作为3的左孩子也可以是一颗子树,这颗树是以6为根节点的树,也是3的左子树。以22为根节点来作为3的右子树也是一样的。

最小的子树是左孩子和右孩子都为空的叶子节点

也就是说每一个节点都可以作为根节点,从而成为父亲节点的子树,子树又可以拆成子树,直到左右都为空的叶子节点为止(叶子节点也可以看作一颗子树)。

遍历一颗树应该用递归的思维,如下是遍历顺序

二叉树前序遍历:根 ——> 左子树——>右子树

二叉树中序遍历:左子树——>根——>右子树

二叉树后序遍历:左子树——>右子树——>根

二叉树层序遍历:一条龙遍历,可参考【C++】详解STL的适配器容器之一:优先级队列 priority_queue-CSDN博客

二叉搜索树概述

概念

二叉搜索树是一种二叉树结构,二叉搜索树存储数据时需要符合如下规则(搜索树是支持泛型的,为了方便理解,小编以int类型为例):

左孩子节点的值 < 父亲节点的值 < 右孩子节点的值

如下示例:

特性

对于所有数据而言,

从整棵树的根节点开始,不断地找左子树,没有左孩子的左子树存的值最小

从整棵树的根节点开始,不断地找右子树,没有右孩子的右子树存的值最大

如果整棵树走中序遍历则是升序

那么用搜索二叉树找一个节点的效率如何呢?

查找的规则很简单,比根节点的值大往右子树走,比根节点的值小往左子树走。每查找一次就能“砍”掉一半数据,只需要查找log N次。类似于二分查找。

上述查找条件是:整棵树基本符合二叉树结构。如果是比较极端的结构,效率会接近N^2。比如下述结构

二叉搜索树搜索的效率的上限很高,下限很低

元素操作

插入

二叉搜索树中不允许有相等的值存在,因为要维持 “左树的值小于根小于右数的值” 这一特性。

如果要插入的值在树中不存在,这个值会插入到中间某个位置吗?

答案是不会,因为这个值一定会从整棵树的根节点比到叶子节点,然后插入到叶子节点的后面。如下示意图:

删除

删除的情况比较复杂,大致可以分为两种情况

孩子节点的个数是1或0

假设被删节点为cur,被删节点的父亲节点为fatherNode

先找是否有cur,找不到,删除失败。

找到了,判断curfatherNode的左孩子还是右孩子

是左孩子,让fatherNode的左指针指向cur的孩子节点

是右孩子,让fatherNode的右指针指向cur的孩子节点

如果没有孩子节点,则fatherNode的指针指向空

改变指向关系后整棵树就没有cur节点了,最后释放cur的内存

fatherNode的左孩子被删应该有新的左孩子接替,右孩子应该有新的右孩子接替,这样才不会破坏二叉搜索树的结构,所以要判断curfatherNode的左孩子还是右孩子

孩子节点的个数是2

孩子节点是1或0的时候只需改变指向关系,然后再释放被删节点的内存即可。

那么被删节点的孩子个数是两个的时候还能这么做吗?

显然是不能的,因为一个父亲节点最多只能有两个孩子节点

删除孩子节点的个数是2的节点,最核心的思想是:

被删节点为cur但不直接删cur这个节点,因为这样会破坏这颗树的结构。

一个孩子节点为1或0的节点为leftMax_Node,让leftMax_Node节点的值和cur节点的交换,然后删leftMax_Node节点

通过替换的方法就完成了cur节点的删除

那么怎么找孩子节点为1或0的节点呢?

左子树的最大节点右子树的最小节点

注意:是被删节点cur的左树(右树),而不是整棵树的左树(右树)

在上文二叉树的特性中已经提过,找最大值或最小值,找的一定是叶子节点

整个二叉搜索树中,左子树存小值,右子树存大值。

如果选左子树的最大值放在根节点的位置,依旧符合左子树的值小于根小于右子树这一规则,删掉该节点不会破坏二叉树的结构。

左子树的最大节点或右子树的最小节点是完美的  “替罪羊” 

模拟实现

模板相关知识可参考:

【C++】详解C++的模板-CSDN博客

内存管理相关知识可参考:

【C++】C++的内存管理-CSDN博客

框架

节点的设计

有两个指针指向左右孩子

在用一个变量存值

template <class K>
class BSNode
{
public:

	BSNode(const K& Key = K())
		:_left(nullptr)
		,_right(nullptr) 
		,_key(Key)  
	{
	}

	BSNode<K>* _left;
	BSNode<K>* _right; 
	K _key;

};

二叉搜索树的设计

只需要一个节点指针指向整棵树的根节点即可

template <class K>
class BSTree
{
	typedef BSNode<K> node;

//......

private:

	node* _root;  
};

查找

查找的代码很简单,小编就不细讲了。

有一点要提醒大家,大家一定要用判断语句把查找条件分开,比根大就往右树查找,比根小就往左树查找。千万不要写成暴力查找——不管值的大小,遍历整棵树

代码如下

bool _FindNode(node* root, const K& key) 
{
	while (root)
	{
		if (root->_key < key) 
		{
			root = root->_right;
		}
		else if (root->_key > key)
		{
			root = root->_left;
		}
		else
		{
			return false;
		}

		return true;
	}
}

可以把上述代码设计成私有,再封装一层共有的函数,这样做的意义是不用传节点的指针了

bool FindNode(const K& key)
{
	return _FindNode(_root, key);           
}

插入

接口

bool Insert(const K& key) 

首先应该判断整棵树是否为空,如果为空,直接让_root直接指向新节点

if (_root == nullptr) //如果为空
{
	_root = new node(key); //头节点指针指向新节点
	return true;
}

二叉搜索树是不允许有重复的值出现的

应该再写一遍查找的逻辑,因为_root是指向整棵树的根的,所以需要定义一个临时指针,当我们找到相等的值时,插入失败。

node* cur = _root;  
node* fatherNode = nullptr; 

while (cur) 
{
	if (cur->_key < key) //插入的元素比当前节点的值大,
	{
		fatherNode = cur; //记录父亲节点的指针
		cur = cur->_right;   //往右树找
	}
	else if (cur->_key > key)
	{
		fatherNode = cur;
		cur = cur->_left;
	}
	else
	{
		return false;
	}
}

没找到的话,说明临时指针已经找到空了,临时指针的父亲节点就是合适的叶子节点了,插在叶子节点的孩子位置即可,是左孩子还是右孩子需要和父亲节点比较大小

  node* newNode =  new node(key);
  if (fatherNode->_key < key)//判断要插入父亲节点的左边还是右边
  {
	  fatherNode->_right = newNode; 
  }
  else
  {
	  fatherNode->_left = newNode; 
  }

删除

接口

bool Erase(const K& key)    

先判断是否是空树

if (nullptr == _root)//如果是空树,删除失败
{
	return false;
}

和插入类似,要判断被删的节点是否存在

node* cur = _root; //临时变量,cur是指要删除的节点
node* fatherNode = nullptr; //cur的父亲节点,

while (cur)//找要删除的节点
{
	if (cur->_key < key) 
	{
		fatherNode = cur;
	cur = cur->_right;
	}
	else if (cur->_key > key)
	{
		fatherNode = cur;
		cur = cur->_left;
	}
	else
	{
		break;
	}
}
if (nullptr == cur) //没找到要删除的节点,删除失败
{
	return false;
}

下面就是要删除节点的逻辑了,上文已经讲了删除的原理,这里细讲代码细节

孩子节点数量为1或0

节点的孩子树不管是1还是0,可以只写左孩子为空的情况和右孩子为空的情况,左右孩子都为空的情况可以不写。

因为左右孩子都为空的情况,可以走左孩子为空的逻辑,只是指向新节点还是指向空的问题。

	if (cur->_left == nullptr) //要删除的节点左为空
	{
		if (cur == _root)  //极端情况,
		{
			_root = cur->_right;
			delete cur;
			return true;
		}

		if (fatherNode->_left == cur)  //判断要删除的节点在父亲节点的左边还是右边
			fatherNode->_left = cur->_right;
		else                                   //改变指向
			fatherNode->_right = cur->_right;

		delete cur; //删除
		return true;

	}
	else if (cur->_right == nullptr)//要删除的节点右为空
	{
		if (cur == _root) //极端情况
		{
			_root = cur->_left;  
			delete cur; 
			return true;
		}

		if (fatherNode->_left == cur)  //判断要删除的节点在父亲节点的左边还是右边
			fatherNode->_left = cur->_left; 
		else                               //改变指向
			fatherNode->_right = cur->_left; 

		delete cur; //删除节点
		return true;
	}

极端情况做了特殊处理

极端情况如下图所示

孩子节点数量为2

else //要删除的节点左右都不为空
{
	node* leftMax_Node = cur->_left;  //左树最大节点 (被删节点的左树)
	node* leftMax_FatherNode = cur; //左树最大节点的父亲节点             

	while (leftMax_Node->_right)      //找代替节点,左树的最右节点(找左树的最大值)
	{
		leftMax_FatherNode = leftMax_Node; 

		leftMax_Node = leftMax_Node->_right;     
	}

	std::swap(cur->_key, leftMax_Node->_key);   //交换完之后,左树的最大节点就是要删的值了  

	if (leftMax_FatherNode->_left == leftMax_Node)  //要判断被删节点是父亲节点的左孩子还是右孩子
		leftMax_FatherNode->_left = leftMax_Node->_left;
	else
		leftMax_FatherNode->_right = leftMax_Node->_left;

	delete leftMax_Node;    
	return true;
}

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

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

相关文章

【基础详解】快速入门入门 SQLite数据可

简介 SQLite 是一个开源的嵌入式关系数据库&#xff0c;实现了自给自足的、无服务器的、配置无需的、事务性的 SQL 数据库引擎。它是一个零配置的数据库&#xff0c;这意味着与其他数据库系统不同&#xff0c;比如 MySQL、PostgreSQL 等&#xff0c;SQLite 不需要在系统中设置…

golang中的字节序 binary BigEndian 大端 , LittleEndian 小端 理解与write写入注意事项

在golang的binary包中有2个字节系的变量定义BigEndian和LittleEndian 这个东西是go里面很有特点的玩意&#xff0c;我们在java, php等语言中是基本看不到&#xff0c;因为大部分的语言默认使用的是BigEndian 大端模式&#xff0c; 而go语言里面是你自己可选的。 这个字节系大小…

Java的类和对象

Java的类和对象 前言一、面向过程和面向对象初步认识C语言Java 二、类和类的实例化基本语法示例注意事项 类的实例化 三、类的成员字段/属性/成员变量注意事项默认值规则字段就地初始化 方法static 关键字修饰属性代码内存解析 修饰方法注意事项静态方法和实例无关, 而是和类相…

@Async详解,为什么生产环境不推荐直接使用@Async?

一、Async 注解介绍&#xff1a; Async 注解用于声明一个方法是异步的。当在方法上加上这个注解时&#xff0c;Spring 将会在一个新的线程中执行该方法&#xff0c;而不会阻塞原始线程。这对于需要进行一些异步操作的场景非常有用&#xff0c;比如在后台执行一些耗时的任务而不…

ssms用户登陆失败,服务器处于单用户模式。目前只有一位管理员能够连接。解决方案

文章目录 问题解决方案单用户模式什么是单用户模式&#xff1f;为什么使用单用户模式&#xff1f;实现步骤 问题 连接smss的时候发现无法连接&#xff0c;显示 服务器处于单用户模式。目前只有一位管理员能够连接 解决方案 打开SQL Server配置管理器 右键属性 在启动参数的最…

Python 之 日志巡检脚本

脚本说明 使用Paramiko库进行SSH连接的自动化脚本&#xff0c;用于检查、配置和排除设备故障。说明如下&#xff1a; 导入所需的库&#xff1a;paramiko、json、logging和concurrent.futures。定义配置文件路径&#xff08;devices.json&#xff09;和日志文件路径&#xff0…

Unity射击游戏开发教程:(26)创建绕圈跑的效果

unity游戏 在本文中,我将介绍如何为敌人创建圆周运动。gif 中显示的确切行为是敌人沿着屏幕向下移动,直到到达某个点,一旦到达该点,它就会绕圈移动。

c语言:摆脱对指针的恐惧【4】

在上一期指针我们讲到了二级指针是的作用是存放一级指针的地址&#xff0c;还讲了指针数组是一个可以存放若干个指针变量的数组&#xff0c;这里我们再复习一下&#xff0c;下面指针数组是什么意思&#xff1f; int* arr1[10]; //整形指针的数组 char *arr2[4]; //一级字符指针…

JavaSE——集合框架二(1/6)-前置知识-可变参数、Collections工具类

目录 可变参数 Collections工具类 Collections的常用静态方法 实例演示 可变参数 可变参数 就是一种特殊形参&#xff0c;定义在方法、构造器的形参列表里&#xff0c;格式是&#xff1a;数据类型...参数名称 可变参数的特点和好处 特点&#xff1a;可以不传数据给它&am…

常用API(正则表达式、爬取、捕获分组和非捕获分组 )

1、正则表达式 练习——先爽一下正则表达式 正则表达式可以校验字符串是否满足一定的规则&#xff0c;并用来校验数据格式的合法性。 需求&#xff1a;假如现在要求校验一个qq号码是否正确。 规则&#xff1a;6位及20位之内&#xff0c;0不能在开头&#xff0c;必须全部是数字…

超详细的前后端实战项目(Spring系列加上vue3)前端篇+后端篇(三)(一步步实现+源码)

好了&#xff0c;兄弟们&#xff0c;继昨天的项目之后&#xff0c;开始继续敲前端代码&#xff0c;完成前端部分&#xff08;今天应该能把前端大概完成开启后端部分了&#xff09; 昨天补充了一下登录界面加上了文章管理界面和用户个人中心界面 完善用户个人中心界面 修改一…

【被小学生教育的一天】

今日文章有感&#xff0c;记博主文章&#x1f4dd;&#xff0c;分享与感触。原文如下&#xff0c; 今天让小学生给我上了一课啊&#xff0c;今天去一所学校就实地考察感触特别深啊&#xff0c;就我从里面抽取一小块啊&#xff0c;给大家看一下&#xff0c;这个学校的教学规划&a…

STM32中断编程入门

文章目录 一、 理论部分1.中断系统2.中断执行流程3.NVIC的基本结构4.EXTI介绍5.AFIO复用IO口 二、实验目的&#xff1a;学习stm32中断原理和开发编程方法。使用标准完成以下任务&#xff1a;&#xff08;一&#xff09;实验一 开关控制LED的亮灭1.代码部分2.运行结果 &#xff…

SpringBoot+Vue开发记录(七)-- 跨域文件与Restful风格

本篇文章的主要内容是关于项目的跨域配置和给项目添加restful风格接口。 重点是文件粘贴 文章目录 一、 跨域二、Restful风格1. 什么是restful风格&#xff1f;2. 项目文件结构3. 新建文件4. 在Controller中进行修改 一、 跨域 跨域问题暂时也就那样&#xff0c;解决方法就是…

【搜索方法推荐】高效信息检索方法和实用网站推荐

博主未授权任何人或组织机构转载博主任何原创文章&#xff0c;感谢各位对原创的支持&#xff01; 博主链接 本人就职于国际知名终端厂商&#xff0c;负责modem芯片研发。 在5G早期负责终端数据业务层、核心网相关的开发工作&#xff0c;目前牵头6G算力网络技术标准研究。 博客…

AI视频教程下载:ChatGPT API、HTML、CSS 和 JS开发AI聊天机器人

在课程中,你将开启一段令人兴奋的聊天机器人开发之旅,并装备自己创建智能对话代理所需的技能和知识。 利用 ChatGPT API、HTML、CSS 和 JavaScript 的强大功能,你将学习如何设计和构建吸引用户并提供个性化体验的聊天机器人界面。深入探讨聊天机器人开发的基础知识,了解对话设…

【Docker学习】深入研究命令docker exec

使用docker的过程中&#xff0c;我们会有多重情况需要访问容器。比如希望直接进入MySql容器执行命令&#xff0c;或是希望查看容器环境&#xff0c;进行某些操作或访问。这时就会用到这个命令&#xff1a;docker exec。 命令&#xff1a; docker container exec 描述&#x…

AI网络爬虫-自动获取百度实时热搜榜

工作任务和目标&#xff1a;自动获取百度实时热搜榜的标题和热搜指数 标题&#xff1a;<div class"c-single-text-ellipsis"> 东部战区台岛战巡演练模拟动画 <!--48--></div> <div class"hot-index_1Bl1a"> 4946724 </div> …

OpenAI撤回有争议的决定:终止永久性非贬损协议

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

HTML静态网页成品作业(HTML+CSS+JS)——心理健康教育介绍网页(4个页面)

&#x1f389;不定期分享源码&#xff0c;关注不丢失哦 文章目录 一、作品介绍二、作品演示三、代码目录四、网站代码HTML部分代码 五、源码获取 一、作品介绍 &#x1f3f7;️本套采用HTMLCSS&#xff0c;使用Javacsript代码使用下拉菜单的实现以及首页图片的轮播&#xff0c…