diff 是20世纪70年代初在Unix 操作系统上开发的一个文本比较工具。最终版本随1974年的Unix 第5版一起发行,由Douglas McIlroy完成。在1976年和James W. Hunt(开发了diff 的一个最初的原型)合写了这篇论文。
关于不同文件比较的算法
J. W. 哈特
加里福利亚,斯坦福大学电子工程系
M. D. 迈克罗伊
新泽西州,贝尔实验室
摘要
diff 程序能报告两个文件间的差异,并把这些差异用一个从任一文件变化到另一文件的列表表示出来。diff 能在合理的时间和空间内对某些典型的输入(比如由计算机维护或生成的文档之间的版本变动)进行检查。在真实测试中,时空利用率随文件的总长度而不同,但已经知道在最坏情况下会是这些文件长度的乘积。
diff的中心算法解决了最长公共子串问题,并将其用来找出不同文件间没有变化的行。通过匹配文件间某些关键的“候选词(candidate)”可以获得具有实用性的效率,这些候选词把文档打断,缩短了两个文件间原始片段的最长公共子串。为了获得更好的性能,使用了散列,等价类划分,折半查找以及动态存储分配等各种技术。
[本文档是从1976年7月的贝尔实验室计算机科学与技术第41号报告扫描下来的。文本用OCR(光学字符识别)转换并手工编辑。图片是重画的。但仍可能存在一些OCR 错误,尤其是在表格和等式中间。如果您发现了这些错误,请报告到[email protected]。]
diff 程序会生成一个表单,列出那些从一个文件变换到另一个文件时必须改变的行,反过来也一样。其思想来源于这几篇文章[I,2,7,8]。为了了解它是如何工作的,让我们来看一个例子。考虑两个文件,为了简单起见,把它们横向排列:
a
b
c
d
e
f
g
w
a
b
x
y
z
e
容易验证通过以下操作可以把第一个文件变成第二个文件,假想每个文件开头有一个第0行:
在第0行之后追加:
w,
把第3、第4行
c
d,
变为:
x
y
z,
删除6、7行:
f
g.
反过来也能由第二个变为第一个:
删除第1行:
w,
把第4到6行
x
y
z,
变为:
c
d,
在第7行之后追加:
f
g.
删除(delete)、置换(change)和添加(append)是仅有的三种diff 定义的合法操作。可以用首字母缩写(有点像qed 文本编辑器[3])表示这些操作,以一种可读的方式描述这种双向变化。交换’a’ 和’d’(译注:’a’ 表示append,’c’ 表示change,‘d’ 表示delete),以及第一个和第二个文件相应的变动行数,我们就可以得到逆向变换。用这种方式,源文件中的行用’<’ 标识,导出文件中的用’>’表示:
0 a 1,1
1,1 d 0
>w
<w
3,4 c 4,6
4,6 c 3,4
<c
<x
<d
<y
---
<z
>x
---
>y
>c
>z
>d
6, 7 d 7
7 a 6, 7
<f
>f
<g
>g
(译注:格式是,先声明对哪几行执行什么操作,接着是参数。比如3, 4 c 4, 6是说把源文件的3、4行变为4至6行。<c,<d等是操作的对象。)
用数学术语,diff 的目标就是报告从一个文件转换成另一个所需变动的最小行数。等价地说,就是未改变的最大行数,或者找出两个文件中出现的最长公共子串。
1. 解决最长公共子串问题
在已知的方法中,没有一个普遍适用的好办法解决最长公共子串问题。最朴素的想法是,逐行检查直到出现不一致,然后以某种方法向前搜索直到两者都遇到一对匹配的行,如此继续——把问题规约到实现‘以某种方式’,也不会有多大帮助。不管怎样,从实用的角度看,当不是满篇都是不一致的时候,从头(或尾)开始剥离匹配的行的这一步是迈对了。剥离策略可能碰到这个问题最头疼的部分——(非线性的)运行时间。
有一个非常简单的关于‘somehow’的启发,当文件之间存在很少的差异并且在一个文件中很少有重复的行时,这个策略会很管用。Johson 和其他人[1, 11]已经采用了这一策略。那就是:当遇到一个不一致,比较各个文件的头k 行和另一个文件从这个不一致起的k 行,k=1,2,…,直到发现一个匹配。在更困难的问题上,这方法严重不同步。为了限制时空消耗,k 通常是有限的,结果更长的变化的段落被阻止重新同步。
有一种简单的动态规划策略解决最长公共子串问题[4, 5]。记第一个文件的各行为Ai,i=1,…,m,第二个文件的为Bj,j=1,…,n。假设Pi,j为第一个文件的头i 行和第二个文件的头j 行的最长公共子串的长度。很显然Pi,j满足
Pi,0=0 i=0,…,m,
P0,j=0 j=0,…,n,
则Pm,n就是我们要求的最长公共子串的长度。从生成Pm,n的整个Pi,j矩阵中,可以很容易地恢复出最长公共子串。
不幸地是,动态规划至少需要O(mn) 规模的时间,更糟地是需要O(mn)规模的空间。差异矩阵的每一行Pi 由前一行Pi-1决定,D. S. Hirschberg 发明了一种聪明的策略在O(n)的空间内计算Pm ,而且不需要额外的空间恢复最大公共子串,恢复过程花费的时间同找到Pm 一样多[6]。
diff算法通过关注关键匹配(essential matches)改进了简单动态规划,关键匹配有可能改变P。关键匹配,当Ai=Bj和Pi,j>max(Pi-1,j, Pi,j-1)时又被Hirschberg[7] 称为’ k-candidates’。一个’ k-candidate’是一个索引对(i, j)使得(1) Ai=Bj, (2)在第一个文件的头i个元素和第二个文件的头j个元素存在一个长度为k 的最长公共子串,(3)在少于i 或j 的子串中不存在长度为k 的公共子串。一个候选(candidate)是某个k-candidate。显然,一个最长公共子串肯定在包含所有候选的名单中。
如果 (i1,j1) 和 (i2,j2) 都是k-candidates,而且i1<j2那么j1>j2。如果j1=j2,(i2,j2) 将会违背上面定义的条件(3);而如果j1<j2以 (i1,j1) 结尾的长为k 的公共子串可以扩展为以 (i2,j2) 结尾的长为k +1 的公共子串。
候选算法有一个简单的图像解释。图1 把的所有格点都标上了黑点。这些点其实描述了一种等价关系,图中任意两条水平线或铅垂线的焦点处要么没有公共字符,要么字符完全一样。一个公共子串可以看作一条把公共点穿在一起的严格单调递增的曲线。下图中有四条这样的曲线(译注:从左上至右下依次为abc, aba, baba, cba)。这些特殊曲线上的点都是候选点。相同k 值的候选点构成一条k-截断曲线(译注:图中虚线标识的曲线)。所有这些用虚线标识的曲线必定都是单调递减的。除了在某些平凡的例子中,候选的数目明显少于mn。而在实际的文件比较中会更少,所以,候选列表通常能很容易存储。
2. diff的细节
图1的点可以用下面的方法在线性空间内存储下来:
(1) 在第二个文件构造元素的等价类列表。这些列表占用O(n)的空间。它们可以通过对第二个文件的各行排序得到。
(2) 把第一个文件的各个元素与合适的等价类联系起来。这种关联需要O(m)的存储空间。事实上,我们有了一个各个垂直方向的点的列表。
有了这些准备,我们就可以从左至右地生成候选了。记K 为指定的各个k 的已经发现的最右k-candidate。为了简化接下来的讨论,用一个伪的0-candidate 填充这个向量,为那些还没有候选的k 填充一个伪的 “篱笆(fence)”候选(m+1, n+1),用来和其他候选比较。K 开始时为空,除了填充,向右移动时更新。在处理了第四列以后,在图1 中标’a’,最靠右的一列候选为
(0,0) (3,1) (4,3) (4,5) (8,7) (8,7) ...
Now a new k-candidate on the next vertical is the lowest dot that falls properly between the ordinates of the previous (k -1)- and k-candidates. Two such dots are on the 5th vertical in Figure I. They displace the 2-candidate and 3-candidate entries to give the new vector K:
现在,在下一列中,新的k-candidate 是正确落在前(k-1)-candidate 和k-candidate 之间的纵坐标的最小的点。图1 中第五列上就有两个这样的点。它们取代2-candidate 和3-candidate 中原来的候选形成新的K 向量:
(0,0) (3,1) (5,2) (5,4) (8,7) (8,7) ...
第六列有两个点(译注:(6, 2)和(6, 4))落到,而不是落在这个名单的坐标之间,因此不是候选。每一个新的k-candidate 都被之前的(k-1)-candidate 束缚,便于以后恢复最长公共子串。更多细节见附录。
在一个给定的列上,候选的决定是一个专门的合并把那一列上的点合并进当前最靠右的候选名单。当点的数目是O(1) ,在这个最多min(m, n)表单上二分搜索会用O(logm)的时间执行合并。既然实际中大多数情况是各列很少的点,我们用二分搜索分别合并各个点,即使最坏情况下处理一列的时间变成了O(nlogm),而不是平常的O(m+n)。
3. 散列
为了能在随机访问存储器中比较(几千行的)大文件,diff 把每一行散列为一个机器字。这可能导致某些不等的行变得相等。假设这个hash 函数确实是随机的,一对给定的比较伪相等的概率将会降到1/M,hash 值的范围是1到M。当k << M 时,由hash 值决定的长度为k 的最长公共子串可期望包含k/M 的伪匹配,所以长为k 的序列有k/M 的概率成为一个伪‘jackpot’序列。在我们的16位机器上,一个5000行的文件出现这种头奖的概率低于10%,而一个500行的文件少于1%。
diff避免在检查原文件中传说的最长公共子串的头奖。在伪相等被删掉之后留下来的被作为答案接受,即使有小概率它的确不是一个最长公共子串。diff会公布jackpot,所以这些案例趋向于相当仔细地检查。在两年中,我们曾经把我们的注意力放在一个编辑过的最长子串jackpot 上,在那个实例中比另一个短。
复杂度
最坏情况下,diff算法并不比普通的动态规划好多少。由第2节知,它遵从最坏情况下的时间复杂度由合并决定,事实上,它是O(mnlogm) (虽然可以达到O(m(m+n)))。最坏情况下的空间复杂度由候选名单所需空间决定,在比较两个文件时至多为O(mn)。
a b c a b c a b c ...
a c b a c b a c b ...
图2暴露了一个问题。当m = n时,风筝形状的区域占了整个区域的1/2,(渐进的)筝形区域中1/3的格点是候选,所以候选数渐进地接近n2/6。*
在实际中,diff工作得比可能的最坏情况要好得多。只有很少的情况发现超过min(m, n) 个候选。事实上,一个带朴素的存储分配算法的早期版本只提供n个候选在用了两个月以后才第一次溢出,这期间它可能运行了100次以上。这样,我们有了一个很好的证据表明diff 在大部分实例中只需要线性空间。
至于实际的时间复杂度,diff 的核心算法是如此的快以至于在最大的实例中我们的实现能处理(大约3500行)将近一半的时间被用于简单的字符处理,如hashing,检测jackpot 等,它与两个文件的字符总数呈线性关系。在一台PDP11/45 机器上比较3500行文件的典型时间为1/4到3/4个cpu 分钟。相反地,一个Hirschberg 的动态规划算法[6]的加速版本在3500行规模的文件上要花费5个cpu 分钟。在第1节开头描述的启发式算法在同样长但平凡的文件上一般要比diff 快2到3倍,但在更复杂的实例中丧失更多优势。既然这两个程序的失效模式相当不同,它们在手边都是很有用的。
--------------------------------------------------------------------------------
直接计算知,当m - 1 和 n – 1 都不是2的因数时,有floor((4mn-m2-n2+2m+2n+6)/12) (译注:floor 为下取整)个候选。当n - 1 and m – 1 是6的倍数时,下限是精确的。
参考文献
[1] S. C. Johnson, ‘ALTER − A Comdeck Comparing Program,’ Bell Laboratories internal memorandum 1971.
[2] Generalizing from a special case solved by T. G Szymanski[8], H. S. Stone proposed and J. W. Hunt refined and implemented the first version of the candidate-listing algorithm used by diff and embedded it in an older framework due to M. D. Mcllroy. A variant of this algorithm was also elaborated by Szymanski[10]. We have had many useful discussions with A. V. Aho and J. D. UIlman. M. E. Lesk moved the program from UNIX to OS/360.
[3] ’Tutorial Introduction to QED Text Editor,’ Murray Hill Computing Center MHCC-002.
[4] S. B. Needleman and C. D. Wunsch, ’A General Method Applicable to the Search for Similarities in the Amino Acid Sequence,’ J Mol BioI 48 (1970) 443-53.
[5] D. Sankoff, ’Matching Sequences Under Deletion/Insertion Constraints’, Proc Nat Acad Sci USA 69 (1972) 4-6.
[6] D. S. Hirschberg, ’A Linear Space Algorithm for Computing Maximal Common Subsequences,’ CACM 18 (1975) 341-3.
[7] D. S. Hirschberg, ’The Longest Common Subsequence Problem,’ Doctoral Thesis, Princeton 1975.
[8] T. G Szymanski, ’A Special Case of the Maximal Common Subsequence Problem,’ Computer Science Lab TR-170, Princeton University 1975
[9] Michael L. Fredman, ’On Computing the Length of Longest Increasing Subsequences,’ Discrete Math 11 (1975) 29-35.
[10] T. G. Szymanski, ’A Note on the Maximal Common Subsequence Problem,’ submitted for publication. [The paper finally appeared as H. W. Hunt III and T. G. Szymanski, ‘A fast algorithm for computing longest common subsequences’, CACM 20 (1977) 350-353.]
[11] The programs called proof, written by E. N. Pinson and M. E. Lesk for UNIX and GECOS use the heuristic algorithm for differential file comparison.