文本diff算法Patience Diff


一般在使用 Myers diff算法及其變體時, 對於下面這種例子工作不是很好, 讓變化不易閱讀, 並且容易導致合並沖突

void Chunk_copy(Chunk *src, size_t src_start, Chunk *dst, size_t dst_start, size_t n)
{
    if (!Chunk_bounds_check(src, src_start, n)) return;
    if (!Chunk_bounds_check(dst, dst_start, n)) return;

    memcpy(dst->data + dst_start, src->data + src_start, n);
}

int Chunk_bounds_check(Chunk *chunk, size_t start, size_t n)
{
    if (chunk == NULL) return 0;

    return start <= chunk->length && n <= chunk->length - start;
}

接下來我們對這段代碼中的兩個方法調整一下順序. 使用原始的 Myers diff 算法, 我們會得到以下的diff, 這個結果是清晰的易於閱讀的, 並且標注了新舊版本中有意義的變動, 這種diff不容易造成合並沖突

+int Chunk_bounds_check(Chunk *chunk, size_t start, size_t n)
+{
+    if (chunk == NULL) return 0;
+
+    return start <= chunk->length && n <= chunk->length - start;
+}
+
 void Chunk_copy(Chunk *src, size_t src_start, Chunk *dst, size_t dst_start, size_t n)
 {
     if (!Chunk_bounds_check(src, src_start, n)) return;
     if (!Chunk_bounds_check(dst, dst_start, n)) return;

     memcpy(dst->data + dst_start, src->data + src_start, n);
 }
-
-int Chunk_bounds_check(Chunk *chunk, size_t start, size_t n)
-{
-    if (chunk == NULL) return 0;
-
-    return start <= chunk->length && n <= chunk->length - start;
-}

但是, 使用線性空間版本的Myers算法會實際得到如下的diff, 這個結果不容易閱讀並且將空行和函數起止符號也標為了變更的一部分, 這種diff容易造成合並沖突.

-void Chunk_copy(Chunk *src, size_t src_start, Chunk *dst, size_t dst_start, size_t n)
+int Chunk_bounds_check(Chunk *chunk, size_t start, size_t n)
 {
-    if (!Chunk_bounds_check(src, src_start, n)) return;
-    if (!Chunk_bounds_check(dst, dst_start, n)) return;
+    if (chunk == NULL) return 0;

-    memcpy(dst->data + dst_start, src->data + src_start, n);
+    return start <= chunk->length && n <= chunk->length - start;
 }

-int Chunk_bounds_check(Chunk *chunk, size_t start, size_t n)
+void Chunk_copy(Chunk *src, size_t src_start, Chunk *dst, size_t dst_start, size_t n)
 {
-    if (chunk == NULL) return 0;
+    if (!Chunk_bounds_check(src, src_start, n)) return;
+    if (!Chunk_bounds_check(dst, dst_start, n)) return;

-    return start <= chunk->length && n <= chunk->length - start;
+    memcpy(dst->data + dst_start, src->data + src_start, n);
 }

這里將引入另一種與Myers非常不同的diff算法, 因為它在某些輸入下, 比線性空間的Myers算法要好. 這個算法稱為patience diff, 這個算法的創造者為BitTorrent的作者Bram Cohen. 在他的博客上有一個簡單的介紹( https://bramcohen.livejournal.com/73318.html , https://alfedenzo.livejournal.com/170301.html ). 我們這里通過例子看一下它的實現.  

首先要注意的是, patience diff實際上不算是一種算法, 而是一種在對比兩個文本時如何在應用diff算法(例如Myers)前, 將文本分為合理的小文本的手段. 做這種預先處理的原因是, Myers經常將一些無意義的行匹配起來, 例如空行和括號, 這會導致一些惱人的匹配結果以及導致合並沖突的結果. Patience diff 的改進是: 對兩個文本都進行一次全掃描, 得到一組共有的, 在各自文本里都只出現了一次的行, 這將助於得到更有意義而不是生硬的內容划分

讓我們看一個例子, 對以下兩個句子進行分段

this is incorrect and so is this

this is good and correct and so is this

從直觀上看, 變化在於將 incorrect 替換成了 good and correct. 將兩句話按詞分行展示

    1   this                            1   this
    2   is                              2   is
    3   incorrect                       3   good
    4   and                             4   and
    5   so                              5   correct
    6   is                              6   and
    7   this                            7   so
                                        8   is
                                        9   this

Patience diff 通過匹配唯一行來進行分段. 唯一行的意思是, 兩邊都僅僅在出現一次. 在這個例子中, 在兩邊都僅僅出現一次的詞是so, 所以將左側Line5和右側Line7關聯起來

                                        1   this
                                        2   is
    1   this                            3   good
    2   is                              4   and
    3   incorrect                       5   correct
    4   and                             6   and

    5   so          <--------------->   7   so

    6   is                              8   is
    7   this                            9   this

於是使用同樣的方法, 對划分后產生的子文本再次划分. 通過對比左側的Line1-4和右側的Line1-6, 以及左側的Line6-7和右側的Line8-9. 在這些區域中又找到了一些匹配的唯一行.

    1   this        <--------------->   1   this
    2   is          <--------------->   2   is

                                        3   good
    3   incorrect                       4   and
    4   and                             5   correct
                                        6   and
--------------------------------------------------------------------
*   5   so                              7   so
--------------------------------------------------------------------
    6   is          <--------------->   8   is
    7   this        <--------------->   9   this

如果記錄下這些匹配的行, 再進一步划分這個文本, 我們最終只留下了一個非空的待比較區域: 左側的Line3-4和右側的Line3-6.

*   1   this                            1   this
--------------------------------------------------------------------
*   2   is                              2   is
--------------------------------------------------------------------
                                        3   good
    3   incorrect                       4   and
    4   and                             5   correct
                                        6   and
--------------------------------------------------------------------
*   5   so                              7   so
--------------------------------------------------------------------
*   6   is                              8   is
--------------------------------------------------------------------
*   7   this                            9   this

在這個區域里, 再無可以匹配的唯一行了, 所以Patience diff完成了這一步的處理, 可以將這個區域的處理交給Myers去計算diff結果. 最后, 將所有結果再收集回來, 將得到

this
is
- incorrect
+ good
+ and
+ correct
and
so
is
this

這相對於原始的Myers算法是一種改進, 因為原始的Myers算法, 會錯誤地將good and correct分開而分別處理

this
is
- incorrect
+ good
and
+ correct
+ and
so
is
this

下面再介紹一個復雜的例子. 假設我們要對以下兩個列表做diff

    1   David Axelrod                   1   The Slits
    2   Electric Prunes                 2   Gil Scott Heron
    3   Gil Scott Heron                 3   David Axelrod
    4   The Slits                       4   Electric Prunes
    5   Faust                           5   Faust
    6   The Sonics                      6   The Sonics
    7   The Sonics                      7   The Sonics

這里大部分的行都是可以匹配的唯一行, 如下所示, 注意最后兩行沒有匹配, 因為它們不是唯一行.

1 <---------> 3
2 <---------> 4
3 <---------> 2
4 <---------> 1
5 <---------> 5

但是, 這里有一些匹配交叉了: 如果你根據上面這些匹配做划分, 這些划分是有沖突的. 這意味着我們不能全部使用這些匹配. 我們需要舍棄一些匹配. 這是通過選擇右側的最長子序列得到的, 在子序列里數字要單調遞增. 例如如果我們選擇以下三組匹配

1 <---------> 3
2 <---------> 4
5 <---------> 5

那么我們可以將文本按如下划分:

                                        1   The Slits
                                        2   Gil Scott Heron
--------------------------------------------------------------------
    1   David Axelrod     <--------->   3   David Axelrod
    2   Electric Prunes   <--------->   4   Electric Prunes
--------------------------------------------------------------------
    3   Gil Scott Heron
    4   The Slits
--------------------------------------------------------------------
    5   Faust             <--------->   5   Faust
--------------------------------------------------------------------
    6   The Sonics                      6   The Sonics
    7   The Sonics                      7   The Sonics

實際上, 3,4,5確實是右側最長的單調遞增序列. 獲得這個序列有很多算法, 而patience diff使用的是patience sorting, 這也是patience diff這個名稱的由來.我們通過下面這個例子來解釋一下patience sorting是如何工作的.

9 4 6 Q 8 7 A 5 10 J 3 2 K

我們希望找到上面這個列表中, 最長的單調遞增的子序列. 上面A是最小的, 比10大的依次是J Q K. 4 6 7是一個可能的序列, 8 10 J K也是. 而序列 7 A 5就不是, 因為A是比7小的.
以下我們通過例子分析一下 Patience sorting 的工作機制. 這個機制類似與卡牌游戲patience或solitaire, 卡牌必須在棧中倒序排列.

首先, 我們將9取出, 作為一個新的棧.

4   6   Q   8   7   A   5   10  J   3   2   K
---------------------------------------------

9

一旦建立了一個新的棧, 算法的處理就按以下規則開始了. 取下列表中的一個牌, 找到所有棧頂的牌大於當前牌的棧中的最左邊的那個, 將當前牌放到這個棧的棧頂. 在我們的例子中, 下一張牌是4, 第一個棧棧頂是9, 所以可以將4放到9這個棧的棧頂.

6   Q   8   7   A   5   10  J   3   2   K
-----------------------------------------

4
9

下一張牌是6, 比4大, 所以不能放到4這個棧的棧頂, 而必須在右側新建一個棧. 對於任何新加入的牌, 如果不是加入最左邊那個棧, 都需要記錄一個指針, 將這個指針指向左側相鄰那個棧的棧頂. 這個例子中, 我們將6的指針指向了左側的4.

Q   8   7   A   5   10  J   3   2   K       6 -> 4
-------------------------------------

4
9   6

下一個是Q, Q比之前的所有值都大, 所以我們又創建了一個新棧並放到右側, 並且將Q的指針指向6.

8   7   A   5   10  J   3   2   K           6 -> 4
---------------------------------           Q -> 6

4
9   6   Q

  

下一個是8, 8比4和6大, 但是比Q小, 所以將8放到Q這個棧的棧頂, 並將指針指向6.

7   A   5   10  J   3   2   K               6 -> 4
-----------------------------               Q -> 6
                                            8 -> 6
4       8
9   6   Q

對7也是同樣的處理.

A   5   10  J   3   2   K                   6 -> 4
-------------------------                   Q -> 6
                                            8 -> 6
        7                                   7 -> 6
4       8
9   6   Q

而A比所有其他數都小, 所以被放到了最左側的棧的棧頂, 因為是最左側, 所以指針不需要指向其他牌.

5   10  J   3   2   K                       6 -> 4
---------------------                       Q -> 6
                                            8 -> 6
A       7                                   7 -> 6
4       8
9   6   Q

接下來是5, 被放到了6這個棧的棧頂, 並將指針指向了A.

10  J   3   2   K                           6 -> 4
-----------------                           Q -> 6
                                            8 -> 6
A       7                                   7 -> 6
4   5   8                                   5 -> A
9   6   Q

10比當前所有棧的棧頂元素都大, 所以新建一個棧放到右側. 指針指向7.

J   3   2   K                               6 -> 4
-------------                               Q -> 6
                                            8 -> 6
A       7                                   7 -> 6
4   5   8                                   5 -> A
9   6   Q  10                              10 -> 7

J比當前所有棧的棧頂元素都大, 所以新建一個棧放到右側. 指針指向10.

The J again is a greater rank than anything on the stacks, so we begin a new one.

3   2   K                                   6 -> 4
---------                                   Q -> 6
                                            8 -> 6
A       7                                   7 -> 6
4   5   8                                   5 -> A
9   6   Q  10   J                          10 -> 7
                                            J -> 10

3可以放置到5這個棧的棧頂, 並將指針指向A.

2   K                                       6 -> 4
-----                                       Q -> 6
                                            8 -> 6
A   3   7                                   7 -> 6
4   5   8                                   5 -> A
9   6   Q  10   J                          10 -> 7
                                            J -> 10
                                            3 -> A

對2也是同樣的處理.

K                                           6 -> 4
-                                           Q -> 6
                                            8 -> 6
    2                                       7 -> 6
A   3   7                                   5 -> A
4   5   8                                  10 -> 7
9   6   Q  10   J                           J -> 10
                                            3 -> A
                                            2 -> A

最后, 因為K比當前所有棧的棧頂元素都大, 所以新建一個棧放到右側. 指針指向J.

                                            6 -> 4
                                            Q -> 6
                                            8 -> 6
    2                                       7 -> 6
A   3   7                                   5 -> A
4   5   8                                  10 -> 7
9   6   Q  10   J   K                       J -> 10
                                            3 -> A
                                            2 -> A
                                            K -> J

通過這個結果, 我們可以使用產生的指針找到最長的遞增序列. 我們通過最右側的棧的棧頂, 順着指針得到以下的序列:

K -> J -> 10 -> 7 -> 6 -> 4

將這個序列倒序就得到了最長的單調遞增的序列 4 6 7 10 J K. Patience diff 使用這個方法處理按左側行號排列的唯一行匹配, 找到最長的單調遞增子序列. 然后使用這個序列來划分兩側的文本.

Pateince diff是根據這個算法取名的, 但是換成其他的算法也都沒問題. Patience diff最重要的地方在於, 它找到了一種主動尋找有意義的內容匹配的方法. 而Myers並沒有使用任何啟發式的算法對文本進行分析, 而是按行對內容進行處理. 對文本進行分析並分塊, 會增加一些處理時間, 但是能得到更優雅的diff結果

Source: https://blog.jcoglan.com/2017/09/19/the-patience-diff-algorithm/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM