【数据结构(邓俊辉)学习笔记】二叉搜索树02——查找、插入和删除

文章目录

  • 1.概述
  • 2. 查找
    • 2.1 查找:算法
    • 2.2 查找:理解
    • 2.3 查找:实现
    • 2.4 查找:语义
  • 3. 插入
    • 3.1 插入:算法
    • 3.2 插入:实现
  • 4. 删除
    • 4.1 删除:框架
    • 4.2 删除:单分支
    • 4.3 删除:双分支

1.概述

之前介绍了二叉搜索树的概念和性质可以看到相对其他数据结构,二叉搜索树的特征首先体现在它独特的元素访问方式,也就是寻关键码访问(call by key)。
在这里插入图片描述

而所有这类访问无非都是通过三种主要的操作接口完成的,也就是静态查找search,以及动态插入和删除。

接下来讨论,在三个接口的背后分别应该采用什么算法?这些算法又该如何实现?显示的效率又将如何?

2. 查找

2.1 查找:算法

作为最基本操作接口首当其冲自然是查找。

在这里插入图片描述
那么如何在BST中查找特定的关键码呢?看上图中具体实例。

首先按照上节推荐方法验证这的确是一棵BST。
  ~  

  1. 每一次查找都是从根节点开始,假设要查找22,那么首先将目标关键码22与根节点处所对应的关键码16做一比较,不难看出,22相对16更大,那么这样一判断结果意味着什么?应该记得BST所处处具有的顺序性,是的,对于根节点而言它的左后代都不可能比它更大,而当前的查找目标却比它更大,这就意味着根节点的整棵左子树都可以被忽略掉,反过来如果树中的确存有目标关键码22,那么它也必然位于右子树中,因此接下来应相应转入这棵右子树,具体地,也就是将控制权交给这棵子树的树根25。
  2. 同样的,每当进入一棵子树,都要将目标关键码与当前的子树根节点进行比较,经过这次比较发现目标关键码22相对于子树根节点要更小,那么这意味着什么呢?同样地,根据顺序性节点25的所有右后代都不会比25小,都不可能等于目标关键码22,因此这些节点同样可以被忽略掉,反过来,如果目标关键码的确存在于这棵子树中,那么它也只能存在于其中的左子树中,接下来要顺藤摸瓜转入25的左子树,也就是将控制权交给这棵子树的根节点19。
  3. 在进入这棵子树之后,同样要将目标关键码22与这棵子树的树根19进行比较,并可以相应忽略掉它的所有左后代,虽然现在只有一个,而将查找范围缩小到这个根节点的右子树,最后一步,在相应进入到这棵右子树之后,同样要将这棵子树的根节点22与目标节点进行比较,终于返现二者相等,至此,这次查找也就相应地以成功而告终。

现在请留意观察在下方列出的这样一个序列,应该看出来这就是这棵BST的中序遍历序列。好的,现在假想地认为它就是一个向量,当然根据全局单调性,它必然是一个有序向量。以下将重新回顾下在这棵BST中刚才的搜索过程,不妨体味一下,这样一个搜索过程在下方的有序向量中对应于怎样的一个过程。

2.2 查找:理解

在这里插入图片描述

  1. 查找首先是对根节点进行一次比较,在有序向量中这对应于以某个轴点为基准进行一次比较,这次比较的结果既可以认为是摒弃掉整棵树的左子树,同时也可以等效地认为是摒弃掉整个向量的左侧子向量,当然也包括根节点以及轴点本身。
  2. 而接下来所做的决策也就是深入到BST的右子树中,这也可以等效地认为在有序向量中将搜索的范围有效地收缩到它的右子向量中。
  3. 以下过程只不过是刚才那一步骤在不同程度上的简单重放而已,每一次都会摒弃掉一棵子树或等效的一个子向量,并且深入到对应的另一侧子树或者说另一侧子向量,每次对新的根节点的比较也相当于在向量中对新的轴点进行比较,如此往复,直到最终收缩为仅含一个元素。

其实对于并不存在的关键码所做的查找,虽然会以失败告终,但整个过程相仿。

