Myers' diff 算法

写这篇文章,

一、DiffUtil 对比列表item 数据,git 文件对比都用到了这个算法。
二、发现国内的博客,帖子,对这个算法的描述很少很少,算法本身又难以理解。
三、上篇DiffUtil 源码分析时遇到了这个算法,我觉得程序员不能怕算法,要跟算法死磕到底,要往下挖源码。
书读百遍其义自现,阅读算法代码也是如此。

对比原则和图画结合

两个字符串,a = "ACBA" b = "CBA"对比这两个字符的差异,有什么发现?
你会脱口而出 :
a 比 b 最前面多了一个"A"
为什么不是:
第一位,A≠C,第二位,C≠B,第三位,B≠A,第四位,b少了一位
由此得到我们对比的原则(我总结的):

  • 计算最大重合部分("CBA"),改变字符最小的原则。

用图片表示,画上斜线的格子就是重合的部分("CBA")。(注意这张图片,这个算法是用表格的方式理解的)


ACBA.png

我们最开始的描述: 第一位多了一个"A",其余不变,是改变最小的
图表的描述就是:x 方向(向右)一步,其余斜线,是横纵走得最少的。

  • 向右一步=增加了 "A",改变最小=横纵走得最少

看图:


ACBA.png

新数据a = "AB" 比上 b= "BB"
有两条最短路径,向右一步,向下一步或者向下一步,向右一步。
我们选择向右一步向下一步的,图表的方式是:向右一步再向下一步,其余斜线。我们的描述是:新数据a相对于旧数据b,第一位增加了a中的A,减去了b中的B,其余不变。

  • 向下=减去

上面能做到图画,和逻辑结合了,请看下面这样一个问题:
a = "ABCABBA"
b ="CBABAC"
a的长度7,b的长度等于6


aa.png

它的最小改变的描述是如何?
这个问题可以改成,从 起点 (0,0)到 终点(7,6)的最短横纵路径是怎么走?


aa.png

一目了然,当黄色路径达只要五步就到达的时候,其他路径还没有到达。
迈尔斯的 diff 算法就是比较快速求出这样一条路径的算法。

提出概念

(需要结合图画理解,这些概念的提出是为计算和表达方便,涉及数学,类似设个x ,设个y 的未知数)

  • snake :一步所走的路径 为 snake,有所不同的是,snake 分为三个点,起点,中点,终点,
    一般走一步没有遇到斜线,中点就是终点, 遇到了的话,路径就包含了斜线 ,终点坐标计算也要包含斜线的坐标。

  • k : 定义x 轴向右增大,y轴 向下增大,定义 k = x-y;

  • d : 定义为步数。
    看下图:


    2566000-f8f59baffd4eb418.png

黄色的线:k线 (注意这些线)

深蓝色的线:snake

浅蓝色的数字:d (步数)

红色的线:算法求出的问题的解(其实这个解有多个这只是求了其中一条)

怎么冒出来的这个k? 所有k相同的点组成了一条线,他们组成了对角线。并且对于k,我们可以使用一个记录的数组V,用k作为该数组的index,x为value,这样我们可以计算求得 y值

这个算法父问题的求解归结为子问题的求解。要知道d=5时所有k对应的最优坐标,必须先要知道d=4时所有k对应的最优坐标,要知道d=4时的答案,必须先求解d=3,以此类推。

思考:
如何使用最小d(步数) 到达终点(N,M),可以得到外循环。

接下来我们想想如何到达point(x,y)? 设 k = x - y,那么能到达点(x,y)的 只能从k-1 或者 k+1两条对角线上到k上,步数的递增,内循环应该是k ,内层循环完毕的结果就是,在步数为d,求得对角线k能到达的最远的x坐标。每向下或向右一步,坐标必然会从一条k线上移动到另外一条k线上,也就是说步数固定情况下,k的改变,我们可以求出相应的移动方向从而确定路径,内循环就是k递增,找到最快一条到达终点(N,M)的路,目的就达成了

上伪代码好好理解下:

2566000-b2db4ffe4e5d7e1f.png

伪码分析

一般到这里就会很多的疑惑,很懵逼:

我们拆分下:

  • 最外面这个循环

     //d 是步数,N 是字符的长度6 ,M 是字符长度7 ,
      //纵然 d 一直右向,再向下,经过斜线,也是大于不了N+M 的
     for (int d = 0; d<= N +M ;d++){
     ···
     }
    
  • 里面这层循环

  1. k = x-y ; 我们可以通过x 求y ,v[]数组里面以k为index,存储最优坐标的x值,取的时候只要知道k值,因为v[k] =x;通过有y =v[k]-k 就可以算出y;
  1. k 结合图形,我们从(0,0)开始,每次只增长一步,向下一步=>k-1,向右一步=>k+1;
    第一步:k的极限,k-1<= k <=k+1
    第二步:k的极限,k-2<= k <=k+2
    第d步: k的极限,k-d<= k <= k+d

  2. 最短d步到达终点(x,y),假设途中经过 一个 3个斜线,经过横线 i 个,经过竖线 j 个
    则x = i+3,y = j+3;终点:(x,y)=>(i+3,j+3)
    假设 d 是奇数,i+j 必然是奇数
    由k = x-y = i +j +6;
    得k 为奇数,d 为奇数,k 只能为奇数,
    所以内循环时 ,k +=2(看图,这句话的意思是一个方格不能从对角线到对面的点,不是向下,就是向右)

    // 每一步递增
     for (int d = 0; d<= N +M ;d++){
   //在每一步下面的各个k的取值,k+=2 (见上面地三点) 
     for( int k = -d ;k <= d ;k +=2){
        // 是否向下
       //这里注意看图上黄色斜线,起点k=0时;k= -1 是向下一步,当k=-d 时必然向下 ,k=d 必然向右,第一步d=1 ,k=-1向下,k = 1 向右。 
        //v[ ]数组保存了k上的 x ,看图, 从第一步完成后,执行第二步,d =2 ,k = - 2, 0 ,2三个数,
        // k = -2 时,向下; k = 0 ,(这时右边的k线 => v[k+1] =1(第一步记录v[ ]为1),左边的k线 => v[k-1] =0(第一步结束时记录为零),
        // 所以向下,k =2 , v[k+1] = 0(没有保存) v[k-1] = 1(第一步保存为1)
        // 这样写的好处,以k 为基准进行的移动,不会出现重复的情况 
        bool down = ( k== - d || (k !=d && v[k-1] < v[ k+1] )); 
   ···
    //save end point
    //保存对应k 的x方向的值(这里的x向右0到7,y向下0到6 )= 保存d步结束,对应k值上的终点坐标; 
     //终点坐标(x,y)=> (x=v[k],y= v[k]-k)
   v[k] = xEnd;
   }
 }
  • 整个伪码流程

      //新建集合,用k值保存对应的x
      v[ 1 ] = 0; 
     //步数递增
    for (int d= 0; d <= N+M ;d++){
     //每一步下面的各个k 的取值循环,    
        for( int k = -d ; k <= d ; k+=2){
        // 是否向下
          bool down = ( k== - d || (k !=d && v[k-1] < v[ k+1] )); 
        //得到上次移动的k,如果这次向下,上次的kPrev = k+1;
        int kPrev = down ? k+1 : k -1; 
        //起点,因为每次移动保存是 k+2 ,所以如果这次的index全是奇数,上一次就全是偶数,在一个数组里面不会进行覆盖
        int xStart = v[kPrev]
        int yStart = xStart - v[kPrev]
        //中点,之前说的,如果没有遇到斜线,它是等于终点的
        int xMid = down ? xStrat : xStart +1;
        int yMid = xMid - k;
        //终点,没有遇到斜线的样子
        int xEnd = xMid;
        int yEnd = yMid;
        //斜线数量
        int s= 0
        //这就是遇到斜线,只有数据相等,才会是斜线,然后,x,y 各加一
        while(xEnd < N && yEnd <M && A[xEnd] ==B[yEnd]){
          xEnd++;
          yEnd++;
          s++;
           }
        // 保存到数组
        v[k] = xEnd;
          //到了最终点(N,M),找到了最短步数的方案
        if(xEnD >= N && yEnd > = M){
          //跳出循环  
          }
             }     
         } 
    

如果看了我注释还是没有懂,我推荐几个网址:
外文的博客,加载有点慢
从DiffUtil到Myers'差分算法
Git是怎样生成diff的:Myers算法
java实现
算法的论文
如果还没有懂,结合图案,多读几遍,注意他的每走一步其实就是k线上切换。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260