熟悉linux的朋友,对diff这个工具一定不会陌生。diff可以用来比对两份文件的异同。而在cvs svn这种版本控制系统中,diff更是发挥着重要作用 。
由于同一个项目有多个子版本,所以某一个子版本在进行了一些bug修复后,想把同样的修复应用到其它的版本上。使用cvs不知道是不是支持这种功能。所以,自己想写一个脚本,来将一些改动自动应用。
首先,想自己动手实现diff。
最初的想法是这样的:
1. 将2份文件的所有行读取到两个list中,src与tar,每个list设置2个游标,分别是Current 和 position,代表当前处理的行数以及在行与行不匹配的情况下,进行超前搜索。
2. 逐行进行比较
2.1 如果src.current == tar.current,表示该行没有改动,Current++
2.2 如果src.current != tar.current,表示两行不同
先对tar进行超前搜索,判断是否存在一行,与src.current相同
tar.position = tar.current+1
while tar.psotion<tar.size
if tar.postion == src.current
那么在tar.current 与tar.postion之间的行,就可以认为是tar文件比src文件多的行数
tar.current=tar.postion+1
src.current++
break
tar.postion++
如果在对tar文件搜索完全部行数后,仍未能找到一行满足tar.postion==src.current
那么就对src进行超前搜索,来判断是否存在src.postion = tar.current
while src.postion<src.size and tar.postion == tar.size <---表示tar进行了完全搜索,但为匹配
处理与对tar的处理一致
最后,如果对src进行了全部搜索,也没有发现匹配行,那么就说明src.current 与 tar.current 是对应行,但是有一行内容发生了变化
if src.postion == src.size
src.current 是内容变化的行,不是新加入的行
首先,这个算法肯定是有bug的。这是对文本差异进行搜索时的第一时间的想法,就是对文本逐行扫描。在现实测试中,这个算法还是能够处理大部分情况的,但是,还是有错误,比如,假如对下面的文本进行处理:
hello; hell;
you; you;
are; are;
great; hello;
great;
按照算法描述,最后会得出这样的结论,右侧的文本比左侧的文本多了hell;you;are;行,少了hello与great间的you;are;行。虽然,这样找出的也是两行文本的差异,但是,确切的说,这不是最好的答案,最起码,两个文件的差别没有那么大,仅仅是hello->hell和多了另一个hello。
所以,最开始想到的算法是不完备的,同时,也是混乱的,含着想当然的意味。
加入脱离现实业务本身,来对它进行一下抽象,我们会得到更好的解决方案。如果每一行只有一个字母的话,那么,就相当于寻找两个字符串的差异了。换句话说,要找出公共字符串的补集。然而,像 A-A^B 和 B-A^B这种集合运算,是不合适的,因为我们的 差异要涉及到顺序的概念。其实这就是lcs问题了。
所谓lcs,就是最长公共子串。子串的概念一定要清晰,就是对于一个串s0,s1...sn,保持顺序不变,从中抽取x个元素组成的新串。最长公共子串也就好理解了。有关概念问题,请参考最长公共子串。
重新回到文件上来,只要我们能够知道两份文件的最长公共内容,我们就很容易的可以得到两份文件存在的差异了。最长公共子串的实现算法,使用递归可以更好的进行描述。
对于两个串A和B,假如A和B的最后一个元素相同,那么,这个元素必然在公共子串中。去掉A与B的最后一个元素,再对新串的最后一个元素对比,相同即去掉,直到发现不相同的元素。对于不相同的元素,我们先去掉A的最后一个元素,让新串与B进行递归处理,得到A‘与B的最长子串,然后去掉B的最后元素,让A和B’递归处理,得到A和B‘的最长子串,取这两个子串中较长的一个,就是A、B在最后一个元素不同时的最长子串。很显然,递归的终止条件是A和B有一个的长度变为0
Python的实现如下:
def lcs2(seqx,seqy):
if len(seqx) == 0 or len(seqy)==0:
return []
if seqx[-1:] == seqy[-1:]:
result = lcs2(seqx[:-1],seqy[:-1])
result.append(seqx[-1:][0])
return result
else:
t1 = lcs2(seqx[:-1],seqy)
t2 = lcs2(seqx,seqy[:-1])
t = t1 if len(t1)>len(t2) else t2
return t
这个递归调用,显然存在着非常大的冗余。和计算Fibonacci数列一样,每一个元素都会被计算多次。所以, 原始算法的效率非常低,对于一个n-n类型的串比较,最坏要发生n*∑2^(i-1)*i 1<=i<=n 次比较,复杂度为O(n^3*2^n)。如果我们能够避免重复的对某一已知情况进行求解,就可以大大简化这个过程。采用动态规划算法时,需要一个n*m的表,来保存所有已知的解。比如,我们从A0与B0开始,表示两个串均为空串。那么A2与B串的公共串,其实就是在已知A1与B’的公共串的基础上,如果A2的最后一个元素与B最后一个相同个,那么就是A1与B‘的公共串再添加上最后A2最后一个,否则,就是A1与B的和A2 B’的公共传中最长的一个。
Python实现:
def lcs3(seqx,seqy):
table = []
for i in range(len(seqx)+1):
table.append([0]*(len(seqy)+1))
for xline in xrange(1,len(seqx)+1):
for yline in xrange(1,len(seqy)+1):
if seqx[xline-1] == seqy[yline-1] :
table[xline][yline]=table[xline-1][yline-1]+1
else:
table[xline][yline] = table[xline][yline-1] if table[xline][yline-1]>table[xline-1][yline] else table[xline-1][yline]
时间复杂度就是m*n,即n^2
此时,得到的table表,每一个表格的行号和列号对应了序列中的字符位置,(x,y)中的值就是长度为x和y的两个串的最长公共子串的长度。
通过最开始的文件差异对比,到后来的LCS算法,可以发现,通过进行抽象,问题得到了更好的理解和建模。抽象之美:)