DAY23:二叉树(十三)二叉树的最近公共祖先+二叉搜索树的最近公共祖先

news2024/11/25 17:34:20

文章目录

    • 236.二叉树的最近公共祖先
      • 思路
      • 完整版
        • 后序遍历的进一步理解
        • 为什么左为空右不为空的时候return right
        • 这个逻辑是否包含p/q本身就是公共祖先的情况
    • 235.二叉搜索树的最近公共祖先
      • 思路
        • 关于遍历顺序
      • 递归法
        • 最开始的写法
        • debug测试
        • 修改版
      • 迭代法
        • 最开始的写法
        • 为什么最开始这种写法不行?
        • 修改版

236.二叉树的最近公共祖先

  • 一定要仔细看提示,二叉树数值不重复,意味着后序遍历不会存在两边找到了同个元素的情况
  • 本题需要进一步理解后序遍历,可以认为后序遍历在"深入"到每个子树的最深层之后,才开始"回溯"并访问节点在某种意义上,这可以被视为从下往上的遍历方式但需要注意的是,它并不是简单地按照树的层级从下往上进行遍历的

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。

在这里插入图片描述
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。

在这里插入图片描述
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出:5
解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。

示例 3:
输入:root = [1,2], p = 1, q = 2
输出:1

在这里插入图片描述

思路

找最近公共祖先,也就是已知两个节点,从下往上去找这两个节点的最近公共祖先。

我们遍历二叉树的时候,不能从下往上去遍历。但是我们的处理顺序是可以从下往上处理的回溯的过程,实际上就是从底往上处理的过程。

针对某一个节点,左子树出现了p或者右子树出现了q,就把信息向上返回。实质上就是后序遍历的处理过程。

后序遍历(左右中)就是天然的回溯过程,可以根据左右子树的返回值,来处理中节点的逻辑

思路就是后序遍历,遇到p就返回,遇到q就返回,当某个节点左右子树返回值都不为空的时候,这个节点就是最近的公共祖先。

  • 注意,因为本题题目提示中说了二叉树数值不重复,所以可以不用考虑两边找到了p/q同个元素的情况

完整版

  • 注意p或者q其中一个本来就是公共祖先的情况。但是这种情况其实和后序遍历处理逻辑是重合的
  • 遇到了p往上返回的时候,要注意这个题目并不是要求返回bool类型,而是要求返回最近公共祖先节点!因此,我们往上返回,应该返回的是root本身
//后序遍历,左子树返回值不为空说明有p或q,右子树同理
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        //终止条件
        if(root==nullptr){
            return nullptr;
        }
        //后序,左右中
        //左
        TreeNode* left = lowestCommonAncestor(root->left,p,q);
        //右
        TreeNode* right = lowestCommonAncestor(root->right,p,q);
        //中,如果遇到了p/q就返回root,这道题目要求是root
        if(root==p||root==q){
            return root;
        }
        //如果都返回了就是最近公共祖先
        if(left!=nullptr&&right!=nullptr){
            return root;
        }
        //注意此处的逻辑!如果right存在left不存在,说明右子树里面有p/q,需要返回right,而不是root!
        if(left==nullptr&&right!=nullptr){
            return right;
        }
        if(left!=nullptr&&right==nullptr){
            return left;
        }
        //如果以上全部不满足,左是空右也是空
        return nullptr;
        
    }
};

后序遍历的进一步理解

在这里插入图片描述
例如这个例子,后序遍历(Post-Order Traversal)的总体顺序是:(10的子树) -> (4的子树) -> 8

对于给定的二叉树,我们可以按照后序遍历的规则列出以下结果:

  • 对于子树10, 1, 7, 6, 5,后序遍历的顺序是:1 -> 6 -> 5 -> 7 -> 10.
  • 对于子树4, 15, 20,后序遍历的顺序是:15 -> 20 -> 4.
  • 对于整个二叉树,后序遍历的顺序是:(10的子树) -> (4的子树) -> 8

所以,这棵树的后序遍历结果是:1, 6, 5, 7, 10, 15, 20, 4, 8。

