代码随想录算法训练营DAY22|C++二叉树Part.8|235.二叉搜索树的最近公共祖先、450.删除二叉搜索树中的结点

本文主要是介绍代码随想录算法训练营DAY22|C++二叉树Part.8|235.二叉搜索树的最近公共祖先、450.删除二叉搜索树中的结点,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 235.二叉搜索树的最近公共祖先
    • 思路
    • 伪代码实现
    • CPP代码
    • 迭代法的CPP代码
  • 701.二叉搜索树中的插入操作
    • 思路
    • 伪代码
      • 递归函数有返回值
      • 递归函数不要返回值
      • 迭代方法
    • CPP代码
      • 递归有返回值
      • 递归无返回值
      • 迭代
  • 450.删除二叉搜索树中的结点
    • 思路(分析五种情况)
      • 没找到删除的点
      • 删的点是叶子结点
      • 要删的结点左为空,右为空
      • 要删的结点左不为空,右为空
      • 要删的结点左右都不为空
    • 伪代码实现
    • CPP总体代码
    • 二叉搜索树的迭代法删除结点
    • 普通二叉树的删除方式

235.二叉搜索树的最近公共祖先

力扣题目链接

文章讲解:235.二叉搜索树的最近公共祖先

视频讲解:二叉搜索树找祖先就有点不一样了!| 235. 二叉搜索树的最近公共祖先

状态:跟上一章思路一样文章链接。但是本题中我们不关心遍历顺序,因为BST的特性已经为我们确定好了二叉搜素树的特性。

思路

这个题目如何利用二叉搜索树的特性呢

当我们在遍历根结点的时候,

如果发现根结点比p和q的数值都大的话,说明p和q一定在我们根结点的左子树。所以这时就向左遍历。

如果发现根结点比q和p的数值都小的话,说明目标结点一定在我们根结点的右子树。所以这时就要想右遍历


如果我们的根结点已经到了p和q之间了呢

其实已经说明了该结点就是p和q的最小公共祖先了,因为我们无论再向哪边遍历,都会错过q或者q的。(这是本题中我认为最重要的逻辑)

伪代码实现

  • 递归函数的参数和返回值:

    • 返回值就是我们的公共祖先
    • 传参就是当前结点和p、q
    TreeNode* traversal (cur, p, q){
    }
    
  • 终止条件:其实都不需要这个终止条件,因为题中说了p和q为不同结点且均存在于给定的二叉搜索树中。也就是说一定会找到公共祖先的,所以并不存在遇到空的情况。

  if (cur == NULL) return NULL;
  • 单层递归条件:这里我们根本不关注遍历顺序和中结点的处理逻辑,因为我们的搜索树已经帮我们规定好了搜索路径。
//左
if (cur->val > p->val && cur->val > q->val){	//当前数值比p大,比q大left = traversal(cur->left, p, q);	//说明我们要向左去搜索if (left != NULL) return left; //这里说明我们已经找到公共祖先了
}
//右
if(cur->val < p->val && cur->val < q->val){right = traversal(cur->right, p, q);if (right != NULL) return right;//如果right不为空,说明我们在右子树找到了想要的结果
}
//当前数值在p和q之间
return cur;

CPP代码

class Solution {
private:TreeNode* traversal(TreeNode* cur, TreeNode* p, TreeNode* q) {if (cur == NULL) return cur;// 中if (cur->val > p->val && cur->val > q->val) {   // 左TreeNode* left = traversal(cur->left, p, q);if (left != NULL) {return left;}}if (cur->val < p->val && cur->val < q->val) {   // 右TreeNode* right = traversal(cur->right, p, q);if (right != NULL) {return right;}}return cur;}
public:TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {return traversal(root, p, q);}
};
//精简后的代码
class Solution {
public:TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {if (root->val > p->val && root->val > q->val) {return lowestCommonAncestor(root->left, p, q);} else if (root->val < p->val && root->val < q->val) {return lowestCommonAncestor(root->right, p, q);} else return root;}
};

迭代法的CPP代码

因为搜索的有序性,所以迭代法也很简单

while(cur){ //只要当前结点不为空,我们就一直搜索if (cur->val > p->val && cur->val > q->val)cur = cur->left;if (cur->val < p->val && cur->val < q->val)cur = cur->right;return cur;
}

701.二叉搜索树中的插入操作

力扣题目链接

文章讲解:701.二叉搜索树中的插入操作

视频讲解:原来这么简单? | LeetCode:701.二叉搜索树中的插入操作

状态:我们不管怎么样,直接插到叶子结点,让新插入到元素成为叶子结点即可。那么本题的两个难点显而易见:

