本文参考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

最低0.47元/天 解锁文章
2542

被折叠的 条评论
为什么被折叠?