比如在这棵树中搜索23,依然会在16这个位置向右并且在25这个位置向左再在19这个位置向右,直到最终到22这个位置,依然试图向右,但是此时已经无路可走了。在这样一个情况下,也就是可以判断整个查找失败的时机与成功的查找相比无非增加了最后一次额外的判断而已,本质上没有任何区别。

不难看出,在这个算法的背后,也就是已经熟知的减而治之策略

具体来说,这里不过是通过一次又一次的比较逐步地缩小整个查找地范围,直至最终抵达平凡的情况。

通过方才对中序遍历序列的比照,也可以看出,整个过程也可以等效地视作是在仿效此前有序向量所行之有效的那种二分查找策略,那么这样一个策略以及查找的过程如何具体实现为代码呢?

2.3 查找:实现

以下给出BST查找算法的一种实现方式
在这里插入图片描述
可以看到对外的标准search接口实际上在内部是调用了名为searchIn的一个功能来实现的。

而作为一个更为基本的算法searchIn可以实现如下

具体来说,也就是在以v为根的当前子树中去查找某一个特定的关键码e。作为一个递归函数,首先给出了递归基,比如如果当前子树已经为空,就可以直接返回失败。否则,如果当前根节点与目标关键码正好相等,也就意味着查找成功,无论如何在这种情况下,都可以直接返回。
  ~  
而在一般情况下,也就是子树非空,同时也没有在子树的树根处命中,就继续递归地深入查找,而查找范围在经过一次比较之后,可以相应地确定为究竟是左子树还是右子树。

不难看出,这个算法每递归一次,当前节点v都会下降一层,因此这个算法在最坏情况下地递归深度也不会超过树的高度,这也是这个算法时间复杂度。

2.4 查找:语义

最后再来考察这里地hot参数,作为一个引用型的参数,它总是在这个算法被首次调用时,统一初始化为初值为NULL的内部变量_hot,因此在随后算法的执行过程中,对这个参数的修改,实质上也就是对内部变量_hot的修改,可以看到,对_hot的修改总是发生在每一次试图深入递归之前,具体来说,_hot会记下此前刚刚接受访问的那个非空的节点。

那么总体而言,_hot最终将会指向谁呢?它所对应的语义又是什么呢?
在这里插入图片描述
为了便于search接口更加简洁也更加准确地被其他的算法使用,有必要对它的接口语义予以明确地定义,这体现在两个方面。

  1. 首先这个接口的返回值在不同的情况下,分别对应什么?

这些语义可以通过上图来表示。实际上也无非就是查找成功以及查找失败这两种情况。
  ~  
我们知道,返回值本身也是引用类型的,这里约定在查找成功时返回的引用将指向一个真实存在的节点,而且这个节点恰好与目标关键码相等。而在查找失败时返回的引用将指向一个空节点,这里所说的空节点是指首先它的数值为NULL,但正因为它是一个引用,更确切地讲,此时所返回的那个数值为null的引用。实际上就是在整个查找的过程中最后一步所试图转入的那个数值为空的分支,它指向查找路径末端节点,当前仍不存在的某一个孩子。

  1. 其次经过这个算法的反复更新,_hot变量的最终取值在各种情况下有对应什么?

在查找成功时,它将指向命中返回节点的父亲。而在失败的时候,它将指向在整个查找过程中最后访问的一个真实存在的节点。

那么如何从语义上将这两种情况下的返回值以及_hot变量统一起来呢?

一种简明的方法就是引入哨兵。是的,在查找失败的时候,可以在末端节点试图转向的那个数值当前为空的引用处增加个假想的哨兵,而且可以进一步的假想着将它的关键码就设置为查找目标。当然不难验证,如果此时的确在这个位置增加一个这样的真实的节点,那么全树依然将是一棵合法的BST。而更有意义的是,在引入了这样一个假想的哨兵之后,就可以从语义上将两种情况统一起来。

具体来说也就是,无论成功与否,返回值总是等效的指向命中节点。尽管在失败情况下,这个命中节点只是假想的,而非真是存在的。而在如此引入了一个假想的哨兵之后,_hot也可以认为总是指向命中节点的父亲

那么基于这样一个语义明确的search接口插入、删除算法又当如何具体实现呢?

3. 插入

3.1 插入:算法

介绍下BST的插入算法。
在这里插入图片描述
具体来说,如果插入的关键码为e,需要首先调用search接口对它进行定位。既然已经约定禁止雷同元素存在,所以这里不妨假设e还不存在。请注意,此时的search尽管会以失败告终,但_hot变量将会指向查找路径的末端节点,而更重要的是作为返回值_hot的某一个当前仍为空的孩子恰好就是待插入节点应该接入的位置。

来看一个具体的实例

同样地,需要首先确认这是一棵BST,接下来如果需要插入40,就会在search的过程中经过一系列的比较,将查找的范围逐步地收缩,直至最终失败于46。请注意,在此时_hot指向的就是这个46,而此时search的返回值恰好就是46当前为NULL的那个左孩子引用,因此只需将待插入的关键码封装为一个节点,并且令46对应的那个孩子引用改为指向这个新节点,即可完成这次插入操作。插入之后就像上图右上方所示。
  ~  
进一步假设需要再插入55,同样地,经过一系列的搜索,最终失败于53。请注意,尽管此时53的左孩子是存在的,但是根据55和53的大小,在这个位置最终试图转向的是它的右孩子,只不过右孩子当前为空,所以search返回的恰好就是53这个当前为空的右孩子引用,而根据此前所做的统一语义约定,在此时_hot指向的恰好就是53,因此同样地,只需将待插入的55封装为节点,并且令53原先为空的那个孩子引用,改为指向这个新节点,也可以顺利地完成一次插入操作。新插入的节点以及此后所对应的BST就是上图右下方的图。

既然每个新节点所插入的位置在此前都是为空,所以在插入之后,它们也必然是叶子,这也是为什么它们都被画作方形而不是圆形。

好了,接下来的问题自然就是这样一个插入过程如何体现为具体代码呢?

3.2 插入:实现

BST的插入算法可以具体地实现如下
在这里插入图片描述

首先针对目标进行一次查找,而且按照约定,这里会忽略掉雷同的元素。也就是说,算法的确会终止于_hot,而返回一个名为x数值为空的引用,所以接下来只需创建一个关键码为e的新节点,并且将_hot作为它的父亲。没错,创建一个以e为关键码的新节点,并且以_hot作为父亲。同时还需要通过这个赋值语句令x不再为空,而改为指向这个新创建的节点(完成节点的双向连接),至此就完成了新创建的这个节点与原树的正确连接。当然接下来还需要更新这棵树的规模,以及新节点历代祖先的高度。

这个代码虽然简单,但是它可以处理各种边界情况。

那么如此实现的insert接口累计的时间复杂度是多少呢?答案是O(h)

不难看出,时间消耗主要集中在两个方面,也就是search以及updataHeightAbove,而且这二者在最坏情况下都不会超过整棵树的高度。因此,总体而言,算法的时间复杂度不过O(h)。

4. 删除

4.1 删除:框架

相对于节点插入操作,BST的节点删除操作要略微复杂一些,在此不妨先给出整个算法的主题框架
在这里插入图片描述

可以看到,首先也需要针对目标做一次查找定位,而且与插入操作正好对称,这里需要忽略的是元素尚不存在的情况,反过来,如果这个元素的确存在,就会调用一个名为removeAt的内部例程来真正地实施删除。当然接下来同样要更新全树的规模,同时更新相关祖先的高度。

就运行时间而言,如果暂时忽略掉removeAt接口,整个算法时间消耗依然主要集中于search以及updateHeightAbove这两个例程,所以同样地,这些时间累计而言也不会超过全树的高度O(h)。

那么接下来的问题就是removeAt可能需要处理哪几种情况?各种情况又当如何具体应对呢?

4.2 删除:单分支

先来考虑第一种情况,这也是相对而言更为简单的一种情况。
在这里插入图片描述
它的特征是经过查找所确定的那个目标节点x至多只有一个孩子,或者反过来,它至少有某棵子树是空的。

