二叉搜索树(二叉排序树、二叉查找树)

二叉搜索树(二叉排序树、二叉查找树)

  • 一、定义
  • 二、操作
    • (一)中序遍历
    • (二)查找
    • (三)插入
    • (四)删除
  • 三、二叉搜索树的应用
  • 四、二叉搜索树操作的性能分析
  • 五、总结

一、定义

二叉搜索树(BST:Binary Search Tree)也叫二叉排序树,也叫二叉查找树。
二叉搜索树要么是一颗空树,要么是具有以下性质的树:
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树
从定义可以看出:二叉搜索树中不能有两个值相等的结点
比如:
在这里插入图片描述

二、操作

下面是二叉搜索树的定义(为了满足多种情况,定义成了一个模板):

template<class T>
class BSTreeNode //二叉搜索树结点
{
public:BSTreeNode(T val = T()) :_val(val), _left(nullptr), _right(nullptr){}BSTreeNode* _left;BSTreeNode* _right;T _val;
};template<class T>
class BSTree //二叉搜索树
{typedef BSTreeNode<T> Node;public:Node* Find(const T& key);bool Insert(const T& key);bool Erase(const T& key);void InOrder();private:Node* _root = nullptr;
};

(一)中序遍历

同二叉树的中序遍历

void _InOrder(Node* root)
{if (root == nullptr)return;_InOrder(root->_left); //中序遍历左子树cout << root->_val << " ";_InOrder(root->_right); //中序遍历右子树
}
void InOrder()
{_InOrder(_root); //递归cout << "\n";
}

(二)查找

从根开始比较查找,比根小则去左子树中查找,比根大则去右子树中查找,在左右子树中以同样的方式继续查找,如果走到空还没找到,这个值就不存在。最多查找树的高度次。
下面是递归和非递归两种实现方法:

Node* _Find(Node* root, const T& key)
{if (root == nullptr) //没找到返回nullptrreturn nullptr;if (root->_val > key) //在右子树中查找return _Find(root->_left, key);else if (root->_val < key) //在左子树中查找return _Find(root->_right, key);else //找到了return root;
}
Node* Find(const T& key)
{//递归//return _Find(_root, key);//非递归Node* root = _root;while (root){if (root->_val == key)return root;else if (root->_val > key)root = root->_left;else if (root->_val < key)root = root->_right;}return root;
}

(三)插入

创建一个二叉搜索树的过程,也就是不断的插入。插入的具体过程如下:
1、树为空,则直接新增节点,赋值给root指针
2、树不空,按二叉搜索树性质查找插入位置,插入新节点
3、每次插入的结点都是叶子结点
比如:
在这里插入图片描述
下面是递归和非递归两种实现方法:

bool _Insert(Node*& root, const T& key)
{if (root == nullptr){root = new Node(key);return true;}if (root->_val > key)return _Insert(root->_left, key);else if (root->_val < key)return _Insert(root->_right, key);elsereturn false;
}
bool Insert(const T& key)
{//递归//return _Insert(_root, key);//非递归Node* newnode = new Node(key); //创建一个新结点if (_root == nullptr) //空树特殊处理{_root = newnode;return true;}Node* root = _root, *pre = nullptr; //pre指向root的双亲while (root) //寻找插入的位置{if (root->_val == key) //已经存在,无需插入return false;else if (root->_val > key) //在左子树中查找插入位置{pre = root;root = root->_left;}else if (root->_val < key) //在右子树中查找插入位置{pre = root;root = root->_right;}}//判断插入到左孩子还是右孩子if (pre->_val > key)pre->_left = newnode;elsepre->_right = newnode;return true;
}

(四)删除