  • 怎么找到待插元素应该去的位置
  • 怎么找到叶子结点呢?

思路

其实就是:无论我们插入什么样的结点,总可以在二叉搜索树的叶子结点找到它的位置

为什么我们不改变二叉树的结构,硬在里面插一个呢?那这就把这个题目做复杂了。我们题目并没有这样的要求

伪代码

递归函数有返回值

  • 确定递归函数参数和返回值

    • 参数–根结点和插入的数值
    • 返回值–插入新结点之后,我们这个新二叉树的根结点。
    TreeNode* insert(root, val){}
    
  • 确定终止条件:如果我们的root等于空,说明我们已经找到插入结点的位置了

    • 其中的return node是本段代码的精髓,一定要领会
if (root == NULL){TreeNode* node = new TreeNode(val);return node;//把新插入的结点向上一层返回,因为我们一层层向下递归到叶子结点了,返回给之前的叶子
}
  • 单层递归逻辑
if (val < root->val) root->left = insert(root->left, val); //还记得之前我们返回了新插入的结点吗,就是把他的位置返回给他了
if (val > root->val)root->right = insert(root->right, val);//至此我们就完成了我们的数值在叶子结点对应的位置
return root;

递归函数不要返回值

  • 确定递归函数参数和返回值,这里我要不要返回值,也就是说知道插入的结点位置,直接让其父结点指向插入结点,结束递归
TreeNode* parent; //记录遍历结点的父结点
void traversal(TreeNode* cur, int val)
  • 确定终止条件:既然没有返回值,我们就需要记录上一个结点,遇到空结点了,就让parent左孩子或者右孩子指向新插入的结点。然后结束递归
if (cur == NULL){TreeNode* node = new TreeNode(val);if (parent->val > val) parent->right = node;else parent->left = node;return;
}
  • 确定单层递归逻辑
//让某结点一直跟在cur结点的屁股后面的常用方法
parent = cur;
if (cur->val > val) traversal(cur->left, val);
if (cur->val < val) traversal(cur->right, val);
return;

迭代方法

关于BST的迭代方法其实普遍都比较简单,因为二叉搜索树毕竟是有序的,遍历方向比较好控制。

迭代方法的基本逻辑就是:

  • 如果root为空,我们需要处理
if (root == nullptr){TreeNode* node = new TreeNode(val);return node;
}
  • 定义一个父结点parent,他是仅仅更在cur后面的结点,后续我们需要它来进行赋值操作
TreeNode* cur = root;
TreeNode* parent = root;
  • 通过迭代方法找到插入点的位置
while (cur != NULL){parent = cur;if (cur->val > val) cur = cur->left;if (cur->val > val) cur = cur->right;
}
  • 利用定义的parent进行赋值
//等我们跳出循环,parent就在插入位置的父结点位置
if (parent->val > val) parent->left = node;
else parent->right = node;
return root;

CPP代码

递归有返回值

class Solution {
public:TreeNode* insertIntoBST(TreeNode* root, int val) {if (root == NULL) {TreeNode* node = new TreeNode(val);return node;}if (root->val > val) root->left = insertIntoBST(root->left, val);if (root->val < val) root->right = insertIntoBST(root->right, val);return root;}
};

递归无返回值

class Solution {
private:TreeNode* parent;void traversal(TreeNode* cur, int val) {if (cur == NULL) {TreeNode* node = new TreeNode(val);if (val > parent->val) parent->right = node;else parent->left = node;return;}parent = cur;if (cur->val > val) traversal(cur->left, val);if (cur->val < val) traversal(cur->right, val);return;}public:TreeNode* insertIntoBST(TreeNode* root, int val) {parent = NULL;//把结点初始化一下子if (root == NULL) {root = new TreeNode(val);}traversal(root, val);return root;}
};

迭代

class Solution {
public:TreeNode* insertIntoBST(TreeNode* root, int val) {if (root == NULL) {TreeNode* node = new TreeNode(val);return node;}TreeNode* cur = root;TreeNode* parent = root; // 这个很重要,需要记录上一个节点,否则无法赋值新节点while (cur != NULL) {parent = cur;if (cur->val > val) cur = cur->left;else cur = cur->right;}TreeNode* node = new TreeNode(val);if (val < parent->val) parent->left = node;// 此时是用parent节点的进行赋值else parent->right = node;return root;}
};

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

力扣题目链接

文章讲解:450.删除二叉搜索树中的结点

视频讲解:调整二叉树的结构最难!| LeetCode:450.删除二叉搜索树中的节点

状态:由于删除二叉搜索树可能涉及到二叉搜索树结构的改变,所以一定要注意分情况讨论,详细的代码实现也一定要记住。

思路(分析五种情况)

  1. 首先要注意,二叉树是链式存储的,所以我们的删除不是真的删除,而是父结点指向新的孩子

  2. 本题相对于之前添加结点的操作,本题要难很多。因为删除结点我们必须要改变二叉树的结构

没找到删除的点

遍历到空结点直接返回

if (root == nullptr) return root;

删的点是叶子结点

左右孩子都为空,直接删除结点,返回NULL为根结点

if (root->left == nullptr && root->right == nullptr){//内存释放delete root;return nullptr;
}

要删的结点左为空,右为空

其左孩子为空,右孩子不为空,删除节点,右孩子补位 ,返回右孩子为根节点

else if (root->left == nullptr){auto retNode = root->left;delete root;return retNode;
}

要删的结点左不为空,右为空

其右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点

else if (root->right == nullptr){auto retNode = root->left;delete root;return reNode;
}

要删的结点左右都不为空

这里就讲究了,涉及到插入结点的操作,因为我们可以让待删结点的右孩子来代替删除位置,那么待删结点的左孩子就必须连根带叶得插入到右孩子,这里我们继续延续701.二叉搜索树中的插入操作中的思想,直接作为右孩子的叶子即可

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;	//释放结点内存return root;
}

伪代码实现

  • 确定递归函数的返回值和参数:返回值就是新结点的根结点;然后root是带删结点,key就是要删除的值
TreeNode* delete(root, key){}
//该函数就是leetCode提供的主函数
  • 确定递归的终止条件:

    • 在本题中,我们不是要遍历整颗二叉树才开始终止,其实只要找到了我们要删除的点就得删。那么既然我们找到了要删除的点了,所以删除逻辑也要写出来
    //没找到要删除的结点
    if (root == NULL) return NULL;
    if (root->val == key){if (root->left == NULL && root->right == NULL)return NULL;//这里的NULL return到哪,其实是返回到被删叶子结点的父结点了else if (root->left != NULL && root->right == NULL) return root->left;//让待删结点的左子树直接返回到待删结点的父结点那儿,完成待删结点的移除else if (root->left == NULL && root->right != NULL)return root->rightl;else{ //先找到待删结点最左侧的值代替位置cur = root->right;while(cur->left != NULL) cur = cur->left;//现在cur指向了右孩子的最左叶子cur->left = root->left; //完成待删结点的左子树连入右子树的叶子return root->right; //真正删除待删结点}
    }
    
  • 单层递归逻辑

//这里的root->left与上文的代码对应,就是接住被删结点的孩子。这里的逻辑非常重要!
if (key < root->val) root->left = delete(root->left, key);
if (key > root->val) root->right = delete(root->right, key);
return root;

CPP总体代码

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;}
};

二叉搜索树的迭代法删除结点

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;}
};

普通二叉树的删除方式

普通二叉树的删除方式就必须遍历整颗树,用交换值的操作来删除目标结点。

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

  • 第一次是和目标结点的右子树最左面结点交换。
  • 第二次直接被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;}
};

这篇关于代码随想录算法训练营DAY22|C++二叉树Part.8|235.二叉搜索树的最近公共祖先、450.删除二叉搜索树中的结点的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/905276

相关文章

C++右移运算符的一个小坑及解决

《C++右移运算符的一个小坑及解决》文章指出右移运算符处理负数时左侧补1导致死循环,与除法行为不同,强调需注意补码机制以正确统计二进制1的个数... 目录我遇到了这么一个www.chinasem.cn函数由此可以看到也很好理解总结我遇到了这么一个函数template<typename T>unsigned

C#实现千万数据秒级导入的代码

《C#实现千万数据秒级导入的代码》在实际开发中excel导入很常见,现代社会中很容易遇到大数据处理业务,所以本文我就给大家分享一下千万数据秒级导入怎么实现,文中有详细的代码示例供大家参考,需要的朋友可... 目录前言一、数据存储二、处理逻辑优化前代码处理逻辑优化后的代码总结前言在实际开发中excel导入很

SpringBoot+RustFS 实现文件切片极速上传的实例代码

《SpringBoot+RustFS实现文件切片极速上传的实例代码》本文介绍利用SpringBoot和RustFS构建高性能文件切片上传系统,实现大文件秒传、断点续传和分片上传等功能,具有一定的参考... 目录一、为什么选择 RustFS + SpringBoot?二、环境准备与部署2.1 安装 RustF

Python实现Excel批量样式修改器(附完整代码)

《Python实现Excel批量样式修改器(附完整代码)》这篇文章主要为大家详细介绍了如何使用Python实现一个Excel批量样式修改器,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一... 目录前言功能特性核心功能界面特性系统要求安装说明使用指南基本操作流程高级功能技术实现核心技术栈关键函

C++统计函数执行时间的最佳实践

《C++统计函数执行时间的最佳实践》在软件开发过程中,性能分析是优化程序的重要环节,了解函数的执行时间分布对于识别性能瓶颈至关重要,本文将分享一个C++函数执行时间统计工具,希望对大家有所帮助... 目录前言工具特性核心设计1. 数据结构设计2. 单例模式管理器3. RAII自动计时使用方法基本用法高级用法

Redis实现高效内存管理的示例代码

《Redis实现高效内存管理的示例代码》Redis内存管理是其核心功能之一,为了高效地利用内存,Redis采用了多种技术和策略,如优化的数据结构、内存分配策略、内存回收、数据压缩等,下面就来详细的介绍... 目录1. 内存分配策略jemalloc 的使用2. 数据压缩和编码ziplist示例代码3. 优化的

Python 基于http.server模块实现简单http服务的代码举例

《Python基于http.server模块实现简单http服务的代码举例》Pythonhttp.server模块通过继承BaseHTTPRequestHandler处理HTTP请求,使用Threa... 目录测试环境代码实现相关介绍模块简介类及相关函数简介参考链接测试环境win11专业版python

Python从Word文档中提取图片并生成PPT的操作代码

《Python从Word文档中提取图片并生成PPT的操作代码》在日常办公场景中,我们经常需要从Word文档中提取图片,并将这些图片整理到PowerPoint幻灯片中,手动完成这一任务既耗时又容易出错,... 目录引言背景与需求解决方案概述代码解析代码核心逻辑说明总结引言在日常办公场景中,我们经常需要从 W

深入解析C++ 中std::map内存管理

《深入解析C++中std::map内存管理》文章详解C++std::map内存管理,指出clear()仅删除元素可能不释放底层内存,建议用swap()与空map交换以彻底释放,针对指针类型需手动de... 目录1️、基本清空std::map2️、使用 swap 彻底释放内存3️、map 中存储指针类型的对象

使用Spring Cache本地缓存示例代码

《使用SpringCache本地缓存示例代码》缓存是提高应用程序性能的重要手段,通过将频繁访问的数据存储在内存中,可以减少数据库访问次数,从而加速数据读取,:本文主要介绍使用SpringCac... 目录一、Spring Cache简介核心特点:二、基础配置1. 添加依赖2. 启用缓存3. 缓存配置方案方案