【二叉树进阶】二叉树经典面试题——最近公共祖先问题

news2024/11/14 17:39:17

文章目录

  • 1. 二叉树的最近公共祖先
    • 1.1 思路1(转换为链表相交问题)
    • 1.2 链表相交问题讲解
    • 1.3 思路2
    • 1.4 思路2AC代码
  • 2. 剑指 Offer 68 - I. 二叉搜索树的最近公共祖先
    • 2.1 思路分析
    • 2.2 AC代码
  • 3. 普通二叉树求最近公共祖先的优化-转化为路径相交的问题
    • 3.1 思路分析
    • 3.2 AC代码

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

题目链接: link

在这里插入图片描述

这道题呢,是给我们一棵二叉树,让我们找出两个指定结点的最近公共祖先。

首先我们来看一下,最近的公共祖先有哪几种情况:

在这里插入图片描述
先来看这个,0和7的最近公共祖先是3,这个没什么问题
然后再看一个
在这里插入图片描述
7和4呢,2 、5 、3是不是都是它们两个的公共祖先啊,但是题目要求找最近的公共祖先,所以是2。
再看一种情况
在这里插入图片描述
5和4的公共祖先是谁啊?
我们可能会认为是3,但是题目说了,一个节点也可以是它自己的祖先,所以应该是5。

那了解了题目的意思,我们来分析一下解题思路。

ps:下面提供多种思路,但不会都实现出来代码,有些思路对于当前这道题目可能并不是特别可行,主要是帮助大家拓展思维。

1.1 思路1(转换为链表相交问题)

首先不知道大家有没有做过这样一道题:

就是链表那块有一个比较经典的题目——相交链表。
在leetcode上也有对应的题目
在这里插入图片描述
其实就是去找两个链表的第一个相交结点。

那大家看:

对于我们当前这道题,是找二叉树中两个结点的最近的公共祖先。
在这里插入图片描述
当前二叉树的结构只有左右孩子两个指针。
如果它是一个三叉链的结构
在这里插入图片描述
还有一个指向parent父结点的指针。
那这道题是不是就可以看作一个链表相交的问题了

在这里插入图片描述
因为有了parent指针我们就可以从孩子结点沿着parent往上走了,就像链表从前完后走一样。

1.2 链表相交问题讲解

那有做过链表相交问题的回顾一下,没做过的思考一下,链表相交问题,可以怎么去找第一个交点
在这里插入图片描述
那在这里我提供两种思路。

第一种,暴力求解:

让A链表的中每个结点依次与B中所有结点逐个比较,第一个相同的结点就是第一个交点。

//暴力求解,让A链表的每个结点依次与B中所有结点逐个比较。O(N^2)
struct ListNode* getIntersectionNode(struct ListNode* headA, struct ListNode* headB)
{
    struct ListNode* curA = headA;
    struct ListNode* curB = headB;
    while (curA)
    {
        curB=headB;
        while (curB)
        {
            if (curA == curB)
                return curA;
            curB = curB->next;
        }
        curA = curA->next;
    }
    return NULL;
}

ps:我这里给的代码是之前用C语言写的。

那想要效率高一点,第二种解法:

首先遍历两个链表找尾,判断两个链表的尾结点是否相同,不相同,那就肯定不相交,直接返回false。
如果相交的话,去找相交点,怎么找呢?
计算出两个链表长度的差值gap,然后让长的那个链表先走gap步,然后两个链表一块走,每走一步,判断两个结点是否相同,第一个相同的结点就是第一个交点。

