C++力扣题目450--删除二叉搜索树中的节点

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

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

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

示例 1:

输入:root = [5,3,6,2,4,null,7], key = 3
输出:[5,4,6,2,null,null,7]
解释:给定需要删除的节点值是 3,所以我们首先找到 3 这个节点,然后删除它。
一个正确的答案是 [5,4,6,2,null,null,7], 如下图所示。
另一个正确答案是 [5,2,6,null,4,null,7]。

示例 2:

输入: root = [5,3,6,2,4,null,7], key = 0
输出: [5,3,6,2,4,null,7]
解释: 二叉树不包含值为 0 的节点

示例 3:

输入: root = [], key = 0
输出: []

提示:

  • 节点数的范围 [0, 104].
  • -105 <= Node.val <= 105
  • 节点值唯一
  • root 是合法的二叉搜索树
  • -105 <= key <= 105

思路

搜索树的节点删除要比节点增加复杂的多,有很多情况需要考虑,做好心理准备。

#递归

递归三部曲:

  • 确定递归函数参数以及返回值

说到递归函数的返回值,在二叉树:搜索树中的插入操作 (opens new window)中通过递归返回值来加入新节点, 这里也可以通过递归返回值删除节点。

代码如下:

TreeNode* deleteNode(TreeNode* root, int key)

  • 确定终止条件

遇到空返回,其实这也说明没找到删除的节点,遍历到空节点直接返回了

if (root == nullptr) return root;

  • 确定单层递归的逻辑

这里就把二叉搜索树中删除节点遇到的情况都搞清楚。

有以下五种情况:

  • 第一种情况:没找到删除的节点,遍历到空节点直接返回了
  • 找到删除的节点
    • 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
    • 第三种情况:删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点
    • 第四种情况:删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
    • 第五种情况:左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点。

第五种情况有点难以理解,看下面动画:

450.删除二叉搜索树中的节点

动画中的二叉搜索树中,删除元素7, 那么删除节点(元素7)的左孩子就是5,删除节点(元素7)的右子树的最左面节点是元素8。

将删除节点(元素7)的左孩子放到删除节点(元素7)的右子树的最左面节点(元素8)的左孩子上,就是把5为根节点的子树移到了8的左孩子的位置。

要删除的节点(元素7)的右孩子(元素9)为新的根节点。.

这样就完成删除元素7的逻辑,最好动手画一个图,尝试删除一个节点试试。

代码如下:

if (root->val == key) {// 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点// 第三种情况:其左孩子为空,右孩子不为空,删除节点,右孩子补位 ,返回右孩子为根节点if (root->left == nullptr) return root->right;// 第四种情况:其右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点else if (root->right == nullptr) return root->left;// 第五种情况:左右孩子节点都不为空,则将删除节点的左子树放到删除节点的右子树的最左面节点的左孩子的位置// 并返回删除节点右孩子为新的根节点。else {TreeNode* cur = root->right; // 找右子树最左面的节点while(cur->left != nullptr) {cur = cur->left;}cur->left = root->left; // 把要删除的节点(root)左子树放在cur的左孩子的位置TreeNode* tmp = root;   // 把root节点保存一下,下面来删除root = root->right;     // 返回旧root的右孩子作为新rootdelete tmp;             // 释放节点内存(这里不写也可以,但C++最好手动释放一下吧)return root;}
}

这里相当于把新的节点返回给上一层,上一层就要用 root->left 或者 root->right接住,代码如下:

if (root->val > key) root->left = deleteNode(root->left, key);
if (root->val < key) root->right = deleteNode(root->right, key);
return root;

整体代码如下:(注释中:情况1,2,3,4,5和上面分析严格对应)

