diff命令实现
diff是类UNIX系统下的一个重要的系统工具,用于比较两个文本文件的差异。
它有三种输出格式
先给大家看看两个用于比对的文件原文
file1:
a
e
b
a
g
h
b
g
g
file2:
b
c
d
g
e
g
j
h
格式一,普通格式:
$ diff file1 file2
1,6d0
< a
< e
< b
< a
< g
< h
7a2,3
> c
> d
8a5
> e
9a7,8
> j
> h
格式二,上下文格式:
*** file1 2021-09-20 11:58:40.660690542 +0800
--- file2 2021-09-20 11:58:50.369119254 +0800
***************
*** 1,9 ****
- a
- e
- b
- a
- g
- h
b
g
g
--- 1,8 ----
b
+ c
+ d
g
+ e
g
+ j
+ h
格式三,合并格式:
--- file1 2021-09-20 11:58:40.660690542 +0800
+++ file2 2021-09-20 11:58:50.369119254 +0800
@@ -1,9 +1,8 @@
-a
-e
-b
-a
-g
-h
b
+c
+d
g
+e
g
+j
+h
实现原理LCS
不管是哪一种输出,它们的核心原理是一致的
diff
的实现原理是最长公共子序列算法(LCS)
接下来,以上面的字符串为例,对该算法进行解析
aebaghbgg
bcdgegjh
首先,最长公共子序列算法和最长子串算法是不一样的两个算法。子串要求不仅先后顺序一致,还要前驱后继也一致,而最长公共子序列算法只要求先后顺序一致。
例如,aeba
是aebaghbgg
的一个子串,也是一个子序列,而abgg
是其的一个子序列,而非子串
公共子序列,就是同时是两个字符串的子序列的一段序列
因此,最长公共子序列,就是两个字符串之间先后顺序一致的最长的一段序列
例如,上面的两个字符串的一个最长公共子序列是egh
a e ba g h bgg
bcdg e g j h
知道了最长公共子序列后,我们就可以用最少的改动来使得两个文本一致
现在,我们对LCS算法进行一个理论的推导。
现在有两个字符串,一个字符串为p1p2p3p4...pm
,另一个为q1q2q3q4...qn
,设它们的一个最长子序列为r1r2r3r4...rk
可以显然得到以下几个性质:
k
的取值一定在0
~max(m, n)
之间,若两个字符串完全没有相同的字符,则其取值为0
,若顺序完全相同,则取值为字符串的长度- 若
pm
等于qn
,那么,rk
也一定等于pm
和qn
。因为最长公共子序列是最长的,而r
已经是一个最长公共子序列了 - 若
pm
不等于qn
,那么有这几种情况:
若
rk
不等于pm
,则r1r2r3...rk
是p1p2p3...p(m-1)
和q1q2q3...qn
之间的最长公共子序列。举例,acde
和abcd
,它们的最长公共子序列显然是acd
,第一个字符串的d
后面的e
就已经和最长公共子序列没关系了
同理,rk
不等于qn
时,r1r2r3...rk
是p1p2p3...pm
和q1q2q3...q(n-1)
之间的最长公共子序列
很显然,这个性质可以用于推导递归公式
我们设L(m, n)
是字符串p1p2...pm
和q1q2...qn
之间的最长子序列的长度
当m
为0或者n
为0的时候,即两个字符串有一个为空串,则显然没有最长公共子序列,L(0, 0)
就是0
当m
不等于0且n
不等于0时,若pm
等于pn
则,L(m, n)=L(m-1, n-1)
。继续拿上面的acde
和abcd
举例,现在它们的最长公共子序列是acd
,当我们在后面再添加一个一样的字符时,例如n
,则acden
和abcdn
的最长公共子序列就成了acdn
,长度增长了1
若pm
不等于pn
,要么L(m-1, n)
是最长的,要么L(m, n-1)
是最长的
因此,递推公式可以写为:
L(m, n) =
0, m == 0 || j == 0;
L(m - 1, m - 1) + 1, m > 0 && n > 0 && p[m] == q[n];
max(L(m, n - 1), L(m - 1, n)), m > 0 && n > 0 && p[m] != q[n]
以最开始的
aebaghbgg
bcdgegjh
举例
当m
为0或n
为0时,取值为0,有L(0...m, 0) = L(0, 0...n) = 0
当m
、n
均不为0时,以L(1...3, 0...n)
举例
a != b
, L(1, 1) = max(L(1, 0), L(0, 1)) = 0
;
a != c
, L(1, 2) = max(L(1, 1), L(0, 2)) = 0
;
…
a != h
, L(1, 8) = max(L(1, 7), L(0, 8)) = 0
;
e != b
, L(2, 1) = max(L(2, 0), L(1, 1)) = 0
;
…
e == e
, L(2, 5) = L(1, 4) + 1 = 1
;
e != g
, L(2, 6) = max(L(1, 6), L(2, 5)) = 1
;
e != j
, L(2, 7) = max(L(1, 7), L(2, 6)) = 1
;
…
b == b
, L(3, 1) = L(2, 0) + 1 = 1
;
b != c
, L(3, 2) = max(L(2, 2), L(3, 1)) = 1
;
…
将全部结果写作表格形式:
| |0 1 2 3 4 5 6 7 8
| | |b|c|d|g|e|g|j|h
-|-------------------
0| |0 0 0 0 0 0 0 0 0
|-
1|a|0 0 0 0 0 0 0 0 0
|-
2|e|0 0 0 0 0 1 1 1 1
|-
3|b|0 1 1 1 1 1 1 1 1
|-
4|a|0 1 1 1 1 1 1 1 1
|-
5|g|0 1 1 1 2 2 2 2 2
|-
6|h|0 1 1 1 2 2 2 2 3
|-
7|b|0 1 1 1 2 2 2 2 3
|-
8|g|0 1 1 1 2 2 3 3 3
|-
9|g|0 1 1 1 2 2 3 3 3
可得这两个字符串的最长公共子序列长度为3
但是,只知道长度是不够的,我们还需要获取对应的具体的子序列,才能知道哪些是需要更改的、哪些是维持原样就好的
我们可以借助上面的表格进行逆推
L(9, 8)=3
,查看发现g != h
,则它的值要么是从L(8, 8)
继承来的,要么是从L(9, 7)
因为L(8, 8) == L(9, 7) == 3
,因此,任意选一个方向进行回溯都是可以的
这里,我们统一选择向上的方向,即回溯至L(8, 8)
g != h
,重复上一步,回溯至L(7, 8)
b != h
,重复上一步,回溯至L(6, 8)
h == h
,因此,回溯序列的最后一位为h
L(6, 8)
的值来自于L(5, 7)
,因此,我们回溯至L(5, 7)
g != j
,回溯至L(5, 6)
g == g
,回溯至L(4, 5)
,回溯序列gh
a != e
,回溯至L(3, 5)
b != e
,回溯至L(2, 5)
e == e
,回溯至L(1, 4)
,回溯序列egh
再回溯,就全是空串了
因此,这两个字符串的一个最长公共子序列为egh
这只是一个结果,如果我们选择另一个方向进行回溯,结果又会发生不同
例如,我们若全部往左回溯,结果将是bgg
,正好就是diff