首先查找元素是否在二叉搜索树中,如果不存在,则返回,否则要删除的结点可能分下面四种情况:
1、要删除的结点无孩子结点
2、要删除的结点只有左孩子结点
3、要删除的结点只有右孩子结点
4、要删除的结点有左、右孩子结点
情况1:直接删除该结点–直接删除
情况2:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点–直接删除
情况3:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点–直接删除
情况4:在它的右子树中寻找中序遍历下的第一个结点(右子树中值最小,也就是右子树中最左边的结点,并且该结点一定没有左子树),用它的值填补到被删除结点中,再来处理该结点的删除问题(该结点删除问题下面注意中会说明)–替换法删除。或者在它的左子树中寻找中序遍历下的最后一个结点(左子树中值最大,也就是左子树中最右边的结点,并且该结点一定没有右子树),用它的值填补到被删除结点中,再来处理该结点的删除问题–替换法删除。
注意:
a:其中可以发现,情况1可以当作情况2或情况3处理,因为情况1删除之后,它的双亲指向空,当作情况2处理的话,虽然它没有左孩子,但是可以看作是null左孩子,然后将null赋值给它的双亲,他的双亲仍然是指向空,情况3也是同理。这样的做的好处是归纳整理可以更好的编程。
b:情况4中用替换法删除,替换的那个结点删除问题该怎么处理,如果用右子树中最小关键字结点替换,该结点一定没有左子树,那么删除该结点就变成了情况3。如果是用左子树中最大关键字结点替换,该结点一定没有右子树,那么删除该结点就变成了情况2。这样一来删除任何一个结点都很容易,要么直接删除,要么替换删除后再使用一次直接删除。
c:对于b中那种处理方法之外还有一种方法,一直用替换法删除替换的那个结点,直到最后一次用叶子结点替换,然后将叶结点删除。如果树的高度很高并且树的性状不理想的情况下,可能需要使用很多次替换法,所有这种方法没有b快。
比如:
在这里插入图片描述

下面是递归和非递归两种实现方法:

bool _Erase(Node*& root, const T& key)
{if (root == nullptr) //没找到return false;if (root->_val > key)return _Erase(root->_left, key);else if (root->_val < key)return _Erase(root->_right, key);else{Node* del = root;//分三种情况(思想同非递归)if (root->_left == nullptr)root = root->_right;else if (root->_right == nullptr)root = root->_left;else if (root->_left != nullptr && root->_right != nullptr){//用右子树中最左边结点替换Node* tmp = root->_right;while (tmp->_left)tmp = tmp->_left;swap(root->_val, tmp->_val); //库函数,交换两个结点的值return _Erase(root->_right, key); //递归删除替换的那个结点(这一步也可不使用递归,非递归也行)//(注意这里并不是从根重新开始查找删除值为key的结点,因为我们上一步交换了待删除结点root和替换结点tmp的值。要是从根结点开始查找,是找不到值为key的结点。而是从结点root的右子树开始删除值为key的结点)}delete del;return true;}
}
bool Erase(const T& key)
{//递归//return _Erase(_root, key);//非递归//一、查找Node* root = _root, *pre = nullptr; //pre指向root的双亲while (root){if (root->_val == key)break;else if (root->_val > key){pre = root;root = root->_left;}else if (root->_val < key){pre = root;root = root->_right;}}//二、删除if (root == nullptr) //没找到return false;else //找到{//找到后,删除的结点分三种情况if (root->_left == nullptr) //情况2{if (root == _root) //根节点特殊处理(主要是因为根结点的时候pre为nullptr,不能用pre进行操作)_root = _root->_right;else{//判断当前结点时双亲的左孩子还是右孩子if (pre->_left == root) pre->_left = root->_right;else if (pre->_right == root)pre->_right = root->_right;}delete root;}else if (root->_right == nullptr) //情况3{if (root = _root) //根节点特殊处理_root = _root->_left;else{	//判断当前结点时双亲的左孩子还是右孩子if (pre->_left == root)pre->_left = root->_left;else if (pre->_right == root)pre->_right = root->_left;}delete root;}else if (root->_left != nullptr && root->_right != nullptr) //情况4{//用右子树中最左边的结点替换//1、找右子树中最左边结点Node* tmp = root->_right, *father = root; //tmp指向右子树中最左边结点,father是指向tmp的双亲结点while (tmp->_left){father = tmp;tmp = tmp->_left;}//2、替换(仅赋值)root->_val = tmp->_val;//3、删除替换结点(有可能root的右孩子就是右子树中最左边结点,有可能不是,所有需要判断赋值)if (father->_left == tmp)father->_left = tmp->_right;else if (father->_right == tmp)father->_right = tmp->_right;delete tmp;}return true;}
}

三、二叉搜索树的应用

K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
比如:私人小区停车场,只允许该小区的车辆进入。每次只需要识别进入车辆的车牌是否在小区的系统中,所有只需要将车牌信息存入系统即可,这里的车牌就是key。
KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。
比如:商场停车场,所有外来车辆都可以进入,并且按停车时长收费。每次车辆进入时记录车辆的车牌号和入场时间,将车牌作为key,入场时间作为value。每次车辆离场时,只需要在系统中查找到该车牌号,然后就能找到车牌对应的入场时间,就可以算出停车费用。KV模型的实现在K的基础上增加一个value就可以。