比如在这样一棵BST中上图中最上方图,如果试图删除其中的69,那么经过逐层地不断深入,最终的确可以定位到69,而且会发现69至多只有一个孩子,此时只需将这个节点大胆地删除掉,并且取而代之以它那个非空地孩子,在这里也就是64,不难验证,经过这样的处理之后整颗BST依然是一棵名副其实的BST。

因此接下来,不妨将这种情况下的处理方法整理并且实现为具体的代码,比如下面就是一种可能的实现方式。
在这里插入图片描述

在这个名为removeAt的算法中,要删除的对象是以x指示的那个节点,请注意,在此前经过搜索所确定的hot也会作为参数传入。接下来需要进行判断,如果x的左孩子并不存在,那么只需直接用它的右孩子来代替,新近提升一层的节点。对称地,如果当前节点没有右孩子,也可以直接用左孩子来代替。这也就是刚刚所说的第一种情况。
  ~  
需要特别指出的是其实这两种情况还涵盖了一种非常特殊的情况,也就是左右孩子可能同时为空,针对这种特殊情况,它依然是能够正确处理。

经过这样的替换操作之后,新进提升一层的节点还需要与它此前的祖父完成直接的连接,这也是以下这两句所完成的任务。至此,情况一的处理已经完全结束。

当然接下来比较棘手的是互补的那种情况,也就是尽管删除的目标存在但是它的左右孩子同时存在,情况一的策略在这种情况下无法直接套用,那么面对这种情况,又当如何处置呢?

4.3 删除:双分支

在这里需要再次使用计算机科学中的法宝,也就是化繁为简,具体来说,要将比较棘手的第二种情况有效地转化为第一种情况。
在这里插入图片描述
以上左图中的一棵BST为例,假设需要删除节点36,可以看到这个节点的确是比较棘手的情况,因为它的左右后代都存在,此时不妨找到它的直接后继。

应该记得在实现二叉树的时候,对于BinNode类型曾经实现一个名为succ接口。应该记得它的功能与语义,没错,就是返回当前节点在中序遍历下的直接后继。
  ~  
具体来说,也就是在全树中寻找不小于当前节点的最小节点,回顾当时的succ算法,在当前节点拥有右后代情况下算法将首先进入到它所对应的右子树,然后在右子树中沿着左侧分支不断地下行,直到最终不能下行,而整个过程最终所抵达地节点就应该是当前节点地直接后继。

对于这个例子而言36地直接后继就是40,那么接下来可以用当前节点36与它的直接后继40互相兑换,比如对这个例子而言,兑换后的状态就是上中图。

可能会有点担心,因为此时的这棵树在36位置违反了顺序性,它已经不再是一棵BST。是的,担心非常有道理,但是其实大可不必,因为这样一个状态只是一个瞬态,马上就会使它成为一棵BST,而且能够将目标节点删除。是的,此时已经可以着手对目标节点实施删除了,难道不是吗?

稍加观察不难发现,此时的局部(36,46)已经转化为此前的第一种情况,也就是说待删除36至多只有一个孩子,更确切地讲它只有右孩子,为什么它没有左孩子呢?因为作为此前节点地直接后继,它必然是某条左侧分支地末端,自然没有左孩子喽。既然如此,可以直接沿用此前第一种情况的处理手法,也就是用待删除节点36的右孩子46去顶替36,可以得到上右图。

此时,不难验证这棵树已经恢复成一棵不择不扣的BST。

需要特意说明的是,在此后还需要令内部_hot变量指向刚刚被实际删除的节点的父亲,并且从53开始不断地向上追溯历代祖先,因为这些祖先的高度有可能因为刚才后代36的删除而发生变化。

那么同样地,这样一颗化繁为简并且顺利处理的过程如何描述并且实现为具体代码呢?
在这里插入图片描述
还是回到刚刚尚未完成的removeAt算法,来补充对第二种情况的处理方法。

