【数据结构】二叉搜索树——高阶数据结构的敲门砖

news2024/11/17 21:19:38

目录

树概述

二叉搜索树概述

概念

特性

元素操作

插入

删除

模拟实现

框架

查找

插入

删除


树概述

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

树是一种很强大的数据结构,数据库,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/1712296.html

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

相关文章

SQL刷题笔记day4补

1题目 我的正确代码 select e.last_name,e.first_name,d.dept_name from employees e left join (select departments.dept_name,dept_emp.emp_no,dept_emp.dept_no from departments join dept_emp on departments.dept_nodept_emp.dept_no) d on e.emp_nod.emp_no复盘&…

1111 对称日

solution 把日期转换为标准位数&#xff0c;不足则补零&#xff0c;判断是否为对称日期。月日可能会缺一个0&#xff0c;年份可能缺0~3个零。 #include<iostream> #include<string> #include<map> using namespace std; int main(){int n, flag;map<str…

Orange AIpro Color triangle帧率测试

OpenGL概述 OpenGL ES是KHRNOS Group推出的嵌入式加速3D图像标准&#xff0c;它是嵌入式平台上的专业图形程序接口&#xff0c;它是OpenGL的一个子集&#xff0c;旨在提供高效、轻量级的图形渲染功能。现推出的最新版本是OpenGL ES 3.2。OpenGL和OpenCV OpenCL不同&#xff0c;…

HTTP协议的基本了解

一、HTTP-概述 HTTP&#xff1a;Hyper Text Transfer Protocol(超文本传输协议)&#xff0c;规定了浏览器与服务器之间数据传输的规则。 http是互联网上应用最为广泛的一种网络协议 。http协议要求&#xff1a;浏览器在向服务器发送请求数据时&#xff0c;或是服务器在向浏览器…

计算机网络7——网络安全1 概述与加密

文章目录 一、网络安全问题概述1、计算机网络面临的安全性威胁2、安全的计算机网络3、数据加密模型 二、两类密码体制1、对称密钥密码体制2、公钥密码体制 随着计算机网络的发展&#xff0c;网络中的安全问题也日趋严重。当网络的用户来自社会各个阶层与部门时&#xff0c;大量…

区间相交-435. 无重叠区间,56. 合并区间

题目连接及描述 435. 无重叠区间 - 力扣&#xff08;LeetCode&#xff09; 56. 合并区间 - 力扣&#xff08;LeetCode&#xff09; 题目分析 二维数组&#xff0c;数组中每个元素为大小为2的一维数组&#xff0c;求移除区间的最小数量&#xff0c;使剩余区间互不重叠。今天写…

Python-3.12.0文档解读-内置函数pow()详细说明+记忆策略+常用场景+巧妙用法+综合技巧

一个认为一切根源都是“自己不够强”的INTJ 个人主页&#xff1a;用哲学编程-CSDN博客专栏&#xff1a;每日一题——举一反三Python编程学习Python内置函数 Python-3.12.0文档解读 目录 详细说明 功能描述 参数 返回值 使用规则 示例代码 基本使用 模运算 变动记录…

Windows 下载安装Apache

一、官网下载 1、打开Apache官网http://httpd.apache.org&#xff0c;点击Download。 2、选择Windows版本&#xff0c;点击链接。 3、选择对应版本选择下载。 二、安装、设置 1、将下载好的解压。 2、依次打开Apache24-conf-httpd.conf,用记事本打开 1)、修改路径 2)、修改…

C# TcpClient

TcpClient 自己封装的话&#xff0c;还是比较麻烦的&#xff0c;可以基于线程&#xff0c;也可以基于异步写&#xff0c;最好的办法是网上找个插件&#xff0c;我发现一个插件还是非常好用的&#xff1a;STTech.BytesIO.Tcp 下面是这个插件作者的帖子&#xff0c;有兴趣的可以…

【深度学习实战—9】:基于MediaPipe的人脸关键点检测

✨博客主页&#xff1a;王乐予&#x1f388; ✨年轻人要&#xff1a;Living for the moment&#xff08;活在当下&#xff09;&#xff01;&#x1f4aa; &#x1f3c6;推荐专栏&#xff1a;【图像处理】【千锤百炼Python】【深度学习】【排序算法】 目录 &#x1f63a;一、Med…

IDEA中各种Maven相关问题(文件飘红、下载依赖和启动报错)

错误情况 包名、类名显示红色、红色波浪线&#xff0c;大量依赖提示不存在&#xff08;程序包xxx不存在&#xff09; 工程无法启动 一、前提条件 1、使用英文原版IDEA 汉化版的可能有各种奇怪的问题。建议用IDEA英文版&#xff0c;卸载重装。 2、下载maven&#xff0c;配置环…

PFC+LLC 概述

总电路图 方案为&#xff1a;PFC&#xff08;NCP1654D&#xff09;LLC&#xff08;NCPB97B&#xff09;同步整流 输入为220V&#xff08;正负20%&#xff09;输出48V&#xff0c;600W电源 组成 1.输入 零线&#xff0c;火线&#xff0c;大地线&#xff0c;有防雷电路&#…

后量子加密算法的数学原理

后量子加密算法是一类专为抵御量子计算机攻击而设计的加密算法。随着量子计算技术的迅速发展&#xff0c;传统的加密算法如RSA和椭圆曲线密码学在量子计算机面前变得脆弱&#xff0c;因此&#xff0c;开发能够在量子计算时代保持安全性的加密算法变得尤为重要。下面将详细介绍后…

使用小猪APP分发打造高效的App封装工具

你是否曾经因为App封装和分发的复杂性而头疼不已&#xff1f;在这个移动应用迅速发展的时代&#xff0c;开发人员不仅需要专注于应用的功能和用户体验&#xff0c;还必须面对繁琐的封装和分发过程。幸运的是&#xff0c;小猪APP分发www.appzhu.cn正好为我们提供了一个简便而高效…

STM32_HAL_使用FPEC实现闪存的读写

STM32的FLASH结构 主存储器&#xff08;Main Memory&#xff09;&#xff1a;这是STM32中最大的存储区域&#xff0c;用于存储用户的程序代码、常量数据以及程序运行时不变的数据。STM32的主存储器通常被组织为多个扇区&#xff08;sector&#xff09;&#xff0c;每个扇区的大…

构建高效稳定的运维服务体系:技术架构解析与最佳实践

在当今数字化时代&#xff0c;运维服务对于企业的稳定运行和业务发展至关重要。本文将深入探讨运维服务的技术架构&#xff0c;介绍如何构建高效稳定的运维服务体系&#xff0c;并分享最佳实践。 ### 1. 概述 运维服务的技术架构是支撑整个运维体系的核心&#xff0c;它涵盖了…

邻接矩阵广度优先遍历

关于图的遍历实际上就两种 广度优先和深度优先&#xff0c;一般关于图的遍历都是基于邻接矩阵的&#xff0c;考试这些&#xff0c;用的也是邻接矩阵。 本篇文章先介绍广度优先遍历的原理&#xff0c;和代码实现 什么是图的广度优先遍历&#xff1f; 这其实和二叉树的层序遍…

基于魔搭开源推理引擎 DashInfer实现CPU服务器大模型推理--理论篇

前言 在人工智能技术飞速发展的今天&#xff0c;如何高效地在CPU上运行大规模的预训练语言模型&#xff08;LLM&#xff09;成为了加速生成式AI应用广泛落地的核心问题。阿里巴巴达摩院模型开源社区ModelScope近期推出了一款名为DashInfer的推理引擎&#xff0c;旨在解决这一挑…

Ingress controller:Kubernetes 的瑞士军刀

原文作者&#xff1a;Brian Ehlert of F5 原文链接&#xff1a;Ingress controller&#xff1a;Kubernetes 的瑞士军刀 转载来源&#xff1a;NGINX 中文官网 NGINX 唯一中文官方社区 &#xff0c;尽在 nginx.org.cn 许多人认为 Ingress controller&#xff08;Ingress 控制器&…

CCF20211201——序列查询

CCF20211201——序列查询 代码如下&#xff1a; #include<bits/stdc.h> using namespace std; #define Max 10000000 int a[Max]{0},b[Max]{0}; int main() {int n,m;int sum0,x0,flag0;cin>>n>>m;for(int i1;i<n;i){cin>>a[i];}for(int i0,x0;i&l…