四、二叉搜索树操作的性能分析

插入和删除操作都必须先查找,因此查找效率代表了二叉搜索树中各个操作的性能。查找的效率取决于结点在树中的深度。
对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树。
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为: l o g 2 N log_2 N log2N
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为: N 2 \frac{N}{2} 2N

五、总结

插入和删除这两个操作多注意一些细节和特殊情况,无论是插入还是删除,都需要知道插入位置和待删除结点的双亲结点,并且删除的时候,删除根节点的时候要特殊处理一下。

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

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

相关文章

解锁服务器外联:TinyProxy一键搭建指南

引言 在服务器需要访问外网的情况下&#xff0c;由于网络安全等原因&#xff0c;许多生产服务器限制了对外网的访问。本文介绍如何通过在一台能够访问外网的服务器上部署TinyProxy来实现代理&#xff0c;使得其他服务器可以通过该代理访问外网。 安装 TinyProxy是一个轻量级…

java异常处理设计

异常的继承体系 java 中的异常的超类是 java.lang.Throwable(后文省略为 Throwable), 他有俩自类Exception和Error&#xff0c;Error是由jvm管理&#xff0c;我们不需要考虑。 RuntimeException是Exception的子类。 检查异常&#xff08;Checked Exceptions&#xff09;&#…

【kubernetes】认识K8S基础理论

目录 一、k8s是什么&#xff1f; 二、为什么要用k8s&#xff1f; 三、k8s的主要功能 四、k8s的集群架构和组件 4.1k8s的集群架构介绍 4.2k8s的master的核心组件 ①kube-apiserver&#xff1a;作为所有服务请求的统一访问入口 ②kube-controller-manager&#xff1a;控制…

腾讯云域名解析

腾讯云域名解析 1.登录腾讯云控制台&#xff0c;点击“云产品”&#xff0c;选择“云解析”&#xff0c;进入云解析界面&#xff1b;2.在此界面可以选择购买或者添加新的域名&#xff0c;若已经购买了域名&#xff0c;则在域名列表处选择需要解析的域名&#xff0c;点击“解析…

用Python实现学生成绩数据分析

我的代码使用了pygal库来创建一个简单的折线图&#xff0c;并将其保存为SVG格式的文件。下面是对您代码的分析&#xff1a; 学生成绩数据分析表&#xff1a; 分析代码&#xff1a; 导入库&#xff1a;您导入了pygal库&#xff0c;这是一个用于生成可缩放矢量图形&#xff08;S…

Chrome插件精选 — 颜色拾取

Chrome实现同一功能的插件往往有多款产品&#xff0c;逐一去安装试用耗时又费力&#xff0c;在此为某一类型插件挑选出比较好用的一款或几款&#xff0c;尽量满足界面精致、功能齐全、设置选项丰富的使用要求&#xff0c;便于节省一个个去尝试的时间和精力。 1. ColorZilla 下…

vue保留用户在列表的操作记录, beforeRouteLeave离开当前组件缓存数据即可

最近遇到一个需求,用户在列表页的查询输入框输入条件后,点击查询,然后此时切换菜单,再回到之前的页面,希望能停留在上一次输入的结果上,如下例子,用户管理页面,输入yangfan这个关键词搜索后,结果如下图: 当我此时点击权限管理后,再点击用户管理切回来,结果依旧如上…

如何修改docker容器的端口映射

要修改 Docker 容器的端口映射&#xff0c;你需要停止并删除现有的容器&#xff0c;然后使用新的端口映射重新运行容器。以下是详细步骤&#xff1a; 停止容器&#xff1a; 使用 docker stop 命令停止正在运行的容器。替换 <container_id> 为你要停止的容器的 ID 或者容器…

什么是智慧公厕?智慧公厕是基于“云大脑”的跨区域公共厕所综合管理系统

在城市快速发展的今天&#xff0c;公共厕所的管理和维护成为了一个重要的问题。传统的公共厕所管理方式往往效率低下、成本较高。然而&#xff0c;随着科技的进步和应用&#xff0c;智慧公厕已经成为了解决这一难题的利器。本文以智慧公厕源头厂家广州中期科技有限公司&#xf…

