Git 中的演算法-最近公共祖先

lucifer發表於2022-04-07

大家好,我是 lucifer。今天給大家分享 Git 中的演算法。

這是本系列的第二篇 - 《Git 中的最近公共祖先》,第一篇在
這裡

<!-- more -->

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 提交是一個連結串列結構。而 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

Java Code:


class Solution {
    HashMap<Integer, TreeNode> map = new HashMap<>(); // 關係為:key 的父親是 value
    HashSet<TreeNode> 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)$

優化

實際上該演算法效率並不高。如果我們倉庫提交很多,也就是 N 非常大,也是會慢的。

有沒有優化的可能?

當然可以。而且優化的角度有很多。

這不,這位同學就想到了預處理
連結在這裡
即第一次維護好了節點資訊,將其存到檔案裡,那麼以後執行 merge-base,就不需要對已
經處理過的節點進行遍歷了。**理論上,不管 merge-base 多少次,我們都僅遍歷一次節
點**。

真的這麼理想麼?

很可惜不是的。比如我執行了 rebase ,reset 等操作改變了節點怎麼處理?這裡的細節很
多,我就不在這裡展開了。感興趣的可以加入我的力扣群討論。

總結

git merge-base 本質上就是一個尋找最近公共祖先的演算法。

而這個演算法最樸素的就是先從一個節點使用雜湊表預處理,然後從另外一個節點開始遍歷,
找到的第一個在雜湊表中出現的節點就是最近公共祖先。

這個演算法也有優化空間,而優化後又需要考慮各種邊界條件,即快取失效問題。

以上就是本文的全部內容了。大家對此有何看法,歡迎給我留言,我有時間都會一一檢視回
答。更多演算法套路可以訪問我的 LeetCode 題解倉庫
https://github.com/azl3979858... 。 目前已經 40K star 啦。大家也可以關
注我的公眾號《力扣加加》帶你啃下演算法這塊硬骨頭。

關注公眾號力扣加加,努力用清晰直白的語言還原解題思路,並且有大量圖解,手把手教你
識別套路,高效刷題。

相關文章