使用动态规划 实现字符级Diff & Patch

文章开头先上demo,只需键入任意内容的两个字符串,页面上就能自动计算并呈现字符串之间的差分。html

demo地址:string-diff-demo.herokuapp.comgit

源码地址:github.com/lqt0223/str…github

动态规划

动态规划(dynamic programming)是你们在算法学习中都会遇到的话题之一。我我的对于它的理解是:算法

  • 动态规划针对的是规模较大的问题 
  • 但就像递归那样,问题的base case是有解的
  • 而且同一个问题的较大规模版本的解,能够经过一组规则,从已有的较小规模的同一问题的解中推导而出
  • 动态规划与递归不一样的是,后者是利用了方法定义了在其过程当中调用本身,“声明式”(declaratively)地造成了整个求解的过程;前者须要创建动态规划矩阵,用以记录每一问题规模下的解,并须要显式地执行循环来不断扩大问题规模和求解,这是“命令式”(imperatively)地形式了求解的过程

能够被动态规划解决的问题,常见的有:数组

  • 背包问题(knapsack problem):给定一个容量有限的背包,与一组带有重量和价值的物品,如何选择其中的几个放入背包使得价值的和最大
  • 最长公共序列问题(longest common sequence problem,下文简称为LCS问题):求两个字符串中,最长的公共序列。公共序列指的是在两个字符串中都出现的序列,这个序列在原字符串中不必定是连续的
  • 子集和问题(subset sum problem):给定一个整数集合,是否存在它的非空子集使子集内的数字和为0
  • ...

最长公共序列问题与Diff & Patch算法的关系

曾经,我本身在codewar等网站上作算法题时,不少次刷到"longest common sequence"或者相似的题目,也经过一些算法书了解到这类问题的一个比较易于理解的算法是动态规划。但我一直不太明白这类问题的实际应用何在。直到最近看到了下面的论文:浏览器

An O(ND) Difference Algorithm and Its Variations - EUGENE W. MYERS数据结构

此文我只读了其中的1-2节,总结一下它的内容实际上是:使用图算法,求解两个字符串之间的LCS,以及最短编辑步骤(shortest edit script,如下简称SES,指的是从字符串A变换至字符串B,所须要的步骤。步骤是针对字符串的操做,例如删除某一位置上的字符、在某一位置上插入字符等)。app

今后文可知:LCS和SES是对偶问题(dual problem),这两个问题只不过是一个优化问题的两个方面。即,当咱们寻找两个字符串的公共子序列时,若是已经找到了最优解(最长公共子序列),那么在此最优解情形下的两个字符串之间的编辑步骤,也就是最短编辑步骤。通俗地讲,求解LCS的过程当中,咱们就能够获得SES工具

因为SES描述了从一个字符串到另外一个字符串的一系列操做步骤,这就相似于各种数据比较工具产生的差量数据。因而咱们知道了,LCS问题的实际应用之一,就是数据的比较、差量计算和差量更新。学习

使用矩阵转化LCS和SES问题

因为是用动态规划来求解LCS和SES问题,咱们须要用到矩阵(二维数组)来记录最优解的一些信息。

这一小节主要是说明在使用矩阵求解以上问题的过程当中,矩阵有哪些性质,以及这些性质对应着LCS或SES问题的什么方面。这些内容也是对于上一小节中提到的论文第2节内容的概括和简化。

若是以前没有接触过使用动态规划求解LCS问题的话,能够看一下下面的视频,从而对于这一求解过程有一个基本概念。

Longest Common Subsequence - Tushar Roy - Youtube

整体来讲,使用矩阵转化并求解LCS和SES问题须要如下三个阶段:

  1. 初始化阶段。假设字符串A长度为m,字符串B长度为n,则初始化一个m + 1 * n + 1的矩阵,将矩阵的第一列和第一行都初始化为0(矩阵中后续须要填入的是LCS的长度,因此在初始化时,第一行或第一列表示两个字符串中的任意一个为空的状况,须要填入0)
  2. 推算阶段。从左至右从上到下,根据必定的推演规则,填写矩阵。即,不断地求解字符串A或B的前缀之间的LCS的长度
  3. 回溯(backtracking)阶段。当矩阵填满时,位于矩阵最右下角的值便是字符串A和B的LCS的长度。若是须要进一步找出LCS是什么,则须要从矩阵的右下角出发,按必定的规则,找到一个到达矩阵左上角的路径,保证通过路径时,LCS的长度值每次减少0或1。

通过三个阶段后,矩阵会变成相似下图的形式。

图中是字符串A为"abcabba",字符串B为"cbabac"时,使用动态规划求解LCS和SES造成的矩阵。由此矩阵咱们能够得出如下关于两个字符串之间的LCS和SES的相关答案:

  1. 两个字符串的LCS长度为4(即矩阵最右下角位置所填入的值)
  2. 两个字符串的LCS为"caba"(这是完成回溯后,经过观察红色箭头所造成的路径而得来;观察上图可知,回溯阶段时,每一次遇到须要向左上角移动的状况下,该坐标对应的字符串A内的某一字符与字符串B内的某一字符相同,即这个字符能够做为LCS的组成字符之一)
  3. 回溯时的每一次移动均可以映射为SES中的某一步:
    1. 向左上角移动,意味着找到了组成LCS的一个字符串,对于SES来讲,表示不须要操做
    2. 向左移动,对于SES来讲,意味着在字符串A中的指定位置删除字符
    3. 向上移动
      1. 若是是在矩阵的第1列(也就是所有被初始化为0的最左边一列)向上移动,对于SES来讲,意味着在字符串A的头部添加字符
      2. 若是是在矩阵的其余列向上移动,对于SES来讲,意味着在字符串A的指定位置的后面添加字符

例:字符串A为"abcabba",字符串B为"cbabac"时,如何知道通过什么样的步骤,能够最快地将字符串A变为字符串B呢?咱们可使用上面的规则,将红色路径翻译成咱们须要的SES

  1. 删除字符串A的第一、2个字符(最左上角的两个向左箭头)
  2. 在字符串A的第3个位置添加字符"b"(从左上至右下的第四个向上箭头) 
  3. 删除字符串A的第6个字符(从左上至右下的倒数第三个向左箭头)
  4. 在字符串A的第7个位置添加字符"c"(最右下角的向上箭头)

通过上述操做后咱们就能够将字符串A变换为字符串B

SES的同时操做问题

上一节的末尾给出了从"abcabba"到"cbabac"的SES,也许你试着用草稿纸或者其余工具来使用这段SES,但却没法顺利地完成字符串的转换。这是由于:SES所表示的编译步骤,须要被同时操做。这个说法比较抽象,下面使用"abcabba"到"cbabac"例子,说明SES的正确用法:

原字符串

a b c a b b a
复制代码
  1. 删除字符串A的第一、2个字符(最左上角的两个向左箭头)(这里用*标记将要被删除的字符)

    * * c a b b a
    复制代码
  2. 在字符串A的第3个位置添加字符"b"(从左上至右下的第四个向上箭头)

    * * c a b b a
          b
    复制代码
  3. 删除字符串A的第6个字符(从左上至右下的倒数第三个向左箭头)

    * * c a b * a
          b
    复制代码
  4. 在字符串A的第7个位置添加字符"c"(最右下角的向上箭头)

    * * c a b * a
          b       c
    复制代码
  5. 将以上相似于hashTable的结构还原为一个字符串,规则为:遇到须要删除的字符时则忽略,遇到纵向伸展的list时将其连缀为一个子字符串,最后将全部子字符串按顺序链接,即获得"cbabac"

由此可知,SES的同时操做,指的是任何一个操做步骤,都不该该影响到字符串最初的字符排列。咱们能够用这种纵向的数据结构,从新整理字符串操做,并在最后转换成目标字符串。

差分可视化

如上一小节所示,SES的应用之一就是直接执行,其结果就是生成目标字符串。

咱们也能够结合原字符串和SES,生成DOM String,在浏览器中将原字符串到目标字符串的差分呈现出来。本文开头的demo便是对于这种应用方式的展现。

后记

不只是字符级的diff & patch,若是在不考虑算法空间复杂度的状况下,动态规划也能够简单地实现单词级、行级的diff & patch。

学习和实现这个算法给我最大的体会是:

  • 使用图形化的表示和求解过程来转化问题,能让一些看似复杂的问题变得直观和简单(例如使用矩阵来记录和求解LCS)
  • 一些已经掌握的算法和算法思想,通过再思考,有时能获得意想不到的更大的收获
相关文章
相关标签/搜索
本站公众号
   欢迎关注本站公众号,获取更多信息