diff 是我们每天都要使用的一个功能,每次提交时,我都习惯先用 git diff --cached 看看这次提交更改了些什么,确定没问题,然后再 git commit。git 生成的 diff 非常直观,直观到我从来都没有去思考过 diff 是怎么生成的,觉得这应该是很简单的一件事,两个文件做个对比,不就行了。

什么是直观的 diff

我们先简单定义一下什么是 diff:diff 就是目标文本和源文本之间的区别,也就是将源文本变成目标文本所需要的操作。

git 为我们生成的 diff 是很直观易懂的,一看就知道我们对文件进行了哪些改动。但是,实际上,diff 生成是一个非常复杂的问题。

举个简单的例子,源文本为 ABCABBA,目标文本为 CBABAC,他们之间的 diff 其实有无穷多种(我们以字符为单位,一般情况下是以行为单位)。比如

1.  - A       2.  - A       3.  + C- B           + C           - AC             B             B- A           - C           - CB             A             A+ A             B             BB           - B           - BA             A             A+ C           + C           + C

上面三种都是有效的 diff,都可以将源文本变成目标文本,但是第二种和第三种没有第一种看起来“直观”。

所以,我们需要个算法,生成“直观”的 diff,怎么样才叫“直观”呢?

  • 删除后新增,比新增后删除要好,也就是说,上面的例子 2 比例子 3 看起来要直观

  • 当修改一块代码时,整块的删除然后新增,比删除新增交叉在一起要好,例如:

      Good: - one            Bad: - one- two                 + four- three               - two+ four                + five+ five                + six+ six                 - three
    

  • 新增或删除的内容应该和代码结构相呼应,例如下面的例子,左边我们可以很直观地看出新增了一个inspect 方法。

      Good: class Foo                   Bad:    class Foodef initialize(name)                def initialize(name)@name = name                        @name = nameend                             +   end+                                   ++   def inspect                     +   def inspect+     @name                         +     @name+   end                                 endend                                 end
    

除了直观以外,diff 还需要短,这一点是好理解的,我们希望 diff 反应的是把源文本变成目标文本需要用的最少的操作。

那么,现在的问题就是:怎样寻找最短的直观的 diff?

diff 与图搜索

”寻找最短的直观的 diff” 是一个非常模糊的问题,首先,我们需要把这个问题抽象为一个具体的数学问题,然后再来寻找算法解决。

抽象的过程交给算法科学家了,抽象的结果是:寻找 diff 的过程可以被表示为图搜索

什么意思呢?还是以两个字符串,src=ABCABBA,dst=CBABAC 为例,根据这两个字符串我们可以构造下面一张图,横轴是 src 内容,纵轴是 dst 内容。

那么,图中每一条从左上角到右下角的路径,都表示一个 diff。向右表示“删除”,向下表示”新增“,对角线则表示“原内容保持不动“。

比如,我们选择这样一条路径:

  1. (0, 0) -> (1, 0)
  2. (1, 0) -> (2, 0) -> (3, 1)
  3. (3, 1) -> (3, 2) -> (4, 3) -> (5, 4)
  4. (5, 4) -> (6, 4) -> (7, 5)
  5. (7, 5) -> (7, 6)

这条路径代表的 diff 如下。

- A
- BC
+ BAB
- BA
+ C

现在,“寻找 diff” 这件事,被抽象成了“寻找图的路径”了。那么,“最短的直观的” diff 对应的路径有什么特点呢?

  • 路径长度最短(对角线不算长度)
  • 先向右,再向下(先删除,后新增)

Myers 算法

Myers 算法就是一个能在大部分情况产生”最短的直观的“ diff 的一个算法,算法原理如下。

首先,定义参数 dk,d 代表路径的长度,k 代表当前坐标 x - y 的值。定义一个”最优坐标“的概念,最优坐标表示 d 和 k 值固定的情况下,x 值最大的坐标。x 大,表示向右走的多,表示优先删除。

还是用上面那张图为例。我们从坐标 (0, 0) 开始,此时,d=0k=0,然后逐步增加 d,计算每个 k 值下对应的最优坐标。

因为每一步要么向右(x + 1),要么向下(y + 1),对角线不影响路径长度,所以,当 d=1 时,k 只可能有两个取值,要么是 1,要么是 -1

d=1k=1 时,最优坐标是 (1, 0)

d=1k=-1 时,最优坐标是 (0, 1)

因为 d=1 时,k 要么是 1,要么是 -1,当 d=2 时,表示在 d=1 的基础上再走一步,k 只有三个可能的取值,分别是 -202

d=2k=-2 时,最优坐标是 (2, 4)

d=2k=0 时,最优坐标是 (2, 2)

d=2k=2 时,最优坐标是 (3, 1)

以此类推,直到我们找到一个 dk 值,达到最终的目标坐标 (7, 6)

下图横轴代表 d,纵轴代表 k,中间是最优坐标,从这张图可以清晰的看出,当 d=5k=1 时,我们到达了目标坐标 (7, 6),因此,”最短的直观的“路径就是 (0, 0) -> (1, 0) -> (3, 1) -> (5, 4) -> (7, 5) -> (7, 6),对应的 diff 如下。

- A
- BC
+ BAB
- BA
+ C

现在我们可以知道,其实 Myers 算法是一个典型的”动态规划“算法,也就是说,父问题的求解归结为子问题的求解。要知道 d=5 时所有 k 对应的最优坐标,必须先要知道 d=4 时所有 k 对应的最优坐标,要知道 d=4 时的答案,必须先求解 d=3,以此类推,和 01 背包问题很是相似。

实现

算法原理知道以后,实现便是一件简单的事情了,myers-diff 仓库是我使用 Go 实现的一个版本,基本流程如下:

  1. 迭代 d,d 的取值范围为 0 到 n+m,其中 n 和 m 分别代表源文本和目标文本的长度(这里我们选择以行为单位)
  2. 每个 d 内部,迭代 k,k 的取值范围为 -d 到 d,以 2 为步长,也就是 -d,-d + 2,-d + 2 + 2…
  3. 使用一个字典 v,以 k 值为索引,存储最优坐标的 x 值
  4. 将每个 d 对应的 v 字典存储起来,后面回溯的时候需要用
  5. 当我们找到一个 d 和 k,到达目标坐标 (n, m) 时就跳出循环
  6. 使用上面存储的 v 字典(每个 d 对应一个这样的字典),从终点反向得出路径

最后补充一句,Git 真正用的是标准 Myers 算法的一个变体。标准的算法有一个很大的缺点,就是空间消耗很大,因为我们需要存储每一个 d 对应的 v 字典。如果输入文件比较大,这样的空间开销是不能接受的。因此 Myers 在他的 论文 中,同时提供了一个算法变体,这个变体需要的空间开销要小得多。但是在某些情况下,变体产生的 diff 会和标准算法有所不同。也就是说,如果你按照上面的算法实现的程序,出来的结果和 git diff 的结果有所不同是正常的。

参考资料

  • The Myers diff algorithm: part 1

Git Diff 算法——Myers算法相关推荐

  1. Git是怎样生成diff的:Myers算法

    diff是我们每天都要使用的一个功能,每次提交时,我都习惯先用git diff --cached看看这次提交更改了些什么,确定没问题,然后再git commit.git生成的diff非常直观,直观到我 ...

  2. linux的diff工具原理之Myers算法

    linux的diff工具原理之Myers算法 前言 diff diff与图搜索 Myers算法 具体实现 1.为什么d的取值范围是[0,N+M]呢? 2.为什么k的取值范围为[-d,d],并且步长为2 ...

  3. git diff与linux diff的输出格式之unified format

    前言 前面有一篇文章<一个有些意思的项目--文件夹对比工具(一)>,里面简单讲了下diff算法之--Myers算法. 既然是算法,就会有实现,比如git diff中有Myers的实现,gi ...

  4. git diff的生成方式:myers diff算法

    概述 本文参考:https://blog.jcoglan.com/2017/02/12/the-myers-diff-algorithm-part-1/ 每个开发者多少都接触过git的diff功能,如 ...

  5. Git 常用操作(5)- git clone/git checkout -b/git diff/git push/git pull

    1. git clone--获取远程仓库 当 git fetch 命令从服务器上抓取本地没有的数据时,它并不会修改工作目录中的内容.它只会获取数据然后 让你自己合并. 然而,有一个命令叫作 git p ...

  6. 算法总结---最常用的五大算法(算法题思路)

    算法总结---最常用的五大算法(算法题思路) 一.总结 一句话总结: [明确所求:dijkstra是求点到点的距离,辅助数组就是源点到目标点的数组] [最简实例分析:比如思考dijkstra:假设先只 ...

  7. 生成树的概念,最小生成树Prim算法 Kruskal算法

    求解最小生成树可以用Prim算法 Kruskal算法

  8. 一文详解如何配置meld做git diff工具

    前言 在git中进行版本比较时,我们经常会使用git diff 版本1 版本1 进行比较两个版本之间的差异. 如果直接使用git diff 对于每一个变化的文件终端都会提示,(Y/n),处理起来较为麻 ...

  9. 期望最大化算法(Expectation-Maximum,简称EM)算法+EM算法+EM的应用

    期望最大化算法(Expectation-Maximum,简称EM)算法+EM算法+EM的应用 EM的应用 EM算法有很多的应用,最广泛的就是GMM混合高斯模型.聚类.HMM等等.具体可以参考Jerry ...

最新文章

  1. Zend Framework 跳转方法(render, forward, redirect)区...
  2. 初学spring(一)
  3. OpenID 和 OAuth 的区别及第三方登录的安全隐患分析
  4. python软件如何安装方法_【新手必看】Python软件下载及安装教程
  5. 【水】对于算法的个人理解
  6. yii 标签用法(模板)
  7. Android四大组件之Activity组件
  8. 【数字信号处理】基于matlab GUI数字信号处理系统【含Matlab源码 1088期】
  9. LTE 注网流程log分析
  10. C64x+中断控制器
  11. 助教日志_沈航软件工程评分1.2班第三周作业及总评成绩
  12. 变量消元(Varible Elimination)和概率边缘化(Marginalization)的关系
  13. grub rescue救援模式的处理
  14. 新车 合格证 二维码 解密
  15. android 利用shape做控件背景(小圆点,空心带边框背景)
  16. 第一章:自己动手写区块链之最小可行区块链
  17. MySQL 工作、底层原理
  18. 网络小贷风控有哪些数据接口?
  19. 基于微信小程序的微相亲平台的设计与实现
  20. 微信小程序页面间传值

热门文章

  1. 05-CSS溢出属性文本溢出省略号
  2. CSS 多行文字溢出显示省略号效果
  3. android+6原生壁纸,福利:全新一加 6手机原生壁纸 安卓旗舰的魅力 让人一见倾心!...
  4. 点云数据集ShapeNet
  5. 使用python获取邮箱邮件
  6. django项目配置使用elasticsearch搜索引擎
  7. elasticsearch搜索分数自定义以及相关度计算相关
  8. todo【面经】牛客网测试相关面试题
  9. SiLK Documentation 翻译
  10. 万字长文解读:从Transformer到ChatGPT,通用人工智能曙光初现