文本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/
文本diff算法Patience Diff的更多相关文章
- 动态规划处理diff算法 Myers Diff (正向)
Eugene W. Myers 在他1986年发表于"Algorithmica"的论文"An O(ND) Difference Algorithm and Its Var ...
- diff算法深入一下?
文章转自豆皮范儿-diff算法深入一下 一.前言 有同学问:能否详细说一下 diff 算法. 简单说:diff 算法是一种优化手段,将前后两个模块进行差异化比较,修补(更新)差异的过程叫做 patch ...
- React Diff算法一览
前言 diff算法一直是React系统最核心的部分,并且由于演化自传统diff,使得比较方式从O(n^3)降级到O(n),然后又改成了链表方式,可谓是变化万千. 传统Diff算法 传统diff算法需要 ...
- 【React自制全家桶】二、分析React的虚拟DOM和Diff算法
一.React如何更新DOM内容: 1. 获取state 数据 2. 获取JSX模版 3. 通过数据 +模版结合,生成真实的DOM, 来显示,以下行代码为例(简称代码1) <div id= ...
- 深入理解React:diff 算法
目录 序言 React 的核心思想 传统 diff 算法 React diff 两个假设 三个策略 diff 具体优化 tree diff component diff element diff 小结 ...
- 虚拟DOM与diff算法
虚拟DOM与diff算法 虚拟DOM 在DOM操作中哪怕我们的数据,发生了一丢丢的变化,也会被强制重建整预DOM树.这么做,涉及到很多元素的重绘和重排,导致性能浪费严重 只要实现按需更新页面上的元素即 ...
- 探究虚拟dom与diff算法
一.虚拟DOM (1)什么是虚拟DOM? vdom可以看作是一个使用javascript模拟了DOM结构的树形结构,这个树结构包含整个DOM结构的信息,如下图: 可见左边的DOM结构,不论是标签名 ...
- react diff算法浅析
diff算法作为Virtual DOM的加速器,其算法的改进优化是React整个界面渲染的基础和性能的保障,同时也是React源码中最神秘的,最不可思议的部分 1.传统diff算法计算一棵树形结构转换 ...
- Vue源码终笔-VNode更新与diff算法初探
写完这个就差不多了,准备干新项目了. 确实挺不擅长写东西,感觉都是罗列代码写点注释的感觉,这篇就简单阐述一下数据变动时DOM是如何更新的,主要讲解下其中的diff算法. 先来个正常的html模板: & ...
随机推荐
- Go语言之进阶篇连接mysql
一.Go连接mysql 1.mysql驱动 地址:https://github.com/Go-SQL-Driver/MySQL 说明: sql.Open()函数用来打开一个注册过的数据库驱动,Go-M ...
- 在ASP.NET MVC中以post方式传递数组参数的示例【转】
最近在工作中用到了在ASP.NET MVC中以post方式传递数组参数的情况,记录下来,以供参考. 一.准备参数对象 在本例中,我会传递两个数组参数:一个字符串数组,一个自定义对象数组.这个自定义对象 ...
- ArcGisServer根据最大最小坐标换算瓦片行列号【转】
1.前言 在上一节中我们知道了屏幕上一像素等于实际中多少单位长度(米或经纬度)的换算方法,而知道这个原理后,接下来我们要怎么用它呢?它和我们前端显示地图有什么关联呢?这一节,我会尽量详细的将这两个问题 ...
- ASP,ASP.net,JSP语法、内置对象对比
1 各自的HelloWord版本 1.1 ASP <% Response.Write("hello asp") %> 文件名为test.asp. 1.2 ASP.ne ...
- Nginx如何启用ETag,提高访问速度
ETag全称EntityTags,HTTP协议规格说明中定义“ETag”为“被请求变量的实体值”. 我们也可以把ETag理解为是一个客户端与服务器关联的记号.这个记号告诉客户端,当前网页在上次请求之后 ...
- 定时删除日志文件---linux定时清理日志
linux是一个很能自动产生文件的系统,日志.邮件.备份等.虽然现在硬盘廉价,我们可以有很多硬盘空间供这些文件浪费,让系统定时清理一些不需要的文件很有一种爽快的事情.不用你去每天惦记着是否需要清理日志 ...
- Android Studio中实现AIDL
AIDL 先来两个传送门: http://www.cnblogs.com/yydcdut/p/3961545.html Android面试,与Service交互方式 http://www.cnblog ...
- AAAI 2018 论文 | 蚂蚁金服公开最新基于笔画的中文词向量算法
AAAI 2018 论文 | 蚂蚁金服公开最新基于笔画的中文词向量算法 2018-01-18 16:13蚂蚁金服/雾霾/人工智能 导读:词向量算法是自然语言处理领域的基础算法,在序列标注.问答系统和机 ...
- 条件随机场 (CRF) 分词序列谈之一(转)
http://langiner.blog.51cto.com/1989264/379166 原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.ht ...
- 迅为4412开发板Linux驱动教程——编写简单应用调用驱动
Linux驱动教程:http://pan.baidu.com/s/1c0hljUS 编写简单应用调用驱动--头文件 • 打印头文件 – include <stdio.h>调用打印函数pri ...