信息安全工程师 软考回顾(一)

&#x1f433;概述 图源&#xff1a;文心一言 信息安全证书已经考了一年有余&#xff0c;尽管我目前没有从业安全的打算&#xff0c;况且自己的实践能力与从业标准依然有所差距&#xff0c;但其中的内容也值得再温习一遍~&#x1f95d;&#x1f95d; 另外&#xff0c;如果你对…

四、深入学习TensorRT,Developer Guide篇(三)

上一篇文章我们一起看了下TensorRT有哪些特性或者支持哪些功能&#xff0c;这一节我们来详细的从API出发研究一下具体的实现&#xff0c;难度要上升了哦&#xff0c;请系好安全带&#xff0c;准备发车&#xff01; 文章目录 3. The C API3.1 The Build Phase3.1.1 Creating a …

常见消息中间件分享

文章目录 概念核心角色作用&使用场景应用解耦异步通信削峰填谷大数据流处理 使用模型点对点模型发布-订阅模型 常见消息中间件介绍一、kafka二、RabbitMQ三、RocketMQ 比较一、Kafka如何实现高吞吐量二、RocketMQ如何实现事务消息 概念 消息中间件是基于队列与消息传递技术…

探索NFC技术在游戏玩具娱乐,医疗保健和穿戴设备领域的三大应用

NFC是与众不同的无线技术。这意味着它只能在两个设备相近时起作用。在其他用无线技术随机广播的方式以被接收时&#xff0c;NFC更重要的独特之处于其使用电源的方式。或者&#xff0c;更确切地说&#xff0c;它可以在不供电的环境下进行工作。它是一种非接触式智能卡技术的演进…

spring-security 过滤器

spring-security过滤器 版本信息过滤器配置过滤器配置相关类图过滤器加载过程创建 HttpSecurity Bean 对象创建过滤器 过滤器作用ExceptionTranslationFilter 自定义过滤器 本章介绍 spring-security 过滤器配置类 HttpSecurity&#xff0c;过滤器加载过程&#xff0c;自定义过…

Centos stream9 环境使用脚本部署LAMP,实现wordpress

本人将所需要的rpm包都下载完成之后&#xff0c;直接使用脚本的形式安装。 如果需要自己下载rpm包的话&#xff0c;请下载如下包 yum install -y libxml2-devel \ tar \ gcc \ expat-devel \ bzip2-devel \ pcre-devel \ openssl-devel \ perl-devel \ sqlite-devel \ libcur…

计算机服务器中了devos勒索病毒怎么办?Devos勒索病毒解密数据恢复

网络技术的不断发展与更新&#xff0c;为企业的生产运营提供了有利保障&#xff0c;企业的生产运营离不开数据支撑&#xff0c;通过企业数据可以综合调整发展运营方向&#xff0c;但网络是一把双刃剑&#xff0c;近期&#xff0c;云天数据恢复中心接到许多企业的求助&#xff0…

32.仿简道云公式函数实战-数学函数-MOD

1. MOD函数 返回两数相除的余数。 结果的符号与除数相同。 2. 函数用法 MOD(number, divisor) 3. 函数示例 返回两数相除的余数。 结果的符号与除数相同。 number: 必需。 要计算余数的被除数。 divisor: 必需。 除数。 4. 代码实战 首先我们在function包下创建math包…

Android 7.0以上charles无法抓取部分https包问题

首先保证配置一切正确 手机通过访问chls.pro/ssl下载.pem证书&#xff0c;如无法安装&#xff0c;在文件管理器中将后缀名改为.crt 在设置中安装该证书 Charles-Proxy - SSL Proxying Setting - Include 添加需要抓包的URL:443即可 以上基本配置结束后&#xff0c;看下代码 代…

w28DVWA-csrf实例

DVWA-csrf实例 low级别 修改密码&#xff1a;修改的密码通过get请求&#xff0c;暴露在url上。 写一个简单的html文件&#xff0c;里面伪装修改密码的文字&#xff0c;代码如下&#xff1a; <html><body><a href"http://dvwa:7001/vulnerabilities/csr…

苍穹外卖学习-----2024/02/21

1.新增员工 /*** 处理SQL异常* param sqlIntegrityConstraintViolationException* return*/ExceptionHandlerpublic Result exceptionHandler(SQLIntegrityConstraintViolationException sqlIntegrityConstraintViolationException){//String message sqlIntegrityConstraintV…