后序遍历并非从下往上遍历,而是首先遍历左孩子节点,然后遍历右孩子节点,最后遍历根节点。这个过程在每个子树中都会被重复。

因此,可以认为后序遍历在"深入"到每个子树的最深层之后,才开始"回溯"并访问节点。在某种意义上,这可以被视为从下往上的遍历方式,但需要注意的是,它并不是简单地按照树的层级从下往上进行遍历的

这棵树的前序和中序也补充一下:

前序遍历:

  • 对于子树10, 1, 7, 6, 5,前序遍历的顺序是:10 -> 1 -> 7 -> 6 -> 5。
  • 对于子树4, 15, 20,前序遍历的顺序是:4 -> 15 -> 20。
  • 对于整个二叉树,前序遍历的顺序是:8 -> (10的子树) -> (4的子树)

所以,这棵树的前序遍历结果是:8, 10, 1, 7, 6, 5, 4, 15, 20。

中序遍历:

  • 对于子树10, 1, 7, 6, 5,中序遍历的顺序是:1 -> 10 -> 6 -> 7 -> 5。
  • 对于子树4, 15, 20,中序遍历的顺序是:15 -> 4 -> 20。
  • 对于整个二叉树,中序遍历的顺序是:(10的子树) -> 8 -> (4的子树)

所以,这棵树的中序遍历结果是:1, 10, 6, 7, 5, 8, 15, 4, 20。

为什么左为空右不为空的时候return right

如果left为空,而right不为空,说明只有右子树包含了这两个指定节点之一或者右子树包含了这两个节点的最近公共祖先,所以返回right。反之,如果right为空,而left不为空,说明只有左子树包含了这两个指定节点之一或者左子树包含了这两个节点的最近公共祖先,所以返回left。

返回的结果最终都会向上层节点传递,直到找到最近公共祖先。

这个算法基于后序遍历的原因是,我们需要检查一个节点的左右子树以确定该节点是否是两个指定节点的公共祖先,所以必须等到遍历了该节点的左右子树之后才能做出决定,这符合后序遍历的顺序:左子树 -> 右子树 -> 根节点。也和后序遍历进一步理解的本质相同,也就是先深入左右子树进行遍历,再"回溯"回根节点。

这个逻辑是否包含p/q本身就是公共祖先的情况

如果p/q本身就是公共祖先,例如下图的情况

在这里插入图片描述
如果是图上情况,左子树不是空的,右子树是空,返回到7的时候,由于if(rootp||rootq)会判断是不是本身和p/q相等,因此此时直接返回了7,其余部分又没有p/q,因此直接一路向上返回。

这个逻辑的核心就在于,遇到了p/q直接把当前和p/q相等或满足祖先条件的节点向上返回,并且当遇到左右有一个返回值的情况,继续保留左边的返回值。但还是会优先返回和p/q相等的节点,包含了p/q本身是公共祖先的情况。

  • 如果涉及到,中要根据左和右的结果来判断,一定是后序遍历。
  • 将结果一层一层返回上去,涉及这个”回溯“的过程,一定是后序遍历

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

  • 本题一定要注意思路的理解, 如果root的值在[p,q]之间,那么root一定是p和q的最近公共祖先

给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5]
在这里插入图片描述
示例 1:

输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
输出: 6 
解释: 节点 2 和节点 8 的最近公共祖先是 6

示例 2:

输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
输出: 2
解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。

说明:

所有节点的值都是唯一的。
p、q 为不同节点且均存在于给定的二叉搜索树中。

思路

本题要利用二叉搜索树的特性,来找最近的公共祖先。

因为二叉搜索树是有序树,且顺序为左子树<根节点<右子树

  • 如果根节点root大于p和q的值,说明公共祖先一定在左子树里。
  • 根节点root小于p和q的值,说明公共祖先一定在右子树里。
  • root的值在p和q之间,说明当前节点就是p和q的公共祖先
    • 节点值在[p,q]之间,说明p一定在节点左子树里,q一定在节点右子树里。
    • 在这种情况下,无论是向左遍历还是向右遍历,总会错过p/q其中的一个!只有当前节点,才能连接p和q!

