【算法思想·二叉树】最近公共祖先问题

本文参考labuladong算法笔记[拓展:最近公共祖先系列解题框架 | labuladong 的算法笔记]

0、引言

如果说笔试的时候经常遇到各种动归回溯这类稍有难度的题目,那么面试会倾向于一些比较经典的问题,难度不算大,而且也比较实用。

本文就用 Git 引出一个经典的算法问题:最近公共祖先(Lowest Common Ancestor),简称 LCA

git pull 这个命令我们经常会用,它默认是使用 merge 方式将远端别人的修改拉到本地;如果带上参数 git pull -r,就会使用 rebase 的方式将远端修改拉到本地。

这二者最直观的区别就是:merge 方式合并的分支会看到很多「分叉」,而 rebase 方式合并的分支就是一条直线。但无论哪种方式,如果存在冲突,Git 都会检测出来并让你手动解决冲突。

那么问题来了,Git 是如何检测两条分支是否存在冲突的呢?

以 rebase 命令为例,比如下图的情况,我站在 dev 分支执行 git rebase master,然后 dev 就会接到 master 分支之上:

这个过程中,Git 是这么做的:

首先,找到这两条分支的最近公共祖先 LCA,然后从 master 节点开始,重演 LCA 到 dev 几个 commit 的修改,如果这些修改和 LCA 到 master 的 commit 有冲突,就会提示你手动解决冲突,最后的结果就是把 dev 的分支完全接到 master 上面。

那么,Git 是如何找到两条不同分支的最近公共祖先的呢?这就是一个经典的算法问题了,下面我来由浅入深讲一讲。

1、寻找一个元素

先不管最近公共祖先问题,我请你实现一个简单的算法:

给你输入一棵没有重复元素的二叉树根节点 root 和一个目标值 val,请你写一个函数寻找树中值为 val 的节点。

函数签名如下:

def find(root: TreeNode, val: int) -> TreeNode:

这个函数应该很容易实现对吧,比如我这样写代码:

# 定义:在以 root 为根的二叉树中寻找值为 val 的节点
def find(root: TreeNode, val: int) -> TreeNode:
    # base case
    if not root:
        return None
    # 看看 root.val 是不是要找的
    if root.val == val:
        return root
    # root 不是目标节点,那就去左子树找
    left = find(root.left, val)
    if left:
        return left
    # 左子树找不着,那就去右子树找
    right = find(root.right, val)
    if right:
        return right
    # 实在找不到了
    return None

这段代码应该不用我多解释了,我下面基于这段代码做一些简单的改写,请你分析一下我的改动会造成什么影响。

首先,我修改一下 return 的位置:

def find(root: TreeNode, val: int) -> TreeNode:
    if not root:
        return None
    # 前序位置
    if root.val == val:
        return root
    # root 不是目标节点,去左右子树寻找
    left = find(root.left, val)
    right = find(root.right, val)
    # 看看哪边找到了
    return left if left else right

这段代码也可以达到目的,但是实际运行的效率会低一些,原因也很简单,如果你能够在左子树找到目标节点,还有没有必要去右子树找了?没有必要。但这段代码还是会去右子树找一圈,所以效率相对差一些。

更进一步,我把对 root.val 的判断从前序位置移动到后序位置:

def find(root: TreeNode, val: int) -> TreeNode:
    if root is None:
        return None
    # 先去左右子树寻找
    left = find(root.left, val)
    right = find(root.right, val)
    # 后序位置,看看 root 是不是目标节点
    if root.val == val:
        return root
    # root 不是目标节点,再去看看哪边的子树找到了
    return left if left is not None else right

这段代码相当于你先去左右子树找,然后才检查 root,依然可以到达目的,但是效率会进一步下降。因为这种写法必然会遍历二叉树的每一个节点

对于之前的解法,你在前序位置就检查 root,如果输入的二叉树根节点的值恰好就是目标值 val,那么函数直接结束了,其他的节点根本不用搜索。

但如果你在后序位置判断,那么就算根节点就是目标节点,你也要去左右子树遍历完所有节点才能判断出来。

最后,我再改一下题目,现在不让你找值为 val 的节点,而是寻找值为 val1  val2 的节点,函数签名如下:

def find(root: Tre
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值