按照刚才的分析,只需要找出当前节点的直接后继,并且令这两个节点数据互换,从而等效地将待删除节点转移至新的位置,而且这个位置至多只有一个分支,当然这个分支只可能是右孩子,要将待删除节点顶替为右孩子,只需在这个右孩子以及它此前的祖父之间正确地完成一次双向连接。

  • 复杂度
    好了,那么作为remove算法地有机部分,这个removeAt算法,它的时间复杂度又是多少呢?是否会超过remove原有的O(h)复杂度呢?

幸好不会,原因在于removeAt本身并不包含任何循环,而其中唯一可能引起复杂度的无非是在第二种情况下对succ()接口的调用,而按照此前的实现方法以及分析结论,这个接口所需要的时间,这个接口所需要的时间也不会超过全树的高度O(h)。

至此,可以得出结论BST的删除操作与插入操作一样,在最坏情况下所需要的时间不会操作全树当时的高度h

那么这个结论是好,还是不足够好呢?后面将针对这个问题做进一步的探讨。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MySQL----redo log重做日志原理及流程

重做日志 redo log:重做日志,用于记录事务操作的变化,确保事务的持久性。redo log是在事务开始后就开始记录,不管事务是否提交都会记录下来,在异常发生时(如数据持久化过程中掉电),…

笔记-python里面的xlrd模块详解

那我就一下面积个问题对xlrd模块进行学习一下: 1.什么是xlrd模块? 2.为什么使用xlrd模块? 3.怎样使用xlrd模块? 1.什么是xlrd模块? ♦python操作excel主要用到xlrd和xlwt这两个库,即xlrd是读excel&…

成都百洲文化传媒有限公司助力商家扬帆远航

在数字经济的浪潮中,电商行业如日中天,成都百洲文化传媒有限公司正是这一领域的佼佼者。作为一家专注于电商服务的传媒公司,百洲文化以其专业的服务、创新的理念和卓越的成果,在业内树立了良好的口碑,成为众多商家信赖…

大数据实训室建设可行性报告

一、建设大数据实训室的背景与意义 随着信息技术的飞速发展,大数据已成为推动社会进步和经济发展的重要力量。中高职院校作为技能型人才培养的摇篮,承担着为社会输送大数据领域高素质、高技能人才的重要任务。因此,建设大数据实训室&#xf…

OKR:2024年目标和关键成果常见问题

