首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Git Diff 算法详解:Myers Diff Algorithm

Git Diff 算法详解:Myers Diff Algorithm

作者头像
WEBJ2EE
发布于 2023-12-04 05:34:56
发布于 2023-12-04 05:34:56
3.9K14
代码可运行
举报
文章被收录于专栏:WebJ2EEWebJ2EE
运行总次数:4
代码可运行

Diff日常

作为一个PROGRAMMER,可能每天你都在使用 GitSVN 管理你所参与项目的代码。每当你提交自己修改后的代码、复读同事写的程序或排查程序异常行为的时候,比较和阅读两个版本代码之间的差异是必不可少的工作。

下图是 GitHub Desktop 展示的代码比对结果,我们可以很容易地看出:

  • 左侧代表“旧”代码,右侧代表“新”代码。
  • 左侧红色区域代表我们删除的代码,而右侧绿色区域代表我们新插入的代码

“代码比对”是 Git、SVN 这类代码版本管理软件中的基本的能力,也是最重要的能力。🚀️ (PS:代码合并的前提就是代码比对👀️ 。)

你知道“代码比对”是怎么做到的吗?😄 (PS:如果你心里已经有答案了,那就不用继续读这篇文章了👀️。)

Git内置的Diff算法

Git 是目前主流的版本控制系统,它的代码比对能力由 Git 内部的比对(Diff)算法实现。

Git 内置有 4 种 Diff 算法,即 Myers,Minimal,Patience 和 Histogram。其中 Myers 是 Git 使用的默认比对算法。我们可以在执行比对命令 git diff 时,通过参数 --diff-algorithm 指定比对算法。

下面,本文将选择 Git 的默认比对算法 Myers,为大家进行详细讲解。

Myers算法简介

Myers Diff Algorithm 是 Git 中的默认代码比对算法,由 Eugene W. Myers 在 1986 年发表。该算法的特点是运行速度快、生成的比对结果可读性强。

Myers 算法本质上是一个动态规划(Dynamic Programming,DP)算法,是一个“最短编辑距离(Minimum Edit Distance)”算法,也是一个“最长公共子序列(Longest Common Subsequence,LCS)”算法。

搞明白 Myers 算法不仅有利于你了解 Git 的底层工作原理,也有助于为你在解决一些工作中遇到的某些问题时提供灵感。

Myers 算法的原文很精练(如下图),想快速搞明白该算法的运行原理有一定难度。我用了两个周时间,反复读了好几遍这篇论文,并用 JavaScript 实现了一遍这个算法。我写这篇文章,一方面是想跟大家分享一下我对 Myers 算法的认识和理解,更重要的是希望能帮正在阅读本文的同学快速以更短的时间掌握 Myers 算法。

我们从论文中的例子开始,思考一下😄 。

主问题1:如何计算下面两个字符串 a 与 b 之间的“差异”?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
a = ABCABBA
b = CBABAC

子问题1.1:什么是“差异”?

答:“差异”是指,我们通过对字符串 a 中的字符进行一系列怎样的操作(注意:只有删除操作、插入操作),能把字符串 a 转换成 字符串 b。

举例子1.1:

一个最简单的“差异”就是,把字符串 a 中的字符逐个删除,然后把字符串 b 中的字符逐个插入。

很显然,这种“差异”对我们来说没有什么意义。想象一下,如果你在比对昨天和今天写的代码差异的时候,版本管理软件告诉你,“差异”是把昨天的代码全部删除、然后把今天的代码全部插入,完全没有意义好嘛😄 。

举例子1.2:

下图也是字符串 a、b 间的一种“差异”。它比上1.1版好很多,有点接近使用 Git、SVN 做比对的效果了。

因为它告诉我们,想要从字符串 a 变成字符串 b:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
a = ABCABBA
b = CBABAC

我们可以先从 a 字符串中删除开头的字符 A、B,然后保持字符 C 不动,然后插入字符 B,然后保持字符 A、B 不动,然后删除字符 B,然后保持字符 A 不动,最后插入字符 C。

举例子1.3:

下图所展示的也是字符串 a、b 间的“差异”,它们都是的。

主问题2:什么样的“差异”更“好”?

我们一直在使用 Git 或 SVN 提供的代码比对功能,我们对“好”的差异比对结果有一种直觉,那就是“把修改过的内容原原本本地展示出来”。

但是从上面的例子中可以看出,对于 a、b 两个字符串间的“差异”,存在多种解释方式,那么我们应该选择什么样的解释,才能够贴近我们对“好”的理解。

在这里我们可以先总结几条规则:

  • 涉及变动的部分尽量少(即:删除、插入操作尽量少)。
  • 我们习惯看到,插入的部分在删除的部分后面。
  • 我们习惯看到,成块的代码被删除或者被插入,而不是代码行交错地删除或插入。
  • 我们习惯看到代码比对结果中所展示的“删除的代码”和“插入的代码”与我们的想法保持一致。

Myers 算法就是这样一种算法,它的运行速度快,而且能够在绝大多数场景下,产出较的代码比对结果。

Myers算法流程

还是从论文中的例子开始,使用 Myers 算法找出下面字符串 a、b 间的差异。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
a = ABCABBA
b = CBABAC

不妨先看一下答案,下图即为 Myers 算法给出的差异比对结果😄 。

ok,进入正题。

图表示法

我们使用一张图(下图),对 a、b 两个字符串间的关系进行表达:

  • 字符串 a,排列在水平方向上。
  • 字符串 b,排列在竖直方向上。
  • 起点(0, 0),代表我们还没有对字符串 a、b 进行任何编辑操作(删除、插入、保持不动)。
  • 图中的任一条线段,代表一个编辑操作
    • 水平线段:表示删除字符串 a 中的一个字符(线段终点所指字符)。例如:从 (0, 0) 移动到 (1, 0),代表删除字符串 a 中的字符 A。
    • 竖直线段:表示插入字符串 b 中的一个字符(线段终点所指字符)。例如:从 (0, 0) 移动到 (0, 1),代表插入字符串 b 中的字符 C。
    • 对角线段:保持 a、b 对应位置上的字符不动(因为同时删除、插入相同的一个字符)。例如:从 (2, 0) 移动到 (3, 1),代表保持字符 C 不变。
  • 一步只能走一条线段(水平或竖直)
  • 从起点(0, 0)一步一步走到终点(7, 6),代表我们找到了一种将字符串 a 转换为字符串 b 的编辑序列,也即找到了一种字符串 a、b 间的“差异”。

举个例子:下图即代表从逐个删除字符串 a 中的所有字符,然后再逐个插入字符串 b 中的所有字符。

举个例子:下图就是 Myers 算法找到的将字符串 a 转换为字符串 b 的编辑序列。

小结一下:从起点(0, 0)到终点(7, 6)的任意一条路径,都是对字符串 a、b 差异的一种解释,即都是一种将字符串 a 转换为字符串 b 的编辑序列。Myers 算法的目标就是找到从起点(0, 0)到终点(7, 6)的一条最优路径。

流程模拟

接下来,我们来模拟一下 Myers 算法的流程,找到从起点(0, 0)到终点(7, 6)的一条最优路径。

  • (0, 0)
    • 可以选择右移一步到(0, 1)
    • 也可以选择下移一步到(0, 1)
    • 从(0, 0)点出发,移动一步,有两种可能
  • (0, 1)(1, 0)
    • 向右移动到(2, 0),由于遇到对角线,可以继续移动到(3, 1)
    • 向下移动到(1, 1),由于遇到对角线,可以继续移动到(2, 4)
    • 向右移动到(1, 1),由于遇到对角线(它不产生编辑操作),所以可以直接移动到(2, 2)
    • 向下移动到(0, 2),由于遇到对角线(它不产生编辑操作),所以可以直接移动到(2, 4)
    • 从(0, 1)出发,有两种可能
    • 从(1, 0)出发,有两种可能
    • 需要注意,(2, 4)可以从(0, 1)或(1, 0)移动过来,因为我们偏向于删除操作优先,所以我们优先选择(2, 4)从(1, 0)移动过来。
  • (2, 4)(2, 2)(3, 1)
    • 向下移动到(3, 2),由于遇到对角线,可以继续移动到(5, 4)
    • 向又移动到(4, 1),由于遇到对角线,可以继续移动到(5, 2)
    • 向右移动到(3, 2),由于遇到对角线,可以继续移动到(5, 4)
    • 向下移动到(2, 3)
    • 向右移动到(3, 4),由于遇到对角线,可以继续移动到(4, 5)
    • 向下移动到(2, 5),由于遇到对角线,可以继续移动到(3, 6)
    • 从(2, 4)出发,有两种可能
    • 从(2, 2)出发,有两种可能
    • 从(3, 1)出发,有两种可能
    • 需要注意,(5, 4)可以从(2, 2)或(3, 1)移动过来,因为我们偏向于删除操作优先,所以我们优先选择(5, 4)从(3, 1)移动过来。
    • 同时注意,到目前为止一共走了 3 步,从(2, 2)往下一步走到(2, 3)是在所有走了 3 步的路径中,距离终点(7, 6)最远的一条路线,基本可以忽略。
  • (3, 6)(4, 5)(5, 4)(5, 2)
    • 向右移动到(6, 2),遇到对角线,到(7, 3)
    • 向下移动到(5, 3),遇到对角线,到(7, 5)
    • 向右移动到(6, 4),遇到对角线,到(7, 5)
    • 向下移动到(5, 5)
    • 向右移动到(5, 5)
    • 向下移动到(4, 6)
    • 向右移动到(4, 6)
    • 已经到底了,不能向下移动了
    • 从(3, 6)出发
    • 从(4, 5)出发
    • 从(5, 4)出发
    • 从(5, 2)出发
    • 同理,优先选择(4, 6)从(4, 5)过来;
    • 同理,优先选择(5, 5)从(5, 4)过来;
    • 对于(7, 5),由于从(5, 4)出发是右移,而从(5, 2)出发是下移,所以优先选择从(5, 4)出发。
  • (4, 6)(5, 5)(7, 5)(7, 3)
    • 从图中可以直接看出,(7, 5)往下走一步即到达终点(7, 6)
    • 其他的点再走一步,不可能到达终点,所以不予讨论了。

到此,从起点(0, 0)走了 5 步,到达了终点(7, 6)。图中粉色路径就是 Myers 算法找出的字符串 a、b 之间的最短编辑距离,也就是 Myers 算法找出的 a 、b 字符串间的差异。

回顾一下找到这条最短路径的过程:

  • 这条路径用了最短的步数(5步)。
    • 即:这条路径包含了最少数量的横向(代表删除操作)和竖向线段(代表插入操作)。
    • 即:这条路径利用了最多的对角线段(因为它代表没有操作,即保持字符不动)。
  • 这条路径偏好横向移动(因为我们偏好删除操作优先)。

Myers算法理论

接下来,我们从理论层面,对 Myers 算法进行分析。

再重新回顾一下图中的几个概念:

  • 移动一格
    • 向右移动一格,代表删除操作,删除字符串 a 中的一个字符(终点位置指向的字符)。
    • 向下移动一格,代表插入操作,插入字符串 b 中的一个字符(终点位置指向的字符)。
    • 对角移动一格,代表没有操作,既不删除也不插入,保持对应位置字符不动。
  • 移动一步
    • 可以向右移动一格,如果遇到对角线,可以沿对角线继续移动。
    • 可以向下移动一格,如果遇到对角线,可以沿对角线继续移动。

让我们换个视角,从步数(深度)维度来观察所走过的路线,从起点(0, 0)到终点(7, 6)只走了 5 ,而且每走完一步,会到达图中不同的点,图中所示就是等深(d)线。

Myers 算法引入了很重的要的一个概念:k 值。

对于任意一点(x, y),我们定义这一点的 k 值为:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
k = x - y

注意到,处于对角线上的点的 k 值是相同的。下图展示出来的就是等 k 线。

Myers 算法巧妙的地方,就是它把 d(步数或深度) 和 k 紧密联系了起来。

联系1:从起点(0, 0)走出 D 步,只可能会停在 k = {-D, -D+2, ... D-2, D} 线上。

如下图:

  • 第 1 步:可能停在 k= {-1, 1} 线上
  • 第 2 步:可能停在 k= {-2, 0, 2} 线上
  • 第 3 步:可能停在 k= {-3, -1, 1, 3} 线上
  • 第 4 步:可能停在 k= {-4, -2, 0, 2, 4} 线上
  • 第 5 步:可能停在 k= {-5, -3, -1, 1, 3, 5} 线上

这里只是给大家通过图示建立一下直观认识,论文中使用了数学归纳法对这个结论进行了证明,感兴趣的同学可以自行阅读。

联系2:移动方式和 k 值的变化关系。

仔细观察一下,我们又可以注意到,移动一格和 k 值的变化有如下关系:

  • 如果向移动一,k 值 +1
  • 如果向移动一,k 值 - 1
  • 如果沿对角线移动,k 值不变。

对于移动一步,我们有:

  • 可以向右移动一格(k 值 +1),如果存在对角线,可以沿对角线继续移动(k 值不变)。所以最终结果也是 k 值 +1
  • 可以向下移动一格(k 值 -1),如果存在对角线,可以沿对角线继续移动(k 值不变)。所以最终结果也是 k 值 -1

联系3:第 D 步 一定是从第 D-1 步移动 1 步过来的。 (PS:好像是废话😄 )

联系4: (PS:重点来了👀️ )

由于,走到第 D 步,可能会停在 k = {-D, -D+2, ... D-2, D} 线上。

对于走了 d 步、停留在 k 线上的点(我们用坐标(d, k)描述),它一定只可能:

  • 从 d - 1步所在的 k - 1线上,右移一步(如果遇到对角线,可以继续沿对角线移动(k 值不变))到达。
  • 从 d - 1步所在的 k +1线上,下移一步(如果遇到对角线,可以继续沿对角线移动(k 值不变))到达。

ok,我们沿着这个思路重新模拟一遍流程。

第 0 步:d=0 停在 k=0 线上,位置(0, 0)。

第 1 步:由于 d=1 可能停在 k=-1、k=1线上

  • 如果 k=-1
    • 它不可能从d=0、k-1=-2线右移一步过来。(看图,因为越界了😄 )
    • 它可能从 d=0、k+1=0 的(0, 0)下移一步到达(0, 1)。
  • 如果k=1
    • 它可能从 d=0、k-1=0的(0, 0)右移一步到达(1, 0)。
    • 它不可能从d=0、k+1=2线下移一步过来。(看图,因为越界了😄 )

第 2 步:由于 d=2 可能停在 k=-2、k=0、k=2线上

  • 如果 k=-2
    • 它不可能从d=1、k-1=-3线右移一步过来。(看图,因为越界了😄 )
    • 它可能从 d=1、k+1=-1的(0, 1)下移一步到(0, 2),沿对角线可继续移动到(2, 4)
  • 如果 k=0
    • 注:因为我们偏向于优先进行删除操作随后插入操作,所以对于 k=0,我门偏向于从k+1=1的(1, 0)移动到(2, 2)(注:因为k+1=1的(1,0)在k-1=-1的(0, 1)的右侧)。
    • 它可能从 d=1、k-1=-1的(0, 1)右移一步到(1, 1),沿对角线可继续移动到(2, 2)
    • 它可能从 d=1、k+1=1的(1, 0)下移一步到(1, 1),沿对角线可继续移动到(2, 2)
  • 如果 k=2
    • 它可能从 d=1、k-1=1的(1, 0)右移一步到(2, 0),沿对角线可继续移动到(3, 1)
    • 它不可能从d=1、k+1=3线下移一步过来。(看图,因为越界了😄 )

第 3 步:由于 d=3 可能停在 k=-3、k=-1、k=1、k=3线上

  • 如果 k=-3
    • 它只可能从d=2、k+1=-2 的(2, 4)下移一步到(2,5),沿对角线移动到(3, 6)
  • 如果 k=-1
    • 两者起点的横坐标都是2,而且因为我们偏好保留连续的删除操作,所以我们从(2, 4)继续右移到(3, 4),然后沿对角线移动到(4, 5)
    • 它可能从 d=2、k-1=-2 的(2, 4)右移
    • 它可能从 d=2、k+1=0 的(2, 2)下移
  • 如果 k=1
    • 由于k+1=2的横坐标比较大,所以我们选择从(3, 1)下移到(3, 2),然后沿对角线移动到(5, 4)
    • 它可以从 d=2、k-1=0的(2, 2)右移
    • 它可能从 d=2、k+1=2 的(3, 1)下移
  • 如果 k=3
    • 它只可以从 d=2、k-1=2的(3, 1)右移到(4, 1),然后沿对角线移动到(5, 2)

第 4 步:由于 d=4 可能停在 k=-4、k=-2、k=0、k=2、k=4线上

  • 如果 k=-4
    • 从图中可以看出,k=-4时不用考虑(越界了...👀️ )
  • 如果 k=-2
    • 由于k+1=2的横坐标 4 比较大,所以我们选择从(4, 5)下移到(4, 6)
    • 它可以从 d=3、k-1=-3的(3, 6)右移
    • 它可能从 d=3、k+1=-1 的(4, 5)下移
  • 如果 k=0
    • 由于k+1=1的横坐标 5 比较大,所以我们选择从(5, 4)下移到(5, 5)
    • 它可以从 d=3、k-1=-1的(4, 5)右移
    • 它可能从 d=3、k+1=1 的(5, 4)下移
  • 如果 k=2
    • 两者起点的横坐标都是5,而且因为我们偏好保留连续的删除操作,所以我们从(5, 4)继续右移到(6, 4),然后沿对角线移动到(7, 5)
    • 它可以从 d=3、k-1=1的(5, 4)右移
    • 它可能从 d=3、k+1=3 的(5, 2)下移
  • 如果 k=4
    • 它只可能从 d=3、k-1=3的(5, 2)右移到(6, 2),然后沿对角线到(7, 3)

第 5 步:由于 d=5 可能停在 k=-5、k=-3、k=-1、k=1、k=4、k=5线上

  • 如果 k=-5、k=-3
    • 从图中可以看出,k=-5、k=-3时不用考虑(越界了...👀️ )
  • 如果 k=-1
    • 由于k+1=0的横坐标 5 比较大,所以我们选择从(5, 5)下移到(5, 6)
    • 它可以从 d=4、k-1=-2的(4, 6)右移
    • 它可能从 d=4、k+1=0 的(5, 5)下移
  • 如果 k=1
    • 由于k+1=2的横坐标 7 比较大,所以我们选择从(7, 5)下移到(7, 6),到达终点!!!!
    • 它可以从 d=4、k-1=0的(5, 5)右移
    • 它可能从 d=4、k+1=2 的(7, 5)下移
  • 如果 k=3
    • 它只可能从 d=3、k+1=4的(7, 3)下移到(7, 4)
  • 如果 k=5
    • 越界了,不用考虑...

至此,我们使用 Myers 算法找了一条从起点(0,0)到终点(7,6)的最短路径。

Myers算法程序

下面让我们把上面的算法流程,转换为可运行代码。

计算这条最短路径的深度

因为 从起点(0, 0)出发走出 d 步时,它只可能落在 k={-d, -d+2, ...., d-2, d} 的 k 线上。

所以我们算法的本质上是,在走出一步后,计算这一步可能落在的每条 k 线上的最远位置,一直到碰到终点为止。

首先,对于字符串 a、b,它们的长度分别为 N、M,那么这个最短路径的深度上限为:N + M。(例如:当字符串a、b间没有任何公共元素时,只能把字符串 a 中的所有字符逐个删除,然后逐个插入字符串 b 中的所有字符。🚀️ )

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const N = a.length;
const M = b.length;

const MAX = N + M;

for (let d = 0; d <= MAX; d++) {
   ...
}
代码语言:javascript
代码运行次数:0
运行
复制

其次,走到第 d 步时,落点只可能在 k={-d, -d+2, ...., d-2, d} 的 k 线上,所以我们只需要计算在这几条 k 线上能在图中走到什么位置(x, y)。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const N = a.length;
const M = b.length;

const MAX = N + M;
  
for (let d = 0; d <= MAX; d++) {
      for (let k = -d; k <= d; k = k + 2) {
        let x, y;
        .... // 计算走到 d 步,各 k 线上可能走到的位置(x, y)
      }
}

到这里,有没有注意到,我们关注的实际上是:d、k、(x, y)间的关系;

技巧1🚀️ :

因为:k = x - y

所以:我们只需要关注(或者说存储):d、k、x 间的关系即可。 (PS:因为 y 可以由 x - k 推出。)

到此,可以用一个二维数组来描述 d、k、x 间的关系:

d

k

k

0

0

1

-1

1

2

-2

0

2

3

-3

-1

1

3

技巧2🚀️ :

因为,走到第 d 步时,落点只可能在 k={-d, -d+2, ...., d-2, d} 的 k 线上。

所以当 d 是偶数时,落点也是在偶数 k 线上;当 d 是奇数是,落点在奇数 k 线上。

并且,走到第 d 步所处的 k 线,只能:

  • 从 d - 1 步的 k-1 线右移(如果有对角线,继续沿对角线移动)到达。
  • 从 d +1 步的 k+1 线下移(如果有对角线,继续沿对角线移动)到达。

也就是说,走到第 d 步,至于 d - 1步有关系,与 d-2、d-3等前面的步骤没有关系。

并且,d 与 d-1,d-1与d-2 ..... 每一对的奇偶数是错开的。

所以,上面的二维数组,可以用一个一维数组替代。

因为,d 的最大数值是 N + M,所以 k 最大范围是 -(N+M) ~ (N+M)。

所以,程序可以简化为:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const N = a.length;
const M = b.length;

const MAX = N + M;

const v: number[] = new Array(2 * MAX + 1).fill(-1);
  
for (let d = 0; d <= MAX; d++) {
      for (let k = -d; k <= d; k = k + 2) {
        let x, y;
        .... // 计算走到 d 步,各 k 线上可能走到的位置(x, y)
      }
}

下面我们就需要根据d、k来计算 x:

因为,走到第 d 步所处的 k 线,只能:

  • 从 d - 1 步的 k-1 线右移(如果有对角线,继续沿对角线移动)到达。
  • 从 d +1 步的 k+1 线下移(如果有对角线,继续沿对角线移动)到达。
  • 而且不能跨越边界(例如:k==-d时,只能从 k+1下移;k==d时,只能从k-1右移)。
  • 而且我们偏向保留最多的连续删除操作(我们会观察 v[k+1]和v[k-1]间的大小)。

所以,综合上面所有的信息,就得到:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private shortest_edit(
    a: Array<T>,
    b: Array<T>
  ): [number, number, Array<number[]>] {
    const N = a.length;
    const M = b.length;

    const MAX = N + M;

    const v: number[] = new Array(2 * MAX + 1).fill(-1);
    const vHis: Array<number[]> = [];

    v[1] = 0;
    for (let d = 0; d <= MAX; d++) {
      vHis.push([...v]);

      for (let k = -d; k <= d; k = k + 2) {
        let x, y;
        if (k == -d || (k != d && this.getX(v, k - 1) < this.getX(v, k + 1))) {
          x = this.getX(v, k + 1);
        } else {
          x = this.getX(v, k - 1) + 1;
        }

        y = x - k;

        while (x < N && y < M && this.equals(a[x], b[y])) {
          x++;
          y++;
        }

        this.setX(v, k, x);

        if (x >= N && y >= M) {
          return [d, k, vHis];
        }
      }
    }

    throw new Error("(d, k) not found!");
  }

根据深度回溯出这条虽短路径

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private backTrack(
    a: Array<T>,
    b: Array<T>,
    d: number,
    k: number,
    vHis: Array<number[]>
  ) {
    const trace: Array<[[number, number], [number, number]]> = [];

    // 起点是终点(N, M)
    let x = a.length;
    let y = b.length;

    vHis.reverse().forEach((v) => {
      const k = x - y;

      let prev_k;
      if (k == -d || (k != d && this.getX(v, k - 1) < this.getX(v, k + 1))) {
        prev_k = k + 1;
      } else {
        prev_k = k - 1;
      }

      const prev_x = this.getX(v, prev_k);
      const prev_y = prev_x - prev_k;

      while (x > prev_x && y > prev_y) {
        trace.push([
          [x - 1, y - 1],
          [x, y],
        ]);

        x = x - 1;
        y = y - 1;
      }

      if (d > 0) {
        trace.push([
          [prev_x, prev_y],
          [x, y],
        ]);
      }

      d--;
      x = prev_x;
      y = prev_y;
    });

    return trace;
  }

根据路径生成比对差异

这一部分相对比较简单,根据上一步计算出的最短路径,并利用基本原则:

  • 横向移动一格(y 相同),代表删除(delete)字符串 a 中的一个字符。
  • 竖向移动一个(x 相同),代表插入(insert)字符串 b 中的一个字符。
  • 对角线移动,表示字符保持不变(equal)。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private makeDiff(
    trace: Array<[[number, number], [number, number]]>,
    a: Array<T>,
    b: Array<T>
  ) {
    const diff: Array<
      {
        operation: "insert" | "delete" | "equal";
      } & E
    > = [];

    trace.forEach((t) => {
      const prev_x = t[0][0];
      const prev_y = t[0][1];
      const x = t[1][0];
      const y = t[1][1];

      const a_ele = a[prev_x];
      const b_ele = b[prev_y];

      if (x == prev_x) {
        diff.unshift({
          operation: "insert",
          ...this.str(b_ele),
        });
      } else if (y == prev_y) {
        diff.unshift({
          operation: "delete",
          ...this.str(a_ele),
        });
      } else {
        diff.unshift({
          operation: "equal",
          ...this.str(a_ele),
        });
      }
    });

    return diff;
  }

完整代码

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class MyersDiff<T, E> {
  equals: (ele_a: T, ele_b: T) => boolean;
  str: (ele: T) => E;

  constructor(equals: (ele_a: T, ele_b: T) => boolean, str: (ele: T) => E) {
    this.equals = equals;
    this.str = str;
  }

  private getX(v: number[], k: number) {
    if (k < 0) {
      return v[v.length + k];
    } else {
      return v[k];
    }
  }

  private setX(v: number[], k: number, x: number) {
    if (k < 0) {
      v[v.length + k] = x;
    } else {
      v[k] = x;
    }
  }

  private shortest_edit(
    a: Array<T>,
    b: Array<T>
  ): [number, number, Array<number[]>] {
    const N = a.length;
    const M = b.length;

    const MAX = N + M;

    const v: number[] = new Array(2 * MAX + 1).fill(-1);
    const vHis: Array<number[]> = [];

    v[1] = 0;
    for (let d = 0; d <= MAX; d++) {
      vHis.push([...v]);

      for (let k = -d; k <= d; k = k + 2) {
        let x, y;
        if (k == -d || (k != d && this.getX(v, k - 1) < this.getX(v, k + 1))) {
          x = this.getX(v, k + 1);
        } else {
          x = this.getX(v, k - 1) + 1;
        }

        y = x - k;

        while (x < N && y < M && this.equals(a[x], b[y])) {
          x++;
          y++;
        }

        this.setX(v, k, x);

        if (x >= N && y >= M) {
          return [d, k, vHis];
        }
      }
    }

    throw new Error("(d, k) not found!");
  }

  private backTrack(
    a: Array<T>,
    b: Array<T>,
    d: number,
    k: number,
    vHis: Array<number[]>
  ) {
    const trace: Array<[[number, number], [number, number]]> = [];

    // 起点是终点(N, M)
    let x = a.length;
    let y = b.length;

    vHis.reverse().forEach((v) => {
      const k = x - y;

      let prev_k;
      if (k == -d || (k != d && this.getX(v, k - 1) < this.getX(v, k + 1))) {
        prev_k = k + 1;
      } else {
        prev_k = k - 1;
      }

      const prev_x = this.getX(v, prev_k);
      const prev_y = prev_x - prev_k;

      while (x > prev_x && y > prev_y) {
        trace.push([
          [x - 1, y - 1],
          [x, y],
        ]);

        x = x - 1;
        y = y - 1;
      }

      if (d > 0) {
        trace.push([
          [prev_x, prev_y],
          [x, y],
        ]);
      }

      d--;
      x = prev_x;
      y = prev_y;
    });

    return trace;
  }

  private makeDiff(
    trace: Array<[[number, number], [number, number]]>,
    a: Array<T>,
    b: Array<T>
  ) {
    const diff: Array<
      {
        operation: "insert" | "delete" | "equal";
      } & E
    > = [];

    trace.forEach((t) => {
      const prev_x = t[0][0];
      const prev_y = t[0][1];
      const x = t[1][0];
      const y = t[1][1];

      const a_ele = a[prev_x];
      const b_ele = b[prev_y];

      if (x == prev_x) {
        diff.unshift({
          operation: "insert",
          ...this.str(b_ele),
        });
      } else if (y == prev_y) {
        diff.unshift({
          operation: "delete",
          ...this.str(a_ele),
        });
      } else {
        diff.unshift({
          operation: "equal",
          ...this.str(a_ele),
        });
      }
    });

    return diff;
  }

  diff(a: Array<T>, b: Array<T>) {
    const [d, k, vHis] = this.shortest_edit(a, b);

    const trace = this.backTrack(a, b, d, k, vHis);

    const diff = this.makeDiff(trace, a, b);

    return diff;
  }
}

Myers算法示例

最后,用上面复刻出来的 Myers 算法,来复现一下对字符串 a、b 之间的差异比较。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const myers = new MyersDiff<string, { ele: string }>(
  (ele_a, ele_b) => {
    return ele_a === ele_b;
  },
  (ele) => {
    return {
      ele,
    };
  }
);

const a = "ABCABBA";
const b = "CBABAC";
const diff = myers.diff(a.split(""), b.split(""));
console.log(JSON.stringify(diff, null, 4));

正确,运行出来的结果更预期一致。

参考

The Myers diff algorithm(James Coglan):

  • https://blog.jcoglan.com/2017/02/12/the-myers-diff-algorithm-part-1/
  • https://blog.jcoglan.com/2017/02/15/the-myers-diff-algorithm-part-2/
  • https://blog.jcoglan.com/2017/02/17/the-myers-diff-algorithm-part-3/

Investigating Myers' Diff Algorithm(Nicholas Butler):

  • https://www.codeproject.com/Articles/42279/Investigating-Myers-diff-algorithm-Part-1-of-2
  • https://www.codeproject.com/Articles/42280/Investigating-Myers-Diff-Algorithm-Part-2-of-2

paper:

  • http://www.xmailserver.org/diff2.pdf

misc:

  • https://blog.robertelder.org/diff-algorithm/
  • https://epxx.co/artigos/diff_en.html
  • https://git-scm.com/docs/diff-options
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-12-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 WebJ2EE 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
1 条评论
热度
最新
佬,优秀好文,一遍看懂,非常感谢
佬,优秀好文,一遍看懂,非常感谢
回复回复点赞举报
推荐阅读
编辑精选文章
换一批
Myers‘Diff之贪婪算法
写这篇文章已经拖了很久了,因为一直在准备后续的 Myers‘Diff之线性空间细化 。最初不知道是什么时候发现 DiffUtil 对比列表 item 数据进行局部刷新,git 文件对比都用到了这个算法。上个月刚好再一次看到了就想深入了解一下。但发现发现国内的博客和帖子,对这个算法的讲述内容比较少,每篇文章都讲述了作者自己认为重要的内容,所以有一个点搞不懂的话没法整体性的进行理解。刚开始我自己就有一个点没想清楚想了好几天,我觉得程序员不能怕算法,书读百遍其义自现,阅读算法代码也是如此,平时多思考偶尔的一点灵光出现会减少你死磕算法浪费的时间。
静默加载
2020/10/20
3K0
Myers‘Diff之贪婪算法
Myers 差分算法 (Myers Difference Algorithm) —— DiffUtils 之核心算法(一)
如果使用过 android architecture 中关于 LiveData 部分的朋友,可能对于DiffUtils这个玩意儿并不陌生。
程序亦非猿
2019/08/16
1.8K0
Myers 差分算法 (Myers Difference Algorithm) —— DiffUtils 之核心算法(一)
【论文阅读笔记】Myers的O(ND)时间复杂度的高效的diff算法
之前咱们三个同学做了个Simple-SCM,我负责那个Merge模块,也就是对两个不同分支的代码进行合并。当时为了简便起见,遇到文件冲突的时候,就直接按照文件的更改日期来存储,直接把更改日期较新的那个文件认为是我们要保留的文件版本。但是这样子做是存在很多问题的,因为这样做就无法对不同分支的代码他们各自的特性进行整合,最终保留的只是其中一个分支的代码。因此,加入按行进行比较的diff算法是非常必要的。
灯珑LoGin
2022/10/31
9110
【论文阅读笔记】Myers的O(ND)时间复杂度的高效的diff算法
一个有些意思的项目--文件夹对比工具(一)
为什么会写这个,因为遇到了有意思的事情,简而言之就是,面试某意向公司,没过;其中一位面试官非常nice,还仔细看了我博客,觉得是不是面试时没展现出来,因此第二天专程打电话过来,给了我一个额外机会,就是花几天时间做一个小项目,过几天提交给他。
低级知识传播者
2022/11/16
6390
一个有些意思的项目--文件夹对比工具(一)
从入门到精通之Boyer-Moore字符串搜索算法详解
本文讲述的是Boyer-Moore算法,Boyer-Moore算法作为字符串搜索算法,兴趣之下就想了解这个算法,发现这个算法一开始还挺难理解的,也许是我理解能力不是很好吧,花了小半天才看懂,看懂了过后就想分享下,因为觉得这个算法真的挺不错的,以前一直以为字符串搜索算法中KMP算很不错的了,没想到还有更好的,Boyer-Moore算法平均要比KMP快3-5倍。 下面是我对该算法的理解,参考了一些关于该算法的介绍,里面每一张图都画的很认真,希望能讲清楚问题,有什么错误、疑问或不懂的地方麻烦大家一定要提出来,共同
Angel_Kitty
2018/04/09
1.7K0
从入门到精通之Boyer-Moore字符串搜索算法详解
一看就懂,一写就懵?搞懂回溯算法,一口气刷了20多道题
回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。——摘自《百度百科》
微芒不朽
2021/12/26
1.7K0
一看就懂,一写就懵?搞懂回溯算法,一口气刷了20多道题
路径规划算法之A*算法
路径规划是非常常见的一类问题,例如移动机器人从A点移动到B点,游戏中的人物从A点移动到B点,以及自动驾驶中,汽车从A点到B点。这类问题中,都有两个关键问题需要解决:
用户10646968
2023/07/07
7930
路径规划算法之A*算法
vue源码分析-diff算法核心原理
之前讲到Vue在渲染机制的优化上,引入了Virtual DOM的概念,利用Virtual DOM描述一个真实的DOM,本质上是在JS和真实DOM之间架起了一层缓冲层。当我们通过大量的JS运算,并将最终结果反应到浏览器进行渲染时,Virtual DOM可以将多个改动合并成一个批量的操作,从而减少 dom 重排的次数,进而缩短了生成渲染树和绘制节点所花的时间,达到渲染优化的目的。之前的章节,我们简单的介绍了Vue中Vnode的概念,以及创建Vnode到渲染Vnode再到真实DOM的过程。如果有忘记流程的,可以参考前面的章节分析。
yyzzabc123
2022/10/18
5340
A星寻路算法详解
A星寻路算法是静态路网中求解最短路径最有效的直接搜索方法,也是解决许多搜索问题的有效算法,它可以应对包括复杂地形,各种尺度的障碍物以及不同地形的路径规划问题。掌握A星寻路算法能够提高路径规划效率,应对各种复杂情况,并在实际应用中发挥重要作用。
astonishqft
2024/02/27
2.3K0
A星寻路算法详解
程序员进阶之算法练习(六十二)AK练习
题目链接 题目大意: 小明有a个1元硬币,b个2元硬币; 小明想要购买一个商品,并且不想找零; 现在小明想知道自己无法给到最低价格是多少;
落影
2022/06/05
5790
老生常谈React的diff算法原理-面试版
从代码可以看出,React通过先判断key是否相同,如果key相同则判断type是否相同,只有都相同时一个DOM节点才能复用。
beifeng1996
2022/10/05
5970
十道简单算法题
前言 最近在回顾以前使用C写过的数据结构和算法的东西,发现自己的算法和数据结构是真的薄弱,现在用Java改写一下,重温一下。 只能说慢慢积累吧~下面的题目难度都是简单的,算法的大佬可直接忽略这篇文章了~入门或者算法薄弱的同学可参考一下~ 很多与排序相关的小算法(合并数组、获取数字每位值的和),我都没有写下来了,因为只要会了归并排序(合并数组),会了桶排序(获取数字每位的值),这些都不成问题了。如果还不太熟悉八大基础排序的同学可看:【八大基础排序总结】 由于篇幅问题,每篇写十道吧~ 如果有错的地方,或者有更好
Java3y
2018/04/02
2.6K0
十道简单算法题
Myers'Diff之线性空间细化
在学习完上一篇文章Myers'Diff之贪婪算法 之后,我对Android源码中的DiffUtil类进行了阅读发现其算法的实现和文章中的方式并不尽相同,而是在其基础之上再次进行的优化。所以本篇文章是以上一篇Myers'Diff之贪婪算法 文章内容基础之上对它的变体进行再次研究的过程。
静默加载
2020/10/20
6180
Myers'Diff之线性空间细化
跳点搜索算法JPS及其优化
引言 寻路算法用途众多,例如在游戏和地图中。A*算法已经众所周知,对于其优化也是层出不穷,然而性能并没有取得突破性进展。本文介绍一种跳点搜索算法JPS以及其四个优化算法,其中三个优化是加速跳点的寻找,
serena
2017/12/11
7K2
跳点搜索算法JPS及其优化
前端学数据结构与算法(十四):01执行的艺术 - 回溯算法(下)
书接上文,上个章节从递归到回溯的做了递进式介绍,相信已经对回溯有了初步的理解,接下来主要介绍更多与回溯相关的题目,从广度上加深对其理解。
飞跃疯人院
2020/12/06
5630
初探富文本之文档diff算法
当我们实现在线文档的系统时,通常需要考虑到文档的版本控制与审核能力,并且这是这是整个文档管理流程中的重要环节,那么在这个环节中通常就需要文档的diff能力,这样我们就可以知道文档的变更情况,例如文档草稿与线上文档的差异、私有化版本A与版本B之间的差异等等,本文就以Quill富文本编辑器引擎为基础,探讨文档diff算法的实现。
WindRunnerMax
2024/02/22
3110
DiffUtil和它的差量算法
学习Myers'Diff 算法是从 DiffUtils 源代码开始的,但DiffUtil和它的差量算法这篇却是文章是在写完 Myers‘Diff之贪婪算法 和 Myers‘Diff之线性空间细化 这两篇算法文章之后着手的。比较先需要学会算法才能理解代码实现并更好的进行使用。
静默加载
2020/10/20
1.5K0
DiffUtil和它的差量算法
n皇后问题总结_模拟退火n皇后
N皇后问题是一个经典的问题,在一个N*N的棋盘上放置N个皇后,每行一个并使其不能互相攻击(同一行、同一列、同一斜线上的皇后都会自动攻击)。
全栈程序员站长
2022/11/11
9750
n皇后问题总结_模拟退火n皇后
前端「N皇后」递归回溯经典问题图解
在我的上一篇文章《前端电商 sku 的全排列算法很难吗?学会这个套路,彻底掌握排列组合。》中详细的讲解了排列组合的递归回溯解法,相信看过的小伙伴们对这个套路已经有了一定程度的掌握(没看过的同学快回头学习~)。
ssh_晨曦时梦见兮
2020/10/15
1.2K0
《算法竞赛进阶指南》0x26 广度变形
在一般的广度优先搜索中,每次沿分支扩展“一步”,逐层搜索,已求解起始状态到每个状态的最小步数
一只野生彩色铅笔
2022/10/31
5280
《算法竞赛进阶指南》0x26 广度变形
相关推荐
Myers‘Diff之贪婪算法
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档