从diff到LCS(Longest common subsequence),抽象之美

熟悉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算法,可以发现,通过进行抽象,问题得到了更好的理解和建模。抽象之美:)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值