二叉树:删除二叉搜索树中的节点

news2024/10/5 21:17:32

删除二叉搜索树中的结点

一、题目描述

给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。

一般来说,删除节点可分为两个步骤:

  1. 首先找到需要删除的节点;
  2. 如果找到了,删除它。

实例:

![[Pasted image 20230202120050.png]]

二、解题思路

我们删除一个结点,首先要找到这个结点,然而,寻找这个结点,会有两种情况:

1.没到了 2.没找到

对于1,没找到,说明树不会被修改,将原来的树原封不动返回。
对于2,找到了,肯定返回的是最终删除了指定元素的树。

这里对2进行重点分析,这个题的思路和递归代码相结合的时候,有些地方很难理解,一会在代码解析部分会重点讲解,这里重点理解思路

那对于每一个结点,我们是怎么处理的呢?

对于每一个结点,我们要分成四种情况:

  1. 当前结点是NULL ,说明已经找完了,找到叶子结点的孩子了,还是没有,那就返回NULL就行了
  2. 当前结点的val是key,那就要删除这个结点了,但是,删除结点还要保证二叉搜索树的结构
    1. 当前结点左右子树都为空:直接删除这个结点,不用改变结构
    2. 当前结点左子树不为空,右子树为空:用左子树替代当前结点
    3. 当前结点左子树为空,右子树不为空:用右子树替代当前结点
    4. 当前结点左右子树都不为空
      1. 先将当前结点左子树,放在当前结点右子树的最左侧孩子的左子树位置上
      2. 用当前结点右子树代替当前结点
      3. 【为什么要这样操作?】
        1. 首先由于这是二叉搜索树,所以,当前结点左子树上所有元素都是小于当前结点右子树上最小元素的
        2. 当前结点右子树上最小元素就是当前结点右子树上最左侧的元素
        3. 所以,将左子树放在右子树最左侧结点的左子树位置是没有问题的
  3. 当前结点的val > key:说明目标key小于当前结点,所以要往当前结点左侧找
  4. 当前结点的val < key:说明目标key大于当前结点,所以要往当前结点的右侧找

上面就是对每个结点具体的操作步骤了。
那具体代码如何组织呢,看下面的解析:

三、代码解析

3.1 函数返回值和参数

这个题目是:删除二叉搜索树中的节点

那么如果删除了,我们期望的返回值就是,删除过指定节点之后的二叉树。
如果没删除,就返回一个空指针。

所以,这个函数的返回值是TreeNode*,二叉树节点指针类型。

参数,很明显,要传入这个树的根节点和目标值key

所以,函数返回值和参数如下

TreeNode* deleteNode(TreeNode* root, int key){
	//函数体
}

3.2 递归函数体

这里按照解题思路中的逻辑来处理代码

TreeNode* deleteNode(TreeNode* root, int key) {
		 //递归终止的条件:遍历完这个树,没有找到目标结点
        if (root == NULL) return root;
        
        //当遍历到的val == key时
        if (root->val == key) {
            //1.遍历到结点的左右子树都为空,
            //那直接删除这个结点,并向删除结点的父节点返回NULL,相当于是用NULl代替了删除结点
            if (root->left == NULL && root->right == NULL) {
                delete root;//删除结点
                return NULL;//向上一层返回NULL,代表用NULL代替了待删除结点
            }
            //2.遍历到结点左子树不为空,右子树为空,那用左子树来代替这个结点
            else if (root->right == NULL && root->left != NULL) {
                TreeNode* retNode = root->left;
                delete root;//删除结点
                return retNode;//向上一层返回刚才保存的左子树,
                //也可以理解为用左子树代替了待删除结点
            }
            //3.遍历到结点右子树不为空,左子树为空,那用右子树来代替这个结点
            else if (root->right != NULL && root->left == NULL) {
                TreeNode* retNode = root->right;
                delete root;//删除结点
                return retNode;//向上返回待删除结点右子树
            }
            //4.遍历到结点左右子树都不为空,
            //将左子树放在右子树的最左侧结点的左子树上,并用右子树代替待删除结点
            else {
                //找到右子树最左侧的结点
                TreeNode* cur = root->right;
                while (cur->left != NULL) {
                    cur = cur->left;
                }
                //将待删除结点的左子树放在右子树左侧结点的左子树上
                cur->left = root->left;
                //用tem暂存待删除结点,因为,一会要改变root的值
                TreeNode* tmp = root;//一会删除tmp就行了
                //用root的右子树替代root
                root = root->right;
                //删除原来的root
                delete tmp;
                //返回现在修改过后的root
                return root;
            }
        }

		//如果当前遍历结点大于key值,说明key在当前结点的左侧,往当前结案的左侧遍历
        if (root->val > key) root->left = deleteNode(root->left, key);
        //如果当前遍历结点小于key值,说明key在当前结点的右侧,王当前结点的右侧遍历
        if (root->val < key) root->right = deleteNode(root->right, key);

        return root;
}

这里用一个例子来展示一下代码是怎么运行的:

图一