#include <math.h>
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    struct ListNode *curA=headA;
    struct ListNode *curB=headB;
    int lenA=0;
    int lenB=0;
    //找尾,先判断是否相交,不相交直接返回NULL,相交再找
    while(curA->next)
    {
        lenA++;
        curA=curA->next;
    }
    //计算准确长度应该这样写:while(curA)
    //这样while(curA->next)比实际长度小1,但是两个都小1,不影响差值
    //而这样写循环结束cur就是尾,可直接判断是否相交
    while(curB->next)
    {
        lenB++;
        curB=curB->next;
    }
    //尾不相等,则不相交
    if(curA!=curB)
        return NULL;
    //计算差值
    int gap=abs(lenA-lenB);
    //找出长的那一个
    struct ListNode *longlist=headA;
    struct ListNode *shortlist=headB;
    if(lenB>lenA)
    {
        longlist=headB;
        shortlist=headA;
    }
    //长表先走差值步
    while(gap--)
    {
        longlist=longlist->next;
    }
    //再一起走,比较两个链表的当前结点是否相同
    //,第一个相同的就是第一个交点
    while(longlist!=shortlist)
    {
        longlist=longlist->next;
        shortlist=shortlist->next;
    }
    return longlist;
}

但是现在题目中的二叉树并不是三叉链结构,要想拷贝转换成三叉链也比较麻烦。
所以我们看第二种思路:

1.3 思路2

那我们要直接去找,怎么做呢?

其实还可以考虑用递归。

观察上面我们分析的这三种情况:

在这里插入图片描述
会发现
第一种情况,要查找的两个结点一个在整棵树根结点的左子树上,一个在右子树上,所以根结点就是它们最近的公共祖先。
第二种情况的话,两个结点都在根结点的左子树(如何判断在哪个子树上,就需要我们自己写一个类似find的函数判断),那首先根结点不会是最近的公共祖先了,其次,公共结点有可能在右子树吗?
是绝对不可能的,所以我们就可以递归去左子树查找。
那后续也是一样,到了左子树发现两个结点都在右子树上,所以再递归到右子树查找。
那此时就走到了2的位置
在这里插入图片描述
那一个结点在2的左,一个在2的右,所以2就是最近的公共祖先,就找到了。
第三种情况呢?
在这里插入图片描述
那对于第三种情况首先还是会递归到左子树,然后走到5这个结点,会发现一个结点时5本身,另一个结点在5的右子树。
所以5就是最近公共祖先。
因此我们得出一个结论,如果两个结点里面有一个是某棵树的根结点,另一个在这棵树的子树上,那么这个根结点就是最近公共祖先。

1.4 思路2AC代码

那我们来写一下代码:

在这里插入图片描述
那然后我们要来实现一下判断一个结点在左子树还是在右子树的这个IsInTree函数。
在这里插入图片描述

那我们就写完了

class Solution {
public:
    bool IsInTree(TreeNode* root, TreeNode* x)
    {
        if(root==nullptr)
            return false;
        return root==x
            || IsInTree(root->left,x)
            || IsInTree(root->right,x);
    }
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if(root==nullptr)
            return nullptr;
        
        //如果根结点是p,q其中一个,那另一个肯定是它孩子,则根结点就是两者的公共祖先
        if(root==p||root==q)
            return root;

        //判断p,q两个结点在当前根结点的左子树还是右子树
        bool pInLeft=IsInTree(root->left,p);
        bool pInRight=!pInLeft;

        bool qInLeft=IsInTree(root->left,q);
        bool qInRight=!qInLeft; 

        //如果一个在当前根结点左子树,一个在右子树,则当前根结点就是最近公共祖先
        if((qInLeft&&pInRight)||(pInLeft&&qInRight))
            return root;
        //如果都在左子树,就递归去左子树查找
        else if(qInLeft&&pInLeft)
            return lowestCommonAncestor(root->left,p,q);
        //如果都在右子树,就递归去右子树查找
        else
            return lowestCommonAncestor(root->right,p,q);
    }
};

测试一下

在这里插入图片描述
但是我们看到这种方法其实时间效率是比较低的,它的时间复杂度是一个O(N^2)
首先我们从根节点不断往左右子树去递归的过程是一个O(N),然后每一次递归去判断在不在的过程也是一个O(N),所以是O(N^2)

不过不用担心,后面我们会进行优化。

2. 剑指 Offer 68 - I. 二叉搜索树的最近公共祖先

那我们上面讲的是普通二叉树寻找公共最近祖先的问题,那这道题其实也有搜索二叉树的版本

题目链接: link
在这里插入图片描述

题目没什么变化,就是上一题是普通二叉树,这道题是搜索二叉树

2.1 思路分析

其实思路根上一题的思路还是一样的,但是,对于搜索二叉树来说,我们还需要手动写一个函数去判断结点在左子树还是在右子树吗?

🆗,不需要了,因为我们通过它们与根结点的大小关系就直接可以判断出来了。
那这样同样的思路,时间复杂度其实就变成O(N)了。
因为不再需要使用那个函数(O(N))去判断了

2.2 AC代码

那代码也很简单,把上一题那个拷贝过来,简单修改一下就行了

在这里插入图片描述
在这里插入图片描述
这次的效率明显就高了。

3. 普通二叉树求最近公共祖先的优化-转化为路径相交的问题

上面普通二叉树求最近公共祖先的问题
在这里插入图片描述
我们实现的算法效率比较低,是O(N^2)的。

那能不能进行一个优化呢?

我们可以将它转换成一个路径相交问题,转换之后的解法就类似上面提到的链表相交问题。

3.1 思路分析

那具体怎么做呢?

首先我们可以获取从根结点开始到两个结点的路径,然后保存到容器里面:

那选择什么容器保存路径呢?
这里用栈(stack)是比较合适的(先进后出),后面大家就会明白。
我们走一个前序的DFS来获取路径:
以这张图为例
在这里插入图片描述
从根结点开始,首先判断根结点不为空,为空直接返回false,不为空先把根结点入栈,然后判读根结点是不是目标结点,是的话,直接返回true,栈里面的根结点就是路径,不是的话,就去左子树找。
在这里插入图片描述
递归去左子树继续找(还是一样,先看根结点为不为空,不为空入栈,判断是否是目标结点,不是就去它的左右子树接着找),如果左子树找到了,就返回true,左子树没找到,再去右子树去找右子树找到了,就返回true。
如果左右子树都没找到,说明走当前这个结点时不正确的路径,那就需要把它从栈里面pop掉。
然后,返回flase,返回到上一层递归调用的地方,继续去上一层,没有找过的地方找。
在这里插入图片描述
那大家看这个找路径这个算法,时间复杂度是多少?
是不是O(N)啊。

那获取了路径,后的步骤就跟链表相交找交点类似:

先让元素多的那个栈出元素,出到两个栈元素个数一样的时候,同时出,然后遇到第一个相同的元素,就是最近的公共祖先。
那这也是一个O(N)

所以该算法整体就是一个O(N)的算法。
当然这种思路的代价是空间复杂度会高一点,因为我们额外开了两个栈。

3.2 AC代码

我们来写一下代码:

在这里插入图片描述
然后我们写一下获取路径的函数就行了。
在这里插入图片描述

提交一下:
在这里插入图片描述
比之前的还是快了挺多的。

class Solution {
public:
    bool GetPath(TreeNode* root, TreeNode* x, stack<TreeNode*>& path)
    {
        if(root==nullptr)
            return false;

        path.push(root);
        if(root==x)
            return x;
        
        if(GetPath(root->left,x,path))
        {
            return true;
        }
        if(GetPath(root->right,x,path))
        {
            return true;
        }

        path.pop();
        return false;
    }
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        stack<TreeNode*> pPath;
        stack<TreeNode*> qPath;

        //获取两个结点的路径
        GetPath(root,q,qPath);
        GetPath(root,p,pPath);

        while(pPath.size()!=qPath.size())
        {
            if(pPath.size()>qPath.size())
            {
                pPath.pop();
            }
            else
            {
                qPath.pop();
            }
        }

        while(qPath.top()!=pPath.top())
        {
            qPath.pop();
            pPath.pop();
        }

        return qPath.top();
    }
};

在这里插入图片描述

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

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

相关文章

国产DSP,C2000,QX320F280049,打破TI垄断,支持国产

数字电源&#xff0c;光伏逆变器&#xff0c;微电机&#xff0c;充电桩&#xff0c;家电&#xff0c;机器人专用

Python绘图坐标系映射,对数坐标系

文章目录 对数坐标系自定义映射对数坐标图 对数坐标系 在实际绘图时&#xff0c;如果 x , y x,y x,y这两轴的数据变化速率相差过多&#xff0c;线性的坐标映射将无法展示图形变化的细节&#xff0c;就需要更改坐标系的数字映射逻辑&#xff0c;以获得更具细节的图像。 在matp…

【华为HCIP | 高级网络工程师】刷题日记(11)

个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名大二在校生&#xff0c;喜欢编程&#x1f38b; &#x1f43b;‍❄️个人主页&#x1f947;&#xff1a;落798. &#x1f43c;个人WeChat&#xff1a;见文末 &#x1f54a;️系列专栏&#xff1a;&#x1f5bc;️ 零…

pycharm bash: 第 0 行: cd: xxxxxxx: 没有那个文件或目录

设置里面的python接口&#xff0c;path mappings 是空的&#xff0c;设置好本地机器和远程机器所对应的目录就好了。如下图:

【100天精通python】Day23:正则表达式,基本语法与re模块详解示例

目录 专栏导读 1 正则表达式概述 2 正则表达式语法 2.1 正则表达式语法元素 2.2 正则表达式的分组操作 3 re 模块详解与示例 4 正则表达式修饰符 专栏导读 专栏订阅地址&#xff1a;https://blog.csdn.net/qq_35831906/category_12375510.html 1 正则表达式概述 python 的…

24考研数据结构-第四章:串

目录 第四章&#xff1a;串4.1串的定义和实现4.1.1串的定义4.1.2串的基本操作字符集编码 4.1.3串的存储结构1. 定长顺序存储表示基本操作实现&#xff08;基于方案四&#xff09; 2. 堆分配存储表示3. 串的链式存储结合链表思考优缺点 知识回顾 4.2串的模式匹配4.2.1朴素模式匹…

ChatGPT辅助写论文:提升效率与创造力的利器

写作是人类最重要的交流方式之一&#xff0c;也是学术研究中不可或缺的环节。然而&#xff0c;写作并不是一件容易的事情&#xff0c;尤其是对于科研人员来说&#xff0c;他们需要花费大量的时间和精力来撰写高质量的论文&#xff0c;并且面临着各种各样的挑战&#xff0c;如语…

用栈实现队列——数据结构与算法

&#x1f636;‍&#x1f32b;️Take your time ! &#x1f636;‍&#x1f32b;️ &#x1f4a5;个人主页&#xff1a;&#x1f525;&#x1f525;&#x1f525;大魔王&#x1f525;&#x1f525;&#x1f525; &#x1f4a5;代码仓库&#xff1a;&#x1f525;&#x1f525;魔…

Flowable-子流程-事务子流程

目录 定义图形标记XML内容使用示例视频教程 定义 事务子流程也称作事务块&#xff0c;是一个嵌入式子流程&#xff0c;用来处理一组必须在同一个事务中完成的活 动&#xff0c;使它们共同成功或失败。事务子流程中如果有一个活动失败或者取消&#xff0c;整个事务子流程的所有…

<C++> 四、模板初阶

1.泛型编程 泛型编程&#xff1a;编写与类型无关的通用代码&#xff0c;是代码复用的一种手段。模板是泛型编程的基础。 如何实现一个通用的交换函数呢&#xff1f;使用函数重载可以实现 void Swap(int &left, int &right) {int temp left;left right;right temp; …

延时函数

其实从学习FreeRTOS以来&#xff0c;一直有个疑问&#xff1a;为什么freertos.c中的延时函数要使用osDelay()&#xff0c;而不直接使用HAL_Delay()呢&#xff1f;两者有什么区别&#xff1f; 本节就来探讨一下这个问题的答案&#xff1a; 延时函数分类 相对延时&#xff1a;…

linux查看日志指令

今天导师要求&#xff0c;使用linux查看日志&#xff0c;这里呢&#xff0c;先是学习了一下ubuntu内核的linux&#xff0c;现在回顾一下查看日志的语句 linux查看日志指令 一、tail二、head三、cat四、more五、view5.1 从上往下找5.2 从下往上找5.3退出 六、搜索6.1模糊搜索6.2…

【新版系统架构补充】-嵌入式技术

嵌入式微处理体系结构 冯诺依曼结构 传统计算机采用冯诺依曼结构&#xff0c;也称普林斯顿结构&#xff0c;是一种将程序指令存储器和数据存储器合并在一起的存储器结构 冯诺依曼的计算机程序和数据共用一个存储空间&#xff0c;程序指令存储地址和数据存储地址指向同一个存…

中国最赚钱公司出炉

7月25日&#xff0c;2023年《财富》中国500强排行榜正式发布。国家电网以5300亿美元的营收位居榜首&#xff0c;中国石油和中国石化分列第二和第三。工商银行则成为最赚钱的公司。 图片来源&#xff1a;财富FORTUNE 1中国500强TOP10 数据显示&#xff0c;今年500家上榜的中国…

华为交换机配置SSH远程登录

连接console线直接配置 1、在服务器端生成本地密钥对 <HUAWEI>system-view [HUAWEI] dsa local-key-pair create Info: The key name will be: HUAWEI_Host_DSA. Info: The key modulus can be any one of the following : 1024, 2048. Info: If the key modulus i…

Qt应用开发(基础篇)——滑块类 QSlider、QScrollBar、QDial

目录 一、前言 二、QAbstractSlider类 1、invertedAppearance 2、invertedControls 3、maximum 4、minimum 5、orientation 6、pageStep 7、singleStep 8、sliderDown 9、tracking 10、sliderPosition 11、value 12、信号 三、QDial类 1、notchSize 2、notchTa…

BES 平台 SDK之代码架构讲解一

本文章是基于BES2700 芯片&#xff0c;其他BESxxx 芯片可做参考&#xff0c;如有不当之处&#xff0c;欢迎评论区留言指出。 1. SDK 目录下包含的文件 1.1 Apps 文件 主要包含了上层应用逻辑代码&#xff0c;比如电池电量管理、按键、main目录下的app_init.c &#xff08;应用…

IBM Spectrum LSF (“LSF“ ,简称为负载共享设施) 用户案例

IBM Spectrum LSF (“LSF” &#xff0c;简称为负载共享设施) 用户案例 IBM Spectrum LSF (“LSF” &#xff0c;简称为负载共享设施) 软件是业界领先的企业级软件。 LSF 在现有异构 IT 资源之间分配工作&#xff0c;以创建共享&#xff0c;可扩展且容错的基础架构&#xff0c…

Linux进程(万字解析)

进程 一.冯诺依曼体系结构二.操作系统1.管理的概念2.系统调用和库函数概念 三.进程1.先描述2.再组织 四.Linux里的PCB1.概念2.理解当前路径3.PID1.kill指令2.获取自己的PID 4.初识fork函数 五.进程状态1.一般意义上的1.运行2.阻塞3.挂起 2.Linux内核里的状态1.运行态2.阻塞态3.…

2023软考下半年考试和报名时间汇总(附详细报名流程)

2023年上半年软考结束了&#xff0c;相信有不少准备报考下半年软考的考生正摩拳擦掌&#xff0c;期待在11月的考试中大显身手。2023下半年软考什么时候报名呢&#xff1f;一起来看看吧~ 根据中国计算机技术职业资格网发布的关于《2023年度计算机技术与软件专业技术资格&#x…