一般在使用 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/