什么是目标和关键结果(OKR)? 目标和关键结果(#OKR#)是一种由结果驱动的目标制定方法。在企业中,OKR经常被用来指导基于结果的成功。使用结果而不是任务作为驱动力,OKRs 鼓励通过度量指标对实现成…

Android 查询及获取应用程序 Package 及 Acticity 名称的方法

一、通过命令查询和获取应用的Package及Acticity (一)通过命令查询应用包名及安装信息 以下是常用命令: 命令形式作用adb shell pm list packages查询系统中所有应用的包名adb shell pm list packages -s查询系统应用包名adb shell pm lis…

onnx进阶算子优化

一、定义 如何保证pytorch 模型顺利转为onnx. 前言pytorch 算子是如何与onnx 算子对齐的?Asinh 算子出现于第 9 个 ONNX 算子集。PyTorch 在 9 号版本的符号表文件中是怎样支持这个算子的?BitShift 算子出现于第11个 ONNX 算子集。PyTorch 在 11 号版本…

Harbor本地仓库搭建004_Harbor配置管理功能_分布式分发功能_仓库管理_用户管理_垃圾清理_审查服务_项目定额---分布式云原生部署架构搭建00

然后我们再看一下配置管理,这里主要有个认证模式 这里我们是数据库,其实就是我们安装的postgresql 可以看到还有LDAP对吧,这个其实就是自己公司如果有 LDAP服务器,那么可以对接过来,那么,这个时候 再登录harbor的时候,就可以直接使用公司的,LDAP来管理,所有的用户了,其实就是…

电脑桌面图标大小怎么调整?多种方法图文教程【全】

随着数字化生活的深入,电脑桌面图标的大小调整成为了我们日常使用中经常需要面对的问题。无论是为了更清晰地查看文件内容,还是为了美化桌面布局,掌握调整图标大小的方法都显得尤为重要。电脑桌面图标大小怎么调整?本文将为您提供…

为什么要选择华为 HCIE-Security 课程?

2020 年我国网络安全市场规模达到 680 亿元,同比增长 25%。随着对网络安全的愈加重视及布局,市场规模将持续扩大。 近年来,随着“云大物工移智”等新兴技术的快速发展和普及应用,数字化已经融入社会经济生活的方方面面&#xff0c…

Golang 依赖注入库Wire应用案例

文章目录 简介Github指南安装案例wire.NewSetwire.Buildwire.Bindwire.Structwire.Valuewire.InterfaceValue 简介 Go语言的依赖注入库Wire是由Google提供的一个代码生成工具,用于简化和自动化依赖注入过程。Wire主要通过生成代码来处理依赖关系,而不是…

java-SpringBoot执行定时任务-任务调度-@EnableScheduling和@Scheduled

文章目录 java借助springBoot框架,执行定时任务0. 项目地址1. 需求分析2、新建springBoot项目3. 编写定时任务3.1 开启调度任务3.2 编写定时任务方法 java借助springBoot框架,执行定时任务 0. 项目地址 https://github.com/OrangeHza/JavaDemo 1. 需求…

揭示西周与汉唐时期的纺织工艺

在中国新疆这片充满神秘色彩的土地上,每一次的考古发掘都仿佛是对历史的一次深情回望,揭示出中华民族悠久而灿烂的文明史。其中,新疆出土的西周和汉唐时期的织物,更是以其精美绝伦的工艺和独特的审美风格,让我们对古代…

加速Python库安装:一键切换pip源,提升下载速度与成功率

pip换源 一、为什么要换源二、如何换源1. 临时换源2. 永久换源Windows系统Linux和macOS系统 3. 使用镜像站工具 三、常见的国内源四、注意事项五、总结 在Python开发中,我们经常需要使用pip来安装各种库。然而,由于网络环境的原因,直接使用pi…

《山西教育》教学版是什么级别的刊物?

《山西教育》教学版是什么级别的刊物? 《山西教育(教学版)》创刊于1956年,是由山西教育报刊社主办的教学刊物。山西省一级期刊,是“宣传教育政策,关注教育热点,传播先进经验,提供教改资讯”的权威性期刊&a…

C语言入门系列:判断和循环常踩的5个坑

文章目录 1. if代码块不带大括号问题描述示例与分析解决办法 2. if条件和大括号之间加了一个分号问题描述示例与分析解决办法 3. 使用号判断相等问题描述示例与分析解决办法 4. while循环的无限循环问题描述示例与分析解决办法 5. for循环中的off-by-one错误问题描述示例与分析…

Matlab只选取自己需要的数据画图

在Matlab作图的时候,经常会在同一个坐标系中作很多数据的图,如下图所示: 这就会导致不同数据所作的线会重叠在一起,不利于数据分析。如果只想对比几个数据的趋势,直接修改代码太过麻烦,可通过Matlab的绘图…

Python酷库之旅-比翼双飞情侣库(15)

目录 一、xlrd库的由来 二、xlrd库优缺点 1、优点 1-1、支持多种Excel文件格式 1-2、高效性 1-3、开源性 1-4、简单易用 1-5、良好的兼容性 2、缺点 2-1、对.xlsx格式支持有限 2-2、功能相对单一 2-3、更新和维护频率低 2-4、依赖外部资源 三、xlrd库的版本说明 …

开源新纪元:ChatTTS——引领对话式文本转语音的新潮流

✨作者主页: Mr.Zwq✔️个人简介:一个正在努力学技术的Python领域创作者,擅长爬虫,逆向,全栈方向,专注基础和实战分享,欢迎咨询! 您的点赞、关注、收藏、评论,是对我最大…

帕金森病患者应对腿部无力的方法

帕金森病是一种慢性神经系统退化性疾病,主要影响运动系统,导致运动功能障碍。患者常见的症状包括肌肉僵硬、运动迟缓、静止性震颤和姿势不稳。这些症状可能会导致患者在行走时感到腿软无力,尤其是在起步或转弯时更为明显。 帕金森病患者在日常…