class Solution {
public:TreeNode* deleteNode(TreeNode* root, int key) {if (root == nullptr) return root; // 第一种情况:没找到删除的节点,遍历到空节点直接返回了if (root->val == key) {// 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点if (root->left == nullptr && root->right == nullptr) {///! 内存释放delete root;return nullptr;}// 第三种情况:其左孩子为空,右孩子不为空,删除节点,右孩子补位 ,返回右孩子为根节点else if (root->left == nullptr) {auto retNode = root->right;///! 内存释放delete root;return retNode;}// 第四种情况:其右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点else if (root->right == nullptr) {auto retNode = root->left;///! 内存释放delete root;return retNode;}// 第五种情况:左右孩子节点都不为空,则将删除节点的左子树放到删除节点的右子树的最左面节点的左孩子的位置// 并返回删除节点右孩子为新的根节点。else {TreeNode* cur = root->right; // 找右子树最左面的节点while(cur->left != nullptr) {cur = cur->left;}cur->left = root->left; // 把要删除的节点(root)左子树放在cur的左孩子的位置TreeNode* tmp = root;   // 把root节点保存一下,下面来删除root = root->right;     // 返回旧root的右孩子作为新rootdelete tmp;             // 释放节点内存(这里不写也可以,但C++最好手动释放一下吧)return root;}}if (root->val > key) root->left = deleteNode(root->left, key);if (root->val < key) root->right = deleteNode(root->right, key);return root;}
};

#普通二叉树的删除方式

这里我在介绍一种通用的删除,普通二叉树的删除方式(没有使用搜索树的特性,遍历整棵树),用交换值的操作来删除目标节点。

代码中目标节点(要删除的节点)被操作了两次:

  • 第一次是和目标节点的右子树最左面节点交换。
  • 第二次直接被NULL覆盖了。

思路有点绕,感兴趣的同学可以画图自己理解一下。

代码如下:(关键部分已经注释)

class Solution {
public:TreeNode* deleteNode(TreeNode* root, int key) {if (root == nullptr) return root;if (root->val == key) {if (root->right == nullptr) { // 这里第二次操作目标值:最终删除的作用return root->left;}TreeNode *cur = root->right;while (cur->left) {cur = cur->left;}swap(root->val, cur->val); // 这里第一次操作目标值:交换目标值其右子树最左面节点。}root->left = deleteNode(root->left, key);root->right = deleteNode(root->right, key);return root;}
};

这个代码是简短一些,思路也巧妙,但是不太好想,实操性不强,推荐第一种写法!

#迭代法

删除节点的迭代法还是复杂一些的,但其本质我在递归法里都介绍了,最关键就是删除节点的操作(动画模拟的过程)

代码如下:

class Solution {
private:// 将目标节点(删除节点)的左子树放到 目标节点的右子树的最左面节点的左孩子位置上// 并返回目标节点右孩子为新的根节点// 是动画里模拟的过程TreeNode* deleteOneNode(TreeNode* target) {if (target == nullptr) return target;if (target->right == nullptr) return target->left;TreeNode* cur = target->right;while (cur->left) {cur = cur->left;}cur->left = target->left;return target->right;}
public:TreeNode* deleteNode(TreeNode* root, int key) {if (root == nullptr) return root;TreeNode* cur = root;TreeNode* pre = nullptr; // 记录cur的父节点,用来删除curwhile (cur) {if (cur->val == key) break;pre = cur;if (cur->val > key) cur = cur->left;else cur = cur->right;}if (pre == nullptr) { // 如果搜索树只有头结点return deleteOneNode(cur);}// pre 要知道是删左孩子还是右孩子if (pre->left && pre->left->val == key) {pre->left = deleteOneNode(cur);}if (pre->right && pre->right->val == key) {pre->right = deleteOneNode(cur);}return root;}
};


 

#总结

读完本篇,大家会发现二叉搜索树删除节点比增加节点复杂的多。

因为二叉搜索树添加节点只需要在叶子上添加就可以的,不涉及到结构的调整,而删除节点操作涉及到结构的调整

这里我们依然使用递归函数的返回值来完成把节点从二叉树中移除的操作。

这里最关键的逻辑就是第五种情况(删除一个左右孩子都不为空的节点),这种情况一定要想清楚

而且就算想清楚了,对应的代码也未必可以写出来,所以这道题目既考察思维逻辑,也考察代码能力

递归中我给出了两种写法,推荐大家学会第一种(利用搜索树的特性)就可以了,第二种递归写法其实是比较绕的。

最后我也给出了相应的迭代法,就是模拟递归法中的逻辑来删除节点,但需要一个pre记录cur的父节点,方便做删除操作。

迭代法其实不太容易写出来,所以如果是初学者的话,彻底掌握第一种递归写法就够了。

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

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

相关文章

Generalized Focal Loss论文个人理解

论文地址&#xff1a;Generalized Focal Loss: Towards Efficient Representation Learning for Dense Object Detection 论文解决问题 研究人员最近更加关注边界框的表示(representation)及其定位质量估计&#xff08;LQE&#xff0c;本论文中指的是IoU score&#xff09;&a…

将YOLO数据集转成COCO格式,单个文件夹转为单个json文件,例如.../images/train转为instance_train.json

写在前面 参考链接&#xff1a;objectdetection-tricks/tricks_4.py 相关视频教学&#xff1a;tricks_4 用于yolov5和v7中的yolo格式转换coco格式的脚本.(如何在v5和v7中输出ap_small,ap_middle,ap_large coco指标)还可以参考相关的VOC转COCO的方式&#xff1a;damo-yolo/voc2…

R语言【paleobioDB】——pbdb_reference():通过参数请求获得多条参考文献的基本信息

Package paleobioDB version 0.7.0 paleobioDB 包在2020年已经停止更新&#xff0c;该包依赖PBDB v1 API。 可以选择在Index of /src/contrib/Archive/paleobioDB (r-project.org)下载安装包后&#xff0c;执行本地安装。 Usage pbdb_references (...) Arguments 参数【...】…

MtimeMtimecmp

Mtime: 实时time计数器&#xff0c;可读可写&#xff1b;mtime必须按照一个固定的频率递增&#xff1b;如果count overflow了&#xff0c;则mtime的值需要卷绕&#xff1b;对于32/64的系统来说&#xff0c;mtime的值都是64bits的&#xff1b; 与mtime对应的&#xff0c;还有一…

【架构师成长之领域驱动开发】

架构师成长之路 1. 如何构建高质量应用&#xff1f;2. 三大设计原则&#xff1f;3.DDD妙招4. 最终的改造结果5.模型 项目中的“坏”味道 可维护性差&#xff1a;大量的第三方模块影响核心代码的稳定性可扩展性差&#xff1a;业务逻辑与数据存储相互依赖&#xff0c;无法复用可测…

项目中使用iframe引入html 解决路由错乱问题以及父子组件传值调用方法

iframe与外部之间传值 父组件 <iframeid"iframe"src"luckysheet/index.html"frameborder"0"scrolling"no"style"width: 100%; height: 60vh; border: 0"/>const frame document.getElementById(iframe);frame.onloa…

Python综合练习之图表

文章目录 文件目录如下图标效果timeline_bar_with_graphic.htmltable_base.html articles.jsonarticlesData.pyarticlesEchartsEntity.pyarticlesEntity.py Python学习了约一个月的时间&#xff0c;这是一篇综合练习的文章。主要做的内容是通过封装对象、实现抽象方法生成统计图…

不可预测的市场中,为何有人持续胜出?

为什么经济学家和证券分析师难以预测经济或股价走势&#xff0c;而少数投资大师却能几十年持续复利&#xff1f;这两个问题看似矛盾&#xff0c;既然无法预测&#xff0c;为何又能产生确定性的赚钱结果呢&#xff1f; 有人认为这是因为幸存者偏差。然而&#xff0c;三十年以上连…

优优嗨聚集团:债务逾期,如何应对与解决?

在现代社会&#xff0c;债务问题已成为越来越多人面临的难题。债务逾期不仅会给个人带来巨大的经济压力&#xff0c;还会影响个人信用记录&#xff0c;甚至可能引发法律纠纷。那么&#xff0c;当债务逾期时&#xff0c;我们应该如何应对与解决呢&#xff1f; 一、了解债务情况 …

14.STL 常用算法

14、STL常用算法 概述&#xff1a; 算法主要是由头文件 、、 组成 是所有STL头文件中最大的一个&#xff0c;范围涉及到比较、 交换、查找、遍历操作、复制、修改等等 体积很小&#xff0c;只包括几个在序列上面进行简单数学运算的模板函数 定义了一些模板类,用以声明函数对象…

1.3 面试经典150题 - 删除有序数组中的重复项

删除有序数组中的重复项 class Solution:def removeDuplicates(self, nums: List[int]) -> int:# 处理边界数据if not nums: return 0if len(nums) 1: return 1# 两个指针&#xff0c;一个记录当前有多少个不重复值的个数&#xff0c;一个记录最新遍历到的值count 1tmp …

C# ObjectArx 绘制表格并设置单元格合并

第一行默认是标题&#xff0c;可设置行【RowType】进行设置类型 Document doc Application.DocumentManager.MdiActiveDocument;using (Transaction tr doc.TransactionManager.StartOpenCloseTransaction()){BlockTable bt tr.GetObject(doc.Database.BlockTableId, OpenMo…

GZ075 云计算应用赛题第9套

2023年全国职业院校技能大赛&#xff08;高职组&#xff09; “云计算应用”赛项赛卷9 某企业根据自身业务需求&#xff0c;实施数字化转型&#xff0c;规划和建设数字化平台&#xff0c;平台聚焦“DevOps开发运维一体化”和“数据驱动产品开发”&#xff0c;拟采用开源OpenSt…

Python3 中常用字符串函数介绍

介绍 Python 中有几个与 字符串数据类型相关的内置函数。这些函数让我们能够轻松修改和操作字符串。我们可以将函数视为在代码元素上执行的操作。内置函数是在 Python 编程语言中定义的&#xff0c;并且可以随时供我们使用的函数。 在本教程中&#xff0c;我们将介绍在 Pytho…

导轨式信号隔离变送器比例阀门线性驱动器4-20mA/0-5V/0-10V转0-165mA/0-80mA/0-1A/0-2A/0-4A

主要特性 精度、线性度误差等级&#xff1a; 0.1、0.2、0.5 级4-20mA/0-5V/0-10V 等标准信号输入0~100mA/0~500mA/0~1A/0-5A 等电流信号输出0~1V(max 2A)/0~10V/0-24V(max 5A) 等电压信号输出信号输入/信号输出 3000VDC 隔离辅助电源&#xff1a;12V、15V 或 24V 直流单电源供…

【微服务】日志搜集elasticsearch+kibana+filebeat(单机)

日志搜集eskibanafilebeat&#xff08;单机&#xff09; 日志直接输出到es中&#xff0c;适用于日志量小的项目 基于7.17.16版本 主要配置在于filebeat&#xff0c; es kibana配置改动不大 环境部署 es kibana单机环境部署 略 解压即可 常见报错&#xff0c;百度即可。 记录…

Android 和 IOS 设备唯一ID如何选择

我们在做Android/IOS应用或游戏的时候,或许总会碰到要获取设备唯一id来标识或跟踪玩家。但随着系统安全性的提高,加强用户的隐私安全,获取设备唯一ID变得越来越困难了,下面我们来分析一下,看看哪些还可以满足我们需求,同时,如果您有关于获取设备唯一ID的新发现,欢迎留言…

【Vue监听属性详细介绍】

Vue监听属性详细介绍 1. 监听属性2. watch 属性3. 计算属性&#xff08;Computed Properties&#xff09;4. 侦听器&#xff08;Listeners&#xff09;方法5. .sync 修饰符6. 注意事项 1. 监听属性 在Vue中&#xff0c;监听属性是一个十分重要的特性&#xff0c;它允许你监听和…

揭秘铭文技术: 它如何改变你的数字世界?

在数字世界的演变过程中&#xff0c;区块链技术被广泛认为是一种革命性的创新&#xff0c;为众多行业和领域带来深远的影响。然而&#xff0c;区块链的潜力远不止于此。近年来&#xff0c;一种新兴应用——区块链铭文&#xff0c;正在逐渐引起人们的关注&#xff0c;成为数字世…

修改csdn的字体大小颜色

修改csdn的字体大小颜色 修改csdn的字体大小颜色 修改csdn的字体大小颜色一、设置字体与颜色格式二、修改字体格式三、修改字体颜色 一、设置字体与颜色格式 <font face"华文行楷" colorred size5>本字体是华文行楷&#xff0c;红色&#xff0c;5号大小</fo…