给出树的根节点是root,目标结点key是3,也就是要删除3这个结点。
现在代码开始运行:
判断当前root是不是NULL => 不是,不用执行return NULL
判断当前root->val是不是key => 不是,不用执行删除修改代码
判断当前root->val是不是大于key => 是,执行递归代码。

图二

为了区分,我们将第一次调用递归函数传入的root称为root_0
将第二次调用递归函数传入的root称为root_1
如果所示,
第一次传入的root_0当前是整个树的根节点。
按照代码执行流程,判断了root_0的val是大于key的,所以执行了相应的递归函数
所以,第二次传入的root_1是根节点的左孩子。

那第二次递归函数做了哪些操作呢?

请添加图片描述

进入第二次调用函数传入root_1,那函数再次重头开始执行

判断当前roor_1是不是NULL => 不是,不执行return NULL
判断当前root_1val是不是key3 => 是,指向判断里面的代码
判断当前root_1的左右是不是都有孩子 = > 执行左右孩子都存在的代码
返回修改过后的root_1

注意看上图

root_1是第二次调用递归函数所得到的结果,这个结果赋值给了第一次调用递归函数时root_0的左子树, 那就相当于是将root_0的左子树修改成了删除后的样子。

最终返回root_0也就是返回整个树的根,代码执行完毕。

到这里就讲解完毕了,如果还是不明白,结合思路,代码,将这个例子自己理一遍思路。

对于开始接触递归的小伙伴,总是觉得思考起来很困难,很抽象。
就对于这个题来说,如果要找的key是1那应该怎么向上传值呢?

那按照同样的思路,最底下一层调用递归函数传入的参数会是2这个结点的左子树,也就是NULL
然后会将NULL赋值给2的左子树。继续往执行,会返回2这个结点,将2这个结点赋值给结点3的左子树
同样,向上返回3这个结点,将3这个结点返回给5的左子树。最后返回root。

所以递归函数,一层一层的,很像是宝塔。我们开始从它尖向下寻找,知道找到了目标,或者找到了最底下一层。
然后开始从最底下一层往上传值,下面一层返回的值,其实是上面一个层函数的组成部分,同样,上面一层函数返回的值又是上上面一层函数的组成部分。直到返回到塔尖。

我们在组织代码的时候,脑子里要有向下寻找的过程,更要有一层一层向上回溯的过程。

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

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

相关文章

2023牛客寒假算法基础集训营5(通过ABCDHIKL) I题有详解(F已补)

其他题待补中…… 链接&#xff1a;2023牛客寒假算法基础集训营5 简单题 A 小沙の好客&#xff08;贪心&#xff0c;前缀和&#xff0c;二分&#xff09; 题意思路 给定nnn个商品的价值&#xff0c;qqq次询问&#xff0c;每次询问k,xk, xk,x即价值不超过xxx的商品最多可以拿…

文件输入输出缓冲流IO综合练习——学生管理系统文件版

综合练习&#xff08;一&#xff09; 键盘录入3个学生信息(学号,姓名,年龄,居住城市)存入集合。然后遍历集合把每一个学生信息存入文本文件(每一个学生信息为一行&#xff0c;自己定义分割标记) 学生类&#xff1a; package com.itheima; /** 标准的学生类*/ public class St…

【数据结构 (3)】1.4 算法和算法分析

文章目录1. 算法的定义及特性算法的特性算法设计的要求2. 算法的时间复杂度分析算法时间复杂度的基本方法算法时间复杂度分析例题算法时间复杂度的计算3. 算法的空间复杂度1. 算法的定义及特性 算法的定义 对特定问题求解方法和步骤的一种描述&#xff0c;它是指令的有限序列…

WSL-Ubuntu 安装、移动

设置” 启用或者关闭Windows功能“&#xff0c;勾选如下选项。Hyper-v若存在灰色&#xff0c;可进入BIOS-Configuration&#xff0c;Intel Virtual Technology设置Enable重启2、安装Ubuntu3、移动。Ubuntu默认安装C盘。首先D盘创建移动的目录&#xff0c;例如&#xff1a;D:\Ub…

使用kubeadm搭建高可用k8s集群

使用kubeadm搭建高可用k8s集群方案选型高可用k8s集群部署准备工作服务器统一配置配置hostname打通ssh免密登录部署etcd集群step1 在master01上生成配置相关文件step2 每台服务器上启动etcd服务step3 检查etcd集群是否正常部署负载均衡 (haproxy keepalived)step1 下载haproxy与…

SAP 分析云 2023.012023.02 版新功能抢先看

大家新年好呀&#xff01;本年度的第一篇推文来啦~本文介绍了SAP 分析云2023.01&2023.02 版本的新功能。这些新功能已经在SAP 分析云FastTrack 客户的系统上线。对于 SAP 分析云季度发布周期 (QRC) 客户&#xff0c;此版本及其功能将作为 QRC 2023 年第1季度版本的一部分提…

[Android开发基础3] Activity的生命周期、创建与配置

文章目录 生命周期 概念 生命周期周期函数 创建Activity 方法一&#xff1a;编译器自动创建与配置 方法二&#xff1a;手动创建与配置 生命周期 概念 生命周期&#xff0c;顾名思义&#xff0c;就是当前的程序单元Activity从启动到销毁之间一系列所经过的状态。 生命周期周…

怎么画室内导航地图,室内地图绘制工具

现在很多楼宇建的越来越大&#xff0c;停车场、商场、展览馆、博物馆、交通枢纽等大型室内场景规模巨大、环境复杂&#xff0c;人们置身其中&#xff0c;一不小心就走错方向&#xff0c;从而多走很多弯路&#xff0c;费时费力。室内导航一直是导航场景的一大难题&#xff0c;如…

【redis6】第十四章(Redis集群)

问题 容量不够&#xff0c;redis如何进行扩容&#xff1f; 并发写操作&#xff0c;redis如何分摊&#xff1f; 另外&#xff0c;主从模式&#xff0c;薪火相传模式&#xff0c;主机宕机&#xff0c;导致ip地址发生变化&#xff0c;应用程序中配置需要修改对应的主机地址、端…

什么是Go语言?

本文首发自「慕课网」&#xff0c;想了解更多IT干货内容&#xff0c;程序员圈内热闻&#xff0c;欢迎关注&#xff01; 作者|慕课网精英讲师 Codey 1. Go 语言的出身 Go&#xff08;又称 golang&#xff09;是 Google 开发的一种静态强类型、编译型、并发型&#xff0c;并具…

OpenMMLab AI实战课笔记-第1节课

1. 第一节课&#xff08;课程链接&#xff09; 1.1 计算机视觉任务 计算机视觉主要实现以下目标&#xff1a; 分类目标检测分割&#xff1a;语义分割、实例分割 (对像素进行精确分类, 像素粒度或细粒度)关键点检测 1.2 OpenMMLab框架 框架选择&#xff1a;PyTorchOpenMML…

多级缓存案例说明

多级缓存案例说明1.安装MySQL1.1.准备目录1.2.运行命令1.3.修改配置1.4.重启2.导入SQL3.创建Demo工程3.1.分页查询商品3.2.新增商品3.3.修改商品3.4.修改库存3.5.删除商品3.6.根据id查询商品3.7.根据id查询库存3.8.启动4.创建商品查询页面4.1.运行nginx服务4.2.反向代理为了演示…

CSS网格教程:网格布局模块/网格容器/网格项目

目录 CSS 网格布局模块 网格布局 浏览器支持 网格元素 实例 Display 属性 实例 实例 网格列&#xff08;Grid Columns&#xff09; 网格行&#xff08;Grid Rows&#xff09; 网格间隙&#xff08;Grid Gaps&#xff09; 实例 实例 实例 实例 网格行&#xff0…

java基础面试题1

目录 Java语言有哪些特点 Java都有那些开发平台&#xff1f; Jdk和Jre和JVM的区别【重要】 面向对象和面向过程的区别 什么是数据结构&#xff1f;Java的数据结构有哪些&#xff1f; 1.数组&#xff1a; 2.队列 Queue 3.链表 Linked List 4.栈Stack 5.树Tree 什么是…

13薪|初级测试工程师

"众推职聘”以交付结果为宗旨的全流程化招聘服务平台&#xff01;今日招聘信息↓【工作内容】1、制定、编写软件测试方案与计划2、根据需求文档编写测试用例&#xff0c;组织测试用例评审3、按时完成软件测试工作任务&#xff0c;执行测试&#xff0c;跟踪缺陷状态&#x…

第十四章 集合(集合框架体系、List)

一、集合框架体系 &#xff08;1&#xff09;可以动态保存任意多个对象 &#xff08;2&#xff09;提供了一系列方便的操作对象的方法&#xff1a;add、remove、set、get等 集合框架体系&#xff1a; 二、Collection 1. Collection 接口常用方法 &#xff08;1&#xff09;add…

学习QCustomPlot【3】库结构

文章目录一、前言二、库结构三、图层3.1、坐标轴层一、前言 学习一个陌生的库&#xff0c;我们首先要明确它有什么用&#xff0c;可以结合库官方examples&#xff0c;学习怎么简单的用。 但是如果要对该库有一个全面的认识&#xff0c;还是需要了解它的开发思路和库结构。 例…

2、计算机视觉之图像分类算法基础(笔记)

什么是图像分类&#xff1f; 识别图像所表示内容的任务称为图像分类。我们可以对图像分类模型进行训练以识别各类图像。例如&#xff0c;您可以训练模型来识别表示三种不同类型动物的照片&#xff1a;兔子、仓鼠和狗。 下面几个神经网络重点关注准确率的问题 上图只是训练方式…

java—for结构

for循环语句1.1循环结构循环结构的组成&#xff1a;初始化语句条件判断语句循环体语句条件控制语句循环结构对应的语法&#xff1a;初始化语句条件判断语句循环体语句条件控制语句1.2for循环语句格式//格式 for (初始化语句;条件判断语句;条件控制语句){ 循环体语句; }执行流程…

记录每日LeetCode 环形链表II Java实现

题目描述&#xff1a; 给定一个链表的头节点 head &#xff0c;返回链表开始入环的第一个节点。 如果链表无环&#xff0c;则返回 null。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xf…