大家好,我是 lucifer。今天给大家分享 Git 中的算法。
【Git 中的算法-最近公共祖先】这是本系列的第二篇 - 《Git 中的最近公共祖先》,第一篇在
这里
git merge-base
git merge-base A B 可以查找 A 提交和 B 提交的最近公共祖先提交。而由于 分支和标签
在 Git 中仅仅是提交的别名,因此 A 和 B 也可以是分支或者标签。
o---o---o---B
/---o---1---o---o---o---A
如上图的 Git 提交情况,那么 git merge-base A B 会直接返回提交 1 的哈希值。
更多关于
merge-base
的用法细节可以参考官方文档
如何查找公共祖先呢? 我们知道 git 每次提交,实际上都是新建了一个提交对象,里面记录一些元信息。比如:
- 提交人
- 提交时间
- 哈希
- 上一次提交的引用
- 。。。
进行开发,因此 git 提交本质上是有向无环图结构。
文章图片
如上图,我们基于提交 2 创建了新分支 dev,dev 上开发后我们可以将其合并到主分支
master。
而当我们执行合并操作的时候,git 会先使用 merge-base 算法计算最近公共祖先。
如果最近公共祖先是被 merge 的 commit, 则可执行 fast-forward。如下图,我们将 dev
合并到 master 就可以 fast-forward,就好像没有创建过 dev 分支一样。
文章图片
最后举一个更复杂的例子。如下图,我们在提交 3 上执行 git merge HEAD 提交 6。会发
生什么?
文章图片
答案是会找到提交 2。那怎么找到 2 呢?
如果从提交 6 出发不断找父节点,找到 1,并将其放到哈希表中。接下来再从提交 3 出发
同样不断找父节点,如果父节点在哈希表中存在,那么我们就找到了公共祖先,由于是找到
的第一个公共祖先,因此其是最近公共祖先,直接返回即可。
力扣中刚好有一个题目,我们来看下。
力扣真题 题目地址(236. 二叉树的最近公共祖先)
https://leetcode-cn.com/probl...
题目描述
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。” 示例 1:输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。示例 2:输入: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 提示:树中节点数目在范围 [2, 105] 内。
-109 <= Node.val <= 109
所有 Node.val 互不相同 。
p != q
p 和 q 均存在于给定的二叉树中。
前置知识
- 二叉树
- 树的遍历
- 哈希表
这道题是给你一个二叉树,让你从二叉树的根出发。
这和 Git 是不一样的,Git 中我们需要从两个提交节点出发往父节点找。
那是不是意味着上面方法不可以套用了?
也不是。我们可以在遍历二叉树的时候维护父子关系,然后问题就转化为了前面的问题。
代码
- 语言支持:Java
class Solution {
HashMap map = new HashMap<>();
// 关系为:key 的父亲是 value
HashSet set = new HashSet<>();
public void dfs(TreeNode root) {
if (root.left != null) {
map.put(root.left.val, root);
dfs(root.left);
}
if (root.right != null) {
map.put(root.right.val, root);
dfs(root.right);
}
}public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
dfs(root);
// 从 p 出发,找到 p 的所有祖先节点,将其放入HashSet
while (p != null) {
set.add(p);
p = map.get(p.val);
}
// 从 q 出发找到第一个能在HashSet中找到的节点,即为最近公共祖先
while (q != null) {
if (set.contains(q)) {
return q;
}
q = map.get(q.val);
}
return null;
}
}
复杂度分析
令 n 为链表长度。
- 时间复杂度:$O(n)$
- 空间复杂度:$O(n)$
有没有优化的可能?
当然可以。而且优化的角度有很多。
这不,这位同学就想到了预处理
,链接在这里。
即第一次维护好了节点信息,将其存到文件里,那么以后执行 merge-base,就不需要对已
经处理过的节点进行遍历了。**理论上,不管 merge-base 多少次,我们都仅遍历一次节
点**。
真的这么理想么?
很可惜不是的。比如我执行了 rebase ,reset 等操作改变了节点怎么处理?这里的细节很
多,我就不在这里展开了。感兴趣的可以加入我的力扣群讨论。
总结 git merge-base 本质上就是一个寻找最近公共祖先的算法。
而这个算法最朴素的就是先从一个节点使用哈希表预处理,然后从另外一个节点开始遍历,
找到的第一个在哈希表中出现的节点就是最近公共祖先。
这个算法也有优化空间,而优化后又需要考虑各种边界条件,即缓存失效问题。
以上就是本文的全部内容了。大家对此有何看法,欢迎给我留言,我有时间都会一一查看回
答。更多算法套路可以访问我的 LeetCode 题解仓库
:https://github.com/azl3979858... 。 目前已经 40K star 啦。大家也可以关
注我的公众号《力扣加加》带你啃下算法这块硬骨头。
关注公众号力扣加加,努力用清晰直白的语言还原解题思路,并且有大量图解,手把手教你
识别套路,高效刷题。
文章图片