因此,本题最重要的一点就是,如果节点root的值在p和q之间,那么节点root就是p和q的最近公共祖先

可以画二叉树模拟一下。如果p/q分别在节点左子树和右子树里面,那么不管向哪个方向遍历,都会错过一个,不再是公共祖先了。也就是说后面不可能再有公共祖先了。

关于遍历顺序

这道题并不需要单独考虑遍历顺序的问题,因为树本身就是有序的,不需要处理中间节点所以没有中间节点的逻辑,只要有一个左和一个右就可以了!

而且本题并不涉及到单调递增的问题,不是一定要中序遍历。并没有中间节点逻辑。

递归法

最开始的写法

  • 这种写法需要加很多if,代码比较冗余,debug的情况比较多
  • 二叉树因为建立的时候比较麻烦,在IDE里debug不太方便,我们可以采用直接在力扣里面打印输出的方式进行调试
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        //搜索树的有序性,按序排列
        if(root==nullptr){
            return nullptr;
        }
        TreeNode* right = nullptr;
        TreeNode* left = nullptr;

        //如果root小于p q 一定在右子树里
        if(root->val<p->val&&root->val<q->val){
            right = lowestCommonAncestor(root->right,p,q);
            //cout<< righ->val <<endl;
        } 
        //如果root大于p q 一定在左子树里
        if(root->val>p->val&&root->val>q->val){
            left = lowestCommonAncestor(root->left,p,q);
            //cout<< left->val <<endl;
        }
        if(root->val>p->val&&root->val<q->val){
            return root;
        }
        if(root->val>q->val&&root->val<p->val){
            cout<< root->val <<endl;
            return root;
        }
        if(root->val==p->val||root->val==q->val){
            //cout<< root->val <<endl;
            return root;
        }

       
        if(right!=nullptr&&left==nullptr){
            return right;
        }
        if(left!=nullptr&&right==nullptr){
            return left;
        }
        return nullptr;
    }
};

debug测试

这种较大的二叉树比较难建,直接在力扣里面打印想看的中间结果就行
在这里插入图片描述

修改版

因为p和q的大小关系,原题目是没有说的,所以我们最好把在PQ之间的情况放在else里面

  • 核心点在于节点数值只要不为空,只有三种情况:小于p q,大于p q,其他情况全部都是我们要找的公共祖先
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        //这道题目并不需要处理中间节点,有左右就可以了
        if(root==nullptr){
            return nullptr;
        }
        //左右
        if(root->val<p->val&&root->val<q->val){
            //也可以写在if里面,写里面的话直接在里面返回
            TreeNode* right = lowestCommonAncestor(root->right,p,q);
            if(right!=nullptr){
                return right;
            }
        }
        if(root->val>p->val&&root->val>q->val){
            TreeNode* left = lowestCommonAncestor(root->left,p,q);
            if(left!=nullptr){
                return left;
            }
        }
        //夹在中间或者相等,这就是其余所有情况
        //直接return 不要写在else里面否则会被判定没有返回值
        return root;
    }
};

迭代法

最开始的写法

class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        //都大于的时候向左遍历
        while(root->val > p->val && root->val > q->val){
            root = root->left;
        }
        //都小于的时候向右遍历
        while(root->val < p->val && root->val < q->val){
            root = root->right;
        }
        //找到最近公共祖先
        return root;
    }
};

为什么最开始这种写法不行?

写法在一个很长的用例上发生了报错

在这里插入图片描述
这是因为整体写法思路错了,不能先一直往左找,再一直往右找,每走一层都要重新判断往哪里走,因为可能先左后右再左。如果是上面的while写法,就是左边一直遍历左子树的左节点,但是错过了左子树的右节点!每一层都需要重新的判断去左边还是右边。

修改版

  • 注意,这里可以写while(1),因为while里面是什么并不重要,总会跳出去
  • 最后还要写返回值是因为函数必须有一个不在if语句内的返回值,其实这里的返回值没有意义,上面一定会返回
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        //这里可以写while(1),因为while里面是什么并不重要,总会跳出去!
        //while(root!=nullptr)
        while(1)
        {
            if(root->val > p->val && root->val > q->val)
                root = root->left;
            else if(root->val < p->val && root->val < q->val)
                root = root->right;
            else
                return root;
        }
        //跳出while之后的返回值,其实这里随便写就行因为while里面一定会return
        return nullptr;
    }
};

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

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

相关文章

chatgpt赋能python:Python收费介绍

Python收费介绍 什么是Python? Python是一种高级的、解释性、面向对象、纯粹的动态语言&#xff0c;多用于快速应用程序开发、脚本编写、系统管理任务等。它有一个简单直观优美的语法&#xff0c;非常容易学习。 Python的收费形式 Python语言本身是免费的&#xff0c;任何…

chatgpt赋能python:Python如何操作Word文档

Python如何操作Word文档 简介 Python是一种高级编程语言&#xff0c;具有易于学习和使用、高效、可移植性强等优点。相信许多Python开发者都遇到过需要使用Python操作Word文档的情况。本文旨在介绍如何使用Python操作Word文档&#xff0c;使开发者能够方便地实现自己的需求。…

chatgpt赋能python:Python怎么改为中文?

Python怎么改为中文&#xff1f; Python是一种高级编程语言&#xff0c;具有易读性、简单性和可扩展性的特点。它广泛应用于Web开发、数据分析、人工智能等领域。如何将Python改为中文&#xff1f;下面将为您详细介绍。 为什么要将Python改为中文&#xff1f; Python的英文是由…

[读论文][谷歌-12s生成] Speed is all your need

论文简要总结 刚读了下speed is all you need这个论文, https://arxiv.org/pdf/2304.11267.pdf 只是用的SD1.4没有对网络进行改造。 只做了4个改动 1 是对norm采用了groupnorm (GPU shader加速) 2 采用了GELU (GPU shader加速) 3 采用了两种attention优化&#xff0c;是partiti…

C语言之指针详解(2)

目录 本章重点 1. 字符指针 2. 数组指针 3. 指针数组 4. 数组传参和指针传参 5. 函数指针 6. 函数指针数组 7. 指向函数指针数组的指针 8. 回调函数 9. 指针和数组面试题的解析 数组指针 数组指针的定义 数组指针是指针&#xff1f;还是数组&#xff1f; 答案是&…

数据结构-队列

数据结构之队列 队列的概念顺序队列循环队列 顺序循环队列的ADT定义1、简单结构体定义2、初始化3、队列的清空4、计算队列的长度5、判断队列是否为空6、插入新的元素7、元素的删除8、遍历输出队列内的所有元素 链队列的ADT定义1、链队列简单结构体定义2、初始化链队列3、判断链…

chatgpt赋能python:Python怎么断行-让代码更易读

Python怎么断行 - 让代码更易读 大多数Python程序员都知道&#xff0c;代码可读性非常重要。好的代码应该易于阅读和理解&#xff0c;而不是让人困惑和痛苦。 然而&#xff0c;我们经常会发现一些Python代码在一行中拥挤着多个表达式、长变量名混杂其中&#xff0c;让人感到相…

数组:为什么数组都从0开始编号?

提到数组&#xff0c;我想你肯定不陌生&#xff0c;甚至还会自信地说&#xff0c;它很简单啊。 是的&#xff0c;在每一种编程语言中&#xff0c;基本都会有数组这种数据类型。不过&#xff0c;它不仅仅是一种编程语言中的数据类型&#xff0c;还是一种最基础的数据结构。尽管…

word中使用通配符批量将参考文献设置为上角标

目录 一、word中的通配符匹配规则 二、匹配单个参考文献 三、匹配多个参考文献 四、操作方式 &#xff08;1&#xff09;打开word中的替换功能 &#xff08;2&#xff09;输入要查找的内容 &#xff08;3&#xff09;选择替换格式 &#xff08;4&#xff09;点击替换 一…

Linux调试工具GDB(1)

文章目录 前言一、GDB概念二、GDB具体使用方法三、GDB断点类型总结 前言 本篇文章我们来介绍一下Linux中的调试利器GDB工具&#xff0c;在Linux的调试中GDB可以帮助我们来解决非常多的问题。 一、GDB概念 GDB是一个功能强大的调试工具&#xff0c;可以用于分析程序崩溃&…

temporal简介

文章目录 前言一、temporal是什么&#xff1f;二、使用步骤1.执行以下命令以启动预构建映像以及所有依赖项。2.运行示例 总结 前言 这两天在国外的网站发现了一个新的golang的微服务框架&#xff0c;感觉挺不错&#xff0c;分亨出来&#xff0c;大家一起看看。 一、temporal是…

python:并发编程(四)

前言 本文将和大家一起探讨python的多进程并发编程&#xff0c;使用内置基本库multiprocessing来实现并发&#xff0c;先通过官方来简单使用这个模块。先打好基础&#xff0c;能够有个基本的用法与认知&#xff0c;后续文章&#xff0c;我们再进行详细使用。 本文为python并发…

【数据库原理与应用 - 第四章】关系数据库规范化理论

目录 一、关系模式规范化理论 1、关系模式规范化概念 2、关系模式应满足的基本要求 3、关系规范化的意义 &#xff08;1&#xff09;一个好的数据库应遵循的标准 &#xff08;2&#xff09;规范化的意义 二、函数依赖 1、函数依赖的概念 &#xff08;1&#xff09;平凡…

Vue--》Vue3打造可扩展的项目管理系统后台的完整指南(五)

今天开始使用 vue3 ts 搭建一个项目管理的后台&#xff0c;因为文章会将项目的每一个地方代码的书写都会讲解到&#xff0c;所以本项目会分成好几篇文章进行讲解&#xff0c;我会在最后一篇文章中会将项目代码开源到我的GithHub上&#xff0c;大家可以自行去进行下载运行&…

学生成绩管理系统(逻辑清楚-简单实用)

1、需求分析 1.1、需求分析概述 需求分析是我们在软件开发中的重要环节&#xff0c;是软件开发的第一步也是最基础的环节&#xff0c;这将决定我们所实现的目标以及系统的各个组成部分、各部分的任务职能、以及使用到的数据结构、各个部门之间的组成关系和数据流程&#xff0…

chatgpt赋能python:Python列表操作:如何使用Python将数据放入列表中

Python列表操作&#xff1a;如何使用Python将数据放入列表中 在Python中&#xff0c;列表是一种重要的数据结构&#xff0c;允许我们将多个项目存储在单个变量中。在本文中&#xff0c;我们将介绍如何将数据放入Python列表中。我们将讨论Python中的列表数据类型以及如何向列表…

chatgpt赋能python:Python中如何放大图片

Python中如何放大图片 简介 图片是网站优化中不可或缺的一部分&#xff0c;然而&#xff0c;当图片在网站中被缩小或拉伸时&#xff0c;会导致其模糊或失真。在这种情况下&#xff0c;可以使用Python中的一些库来放大图片&#xff0c;同时保持图像的清晰度和质量。在本篇文章…

JavaScript 进阶 - 第3天

文章目录 JavaScript 进阶 - 第3天1 编程思想1.1 面向过程1.2 面向对象&#xff08;oop&#xff09; 2 构造函数3 原型对象3.1 原型3.2 constructor 属性3.3 对象原型3.4 原型继承3.5 原型链&#xff08;面试高频&#xff09; JavaScript 进阶 - 第3天 了解构造函数原型对象的语…

C++友元函数friend使用的学习总结

C友元函数friend使用的学习总结 1. 友元函数简介1.1 使用友元函数的目的1.2 友元函数的三种实现方法 2.全局函数做友元3.类做友元4.成员函数做友元注意&#xff01; 1. 友元函数简介 1.1 使用友元函数的目的 允许一个函数或者类访问另一个类中的私有成员,使得两个类可以共享同…

HTML学习(一)

Web标准 主要包括&#xff1a;结构&#xff0c;外观&#xff0c;行为三个方面 HTML标签&#xff0c;把标签学会&#xff0c;这门语言就基本学会了 双标签的关系&#xff0c;并列关系和包含关系 每个标签起作用的位置 vscode自动生成的代码的解释 <!DOCTYPE>文档类型声…