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

文本diff算法Patience Diff的更多相关文章

  1. 动态规划处理diff算法 Myers Diff (正向)

    Eugene W. Myers 在他1986年发表于"Algorithmica"的论文"An O(ND) Difference Algorithm and Its Var ...

  2. diff算法深入一下?

    文章转自豆皮范儿-diff算法深入一下 一.前言 有同学问:能否详细说一下 diff 算法. 简单说:diff 算法是一种优化手段,将前后两个模块进行差异化比较,修补(更新)差异的过程叫做 patch ...

  3. React Diff算法一览

    前言 diff算法一直是React系统最核心的部分,并且由于演化自传统diff,使得比较方式从O(n^3)降级到O(n),然后又改成了链表方式,可谓是变化万千. 传统Diff算法 传统diff算法需要 ...

  4. 【React自制全家桶】二、分析React的虚拟DOM和Diff算法

    一.React如何更新DOM内容: 1.  获取state 数据 2.  获取JSX模版 3.  通过数据 +模版结合,生成真实的DOM, 来显示,以下行代码为例(简称代码1) <div id= ...

  5. 深入理解React:diff 算法

    目录 序言 React 的核心思想 传统 diff 算法 React diff 两个假设 三个策略 diff 具体优化 tree diff component diff element diff 小结 ...

  6. 虚拟DOM与diff算法

    虚拟DOM与diff算法 虚拟DOM 在DOM操作中哪怕我们的数据,发生了一丢丢的变化,也会被强制重建整预DOM树.这么做,涉及到很多元素的重绘和重排,导致性能浪费严重 只要实现按需更新页面上的元素即 ...

  7. 探究虚拟dom与diff算法

    一.虚拟DOM (1)什么是虚拟DOM? vdom可以看作是一个使用javascript模拟了DOM结构的树形结构,这个树结构包含整个DOM结构的信息,如下图:   可见左边的DOM结构,不论是标签名 ...

  8. react diff算法浅析

    diff算法作为Virtual DOM的加速器,其算法的改进优化是React整个界面渲染的基础和性能的保障,同时也是React源码中最神秘的,最不可思议的部分 1.传统diff算法计算一棵树形结构转换 ...

  9. Vue源码终笔-VNode更新与diff算法初探

    写完这个就差不多了,准备干新项目了. 确实挺不擅长写东西,感觉都是罗列代码写点注释的感觉,这篇就简单阐述一下数据变动时DOM是如何更新的,主要讲解下其中的diff算法. 先来个正常的html模板: & ...

随机推荐

  1. 28个HTML5特征、窍门和技术

    原文地址:  http://www.zhangxinxu.com/wordpress/2010/08/%E7%BF%BB%E8%AF%91-%E4%BD%A0%E5%BF%85%E9%A1%BB%E7 ...

  2. Arcgis ArcMap 10 如何生成msd地图文档定义【 arcgis mxd怎么转换成msd】

    .mxd是arcgis 的地图文档后缀名. .msd是arcgis 的地图服务定义文件,是 map service definition 的缩写. 基于 MSD 的服务支持 Maplex.制图表达和新 ...

  3. Linux下线程同步的几种方法

    Linux下提供了多种方式来处理线程同步,最常用的是互斥锁.条件变量和信号量. 一.互斥锁(mutex) 锁机制是同一时刻只允许一个线程执行一个关键部分的代码.  1. 初始化锁 int pthrea ...

  4. python将控制台输出保存至文件

    很多时候在Linux系统下运行python程序时,控制台会输出一些有用的信息.为了方便保存这些信息,有时需要对这些信息进行保存.这里介绍几种将控制台输出保存到文件中的方式:1 重定向标准输出流重定向标 ...

  5. oracle常用& to_date()怎么转换带am pm的时间格式

    Oracle一.字符函数--大小写转换函数1.LOWER (strexp)    返回字符串,并将所有的字符小写. select lower('ABCDE') from dual --输出empbai ...

  6. 由易信界面——谈谈fragment 状态的保存

    看看我要实现的效果: 其实,这种左右界面切换保存布局方式,不只是易信界面这么用罢了.这更是大多数app布局的主流,而在android平台上面,随着谷歌大力推荐fragment的使用,用fragment ...

  7. MFC增强----任务对话框CTaskDialog类

    /** 注意:从Windows Vista系统才开始支持CTaskDialog类,所以在使用时最好调用 CTaskDialog::IsSupported() 方法做判断 同时:CTaskDialog类 ...

  8. Laravel5.5 Jwt 1.0 beta 配置

    https://github.com/tymondesigns/jwt-auth/issues/860 1 下载开发者版本   image.png 修改composer.json,添加 "t ...

  9. C#获取程序启动目录

    //WCF service: string servicePath = System.Web.Hosting.HostingEnvironment.MapPath("~"); // ...

  10. linux下判断文件和目录是否存在[总结]

    1.前言 工作中涉及到文件系统,有时候需要判断文件和目录是否存在.我结合APUE第四章文件和目录,总结一下如何正确判断文件和目录是否存在,方便以后查询. 2.stat系列函数 stat函数用来返回与文 ...