【轉】Diff算法與實現


什么是Diff

在日常工作中,diff是大家常用的一個工具,它能快速的計算出兩個文本的差異,並將差異結果一目了然的展示出來,幫助我們快速定位在不同版本中文件的修改位置。

 

 

以上流程圖簡單描述了我們使用diff程序的流程,只需往diff程序中輸入text1與text2(我們規定,text1為初始文本,text2為編輯文本),diff程序會自動計算出兩個文本的差異並可視化輸出。

在這篇文章中,將會簡單的談一談diff程序內部流程是如何流轉並實現的,以及介紹在實現過程中會遇到的一些算法問題。

問題分析

舉個簡單的例子:

text1: console.log('hello world');
text2: console.log('hi js');

console.log(‘helloi worldjs‘);

從結果中我們可以看出,diff程序的可視化輸出,無非是將文本中未改變的部分原樣輸出,其余字符,若是在text1中,則被標記為刪除;若是在text2中,則被標記為新增。如下使用紅色標記兩個文本的未改變部分:
text1: console.log(‘hello-world‘);
text2: console.log(‘hi-js‘);
紅色部分除外,在text1中的elloworld在結果中都將被標記為刪除;text2中的ijs在結果中都將被標記為新增。
因此,diff程序的關鍵就在於如何找到text1與text2所有相同文本(即找到標記紅色的文本),然后再分別標記出剩下的刪除或新增文本即可。

如何diff

LCS

計算兩個文本相同部分的問題,即求兩個字符串的最長公共子序列(Longest Common Subsequence,簡稱LCS)問題,介紹LCS問題解法之前,先了解以下概念。

Subsequence(NOT Substring)

設序列Xm=<x1, x2, …, xm>,從Xm序列中任意取出若干個元素,按照元素下標從小到大的順序排列得到一個新的序列Zk=<z1, z2, ..., zk>,則Zk為Xm的一個子序列。子序列只要求元素的前后位置關系與原序列中保持一致即可,不必保證元素必需是連續的。

公共子序列

給出兩個序列Xm與Yn,找到一個序列Zk,滿足:Zk即是Xm的子序列,又是Yn的子序列,則Zk為Xm與Yn的一個公共子序列

最長公共子序列

給出兩個序列Xm與Yn,找到一個Xm與Yn的公共子序列Zk,Zk的長度是Xm與Yn所有公共子序列中長度最長的一個。

暴力美學

最簡單直接的,就是暴力求解,如下為暴力法求解LCS的偽代碼:

Zk = 空
for Xm的所有子序列 Zx
if Zx是Yn的子序列 && Zx長度大於Zk
Zk = Zx

一個長度為m的序列,存在2^m個子序列;判斷Zx序列是否為Yn序列的子序列,時間復雜度為O(n);因此暴力求解算法的時間復雜度為O(n*2^m)。指數級別時間復雜度的算法,基本上是要把電腦搞掛的,這顯然不是我們想要的結果。

動態規划

動態規划一般也只能應用於有最優子結構的問題。最優子結構的意思是局部最優解能決定全局最優解(對有些問題這個要求並不能完全滿足,故有時需要引入一定的近似)。簡單地說,問題能夠分解成子問題來解決。
使用動態規划算法求解最優解的時候,最關鍵的問題在於如何找到最優解問題的轉化方程。

設序列Xm=<x1, x2, …, xm>和Yn=<y1, y2, …, yn>的一個最長公共子序列Zk=<z1, z2, …, zk>,則:

  1. 若xm=yn,可以使用反證法證明:xm(yn)必然是Zk的最后一個字符,即zk=xm=yn,且Zk-1是Xm-1和Yn-1的最長公共子序列。因此 LCS(X, Y) 即可以轉化成 LCS(Xm-1, Yn-1) + 1
  2. 若xm≠yn且zk≠xm ,亦可用反證法證明:Z是Xm-1和Y的最長公共子序列;
  3. 若xm≠yn且zk≠yn ,同第二點,可得出Z是X和Yn-1的最長公共子序列。
    其中:Xm-1=<x1, x2, …, xm-1>,Yn-1=<y1, y2, …, yn-1>,Zk-1=<z1, z2, …, zk-1>

整合2、3點的結論,得出當xm≠yn時,可以將 LCS(X, Y) 轉化成 MAX(LCS(Xm-1, Y), LCS(X, Yn-1))

設dp[i][j]表示序列Xi和Yj的一個最長公共子序列Zk的長度,即dp[i][j] = k。
因此我們可以得到以下公式:

 

 

根據此公式,我們定義一個二維數組dp,size為m*n,dp數組中的每個元素的值,都可以通過其左上/左/上三個方位的單元格得到,因此我們只需順序填滿dp表格,在dp[m][n]處就可以得到Xm與Yn的最長公共子序列長度。如下所示(X: ABCABBA, Y: CBABAC):

僅僅得到最長公共子序列的長度顯然不足以達到我們的目的,為了能夠得到一個完整的LCS,我們在計算dp表格的過程中,還需要同時記錄計算每個單元格值的來源位置,使用↖︎三個字符來表示,如下圖(若dp[i-1][j]與dp[i][j-1],即當前位置的左單元格和上單元格相等時,默認取左邊單元格,即dp[i][j-1]),:

 


在上圖的表格中,我們只需要從最右下角的單元格開始,按照箭頭的指示就可以一步步往左上角靠近,如圖中標記紅色的路線。在此路線中,符號表示Y掃過一個字符,此字符需標記為新增;符號表示X掃過一個字符,此字符需標記為刪除;↖︎表示X、Y同時掃過一個字符,此字符為LCS中的元素,為未改變字符。
如上操作,我們就可以得到X與Y的一個ABCABABAC

至此我們已經可以實現一個簡單的diff程序了。
前往閱讀“閹割版”Diff插件源碼
點我查看“閹割版”Diff插件Demo演示

SED

除了上面介紹的LCS算法之外,最短編輯距離(Shortest Edit Distance,簡稱SED)算法同樣可以用來實現文本diff。
SED問題的描述是:給出兩個字符串text1與text2,求將text1編輯成text2所需要的最短編輯距離。定義有效的編輯操作有:刪除一個字符、新增一個字符、替換一個字符,其中一次刪除或新增操作編輯距離記為1,替換操作的編輯距離記為2(因為一個替換操作相當於一個刪除操作加上一個新增操作)。

仔細觀察可以發現,SED問題其實是LCS問題的一個逆向描述。LCS求盡可能多的相同部分,而SED求盡可能少的編輯部分(即不同部分)。他們必然滿足一個關系:m + n === 2 * LCS(X, Y) + SED(X, Y); 其中m、n分別為X與Y的長度。因此SED問題的結果與LCS是等效的,求解SED問題同樣可以使用動態規划的思想來解,只是在公式上有所不同罷了,以下給出SED的求解公式:

設f[i][j]表示從字符xi轉化成yj的編輯距離;
設dp[i][j]表示序列Xi=<x1, x2, …, xi>轉化成Yj=<y1, y2, …, yj>的最短編輯距離;

 

同樣,根據公式,依次填滿dp表格,同時標記上每個單元格的來源方位,即可找到兩個文本的差異,演算過程類似LCS,在此不再贅述。

優化

上面介紹的LCS與SED算法,時間復雜度都是O(n*m),因此精確計算LCS/SED是非常耗費性能的。
對於基於字符的diff程序,n與m分別為text1與text2的字符數。一個簡單有效的優化方式就是轉化成基於行的diff,轉化后n與m分別為text1與text2的行數,這就可以大大加速diff的耗時,同時也使得diff結果更具語義(svn/git 的diff命令,就是基於行的diff)。

預處理

除了轉化成基於行的diff之外,在進行LCS/SED之前,可以進行以下預處理操作來加速diff程序

Equality

簡單的判斷text1與text2是否全等,可以免去許多不必要的計算。在一個龐大的項目庫中,往往每次修改的文件只是其中很小的一部分,因此絕大部分的文件都可以通過判斷內容是否全等直接得到結果,無需再進行LCS/SED計算。

Common Prefix/Suffix

提取text1與text2的公共前綴與后綴,再對剩余部分進行diff,可以縮小text1與text2的diff范圍。提取公共前綴/后綴的操作,可以使用二分查找的算法,可以在O(logn)的時間復雜度內完成(log以2為底數)。

Singular Insertion/Deletion

在上一步提取完公共前綴/后綴之后,若是text1剩下的為空字符串,則表示僅僅是新增了text2剩下部分的字符串;若是text2剩下的為空字符串,則表示僅僅是刪除了text1剩下部分的字符串。因此在這種情況下,也可以直接得到結果,無需再進行LCS/SED計算。

Two Edits

同樣再提取公共前綴/后綴之后,剩余部分在頭尾都不會有相同的字符,但是在中間部分可能仍有大量的相同部分,若我們能找到剩余部分的一個公共子串,該公共子串可以將text1、text2剩余部分都分割成兩個部分[text1_pre, text1_suf]、[text2_pre, text2_suf],那么我們就可以轉化成求diff(text1_pre, text2_pre)和diff(text1_suf, text2_suf),再將得到的結果拼接在一起,就可以得到完整的diff結果。為了保證這個分割是有效的,可以規定這個公共字串的長度必需大於較長串長度的二分之一。

計算最長公共子串仍然是一個復雜的程序,但是我們可以利用滿足長度大於二分之一較長串長度的特殊條件,進行特殊查找即可。

 

 

拖動一個二分之一長度的字符串,我們可以發現,這個字符串必然會包含第二個四分之一子串(圖中紅色部分)或者第三個四分之一子串。利用這個關系,我們只需要分別取這個四分之一子串,判斷是否是另一個字符串的子串,若是,則先用此四分之一子串將兩個字符串分隔開,如下圖所示,分別求前部分的公共后綴,以及后半部分的公共前綴,然后將公共后綴+四分之一子串+公共前綴即可得到一個公共子串,如此求完所有的公共子串,取最長的一個,判斷其長度是否大於長串的二分之一即可。

 

 

后處理

后處理主要是將前面計算得到的diffs數組進行merge合並,並且做一些語義化的處理,是的輸出結果更具語義,利於閱讀。

Semantics

對於下圖所示的例子,以下計算出的6個diff結果都是等效的。之所以有這么多等效的結果,是因為diffs的編輯具有可移動性。若一個編輯操作的左右兩邊都是相同部分,滿足:

  1. 編輯部分的第一個字符等於其后相同部分的第一個字符,則此編輯部分可右移一個元素;
  2. 編輯部分的最后一個字符等於其前面相同部分的第一個字符,則此編輯部分可左移一個元素。

 

 

這6個diff結果中,顯然Diff 3與Diff 4是更具有語義的,因此,我們可以指定一個規則,給每個diff結果進行一個評分,得分高的diff結果,則表示它更具有語義。可以有一下幾點評分項目:

  1. 編輯的邊界是一個非字母數字字符,1分;
  2. 編輯的邊界是一個空白符,2分;
  3. 編輯的邊界是一個換行符,3分;
  4. 編輯的邊界是一個空行,4分;
  5. 編輯的邊界不再是相等部分(移動消耗了整個相等部分),5分。

依據以上幾點評分規則,我們可以得到,Diff 1、2、5、6的結果,的0分;Diff 3、4的結果,因為邊界分別有兩個空白符,因此得4分。所以程序就可以判斷出,Diff 3、4得結果,才是更具有語義的。

總結

總結以上的描述,我們可以得出以下完整的Diff程序流程圖:

 

閱讀資料

Diff Strategies
An O(ND) Difference Algorithm and Its Variations∗

 

/**
 * Diff Match —— Longest Common Subsequence
 */
var DiffMatch = (function(){

  var DIFF_EQUAL = 0;
  var DIFF_DELETE = -1;
  var DIFF_INSERT = 1;

  var DIFF_PATH_DIAGONA = '↖︎';
  var DIFF_PATH_VERTICAL = '';
  var DIFF_PATH_HORIZONTAL = '';

  function DiffMatch(){
    this.text1 = '';
    this.text2 = '';

    this.dp = [];
    this.path = [];
  }

  /**
   * 找出兩個文本的差異 以數組返回
   * 返回數組結構: [[DIFF_type, string], ...]
   * @param  {string}   text1  old string
   * @param  {string}   text2  new string
   * @return {array}    array of diffs
   * @author DHB(daihuibin@weidian.com)
   */
  DiffMatch.prototype.diff = function(text1, text2, line_based){
    // 開始時間
    var st = new Date().getTime();

    if(line_based){
      text1 = text1.split('\n');
      text2 = text2.split('\n');

      text1.forEach(function(text, index) {
        text1[index] = text + '\n';
      });

      text2.forEach(function(text, index) {
        text2[index] = text + '\n';
      });
    }

    var len1 = text1.length;
    var len2 = text2.length;

    this.init(text1, text2);

    // 空間優化
    var k = 1;
    var k_subtract_one = 0;

    for(var i = 1; i <= len1; (++i, k_subtract_one = k, k = Number(!k))){
      for(var j = 1; j <= len2; ++j){
        if(text1[i - 1] === text2[j - 1]){
          this.dp[k][j] = this.dp[k_subtract_one][j - 1] + 1;
          this.path[i][j] = DIFF_PATH_DIAGONA;
        } else if(this.dp[k_subtract_one][j] > this.dp[k][j - 1]){
          this.dp[k][j] = this.dp[k_subtract_one][j];
          this.path[i][j] = DIFF_PATH_VERTICAL;
        } else {
          this.dp[k][j] = this.dp[k][j - 1];
          this.path[i][j] = DIFF_PATH_HORIZONTAL;
        }
      }
    }

    this.diffs = this.getDiffsFromPath();

    var et = new Date().getTime();
    this.timeConsumed = et - st;
    return this.diffs;
  };

  /**
   * 數據初始化
   * @param  {string}   text1 old string
   * @param  {string}   text2 new string
   * @return {null}   null
   * @author DHB(daihuibin@weidian.com)
   */
  DiffMatch.prototype.init = function(text1, text2){
    this.text1 = text1;
    this.text2 = text2;

    var len1 = text1.length;
    var len2 = text2.length;

    // 空間優化 2*len2
    this.dp = [];
    for(var i = 0; i < 2; ++i){
      this.dp.push([]);
      for(var j = 0; j <= len2; ++j){
        this.dp[i].push(0);
      }
    }

    this.path = [];
    for(var i = 0; i <= len1; ++i){
      this.path.push([]);
      for(var j = 0; j <= len2; ++j){
        this.path[i].push(0);
      }
    }
  };

  /**
   * 從path中讀出最長公共子序列
   * @return {string}   Longest common Subsequence
   * @author DHB(daihuibin@weidian.com)
   */
  DiffMatch.prototype.getLCS = function(){
    var lcs = '';
    var path = this.path;
    var text = this.text1;

    function path_lcs(i, j){
      if(i === 0 || j === 0){
        return;
      }
      if(path[i][j] === DIFF_PATH_DIAGONA){
        path_lcs(i - 1, j - 1);
        lcs += text[i-1];
      } else if(path[i][j] === DIFF_PATH_VERTICAL){
        path_lcs(i - 1, j);
      } else if(path[i][j] === DIFF_PATH_HORIZONTAL){
        path_lcs(i, j - 1);
      }
    }

    path_lcs(this.text1.length, this.text2.length);

    return lcs;
  };

  /**
   * 從path中獲取diffs
   * @return {array}   差異數組
   * @author DHB(daihuibin@weidian.com)
   */
  DiffMatch.prototype.getDiffsFromPath = function(){
    var diffs = [];

    var path = this.path;
    var text1 = this.text1;
    var text2 = this.text2;

    function path_diffs(i, j){
      if(i === 0 || j === 0){
        if(i !== 0){
          if(typeof text1 === 'string'){
            diffs.push([DIFF_DELETE, text1.substring(0, i)]);
          } else {
            diffs.push([DIFF_DELETE, text1.splice(0, i).join('')]);
          }
        }
        if(j !== 0){
          if(typeof text2 === 'string'){
            diffs.push([DIFF_INSERT, text2.substring(0, j)]);
          } else {
            diffs.push([DIFF_INSERT, text2.splice(0, j).join('')]);
          }
        }
        return;
      }
      if(path[i][j] === DIFF_PATH_DIAGONA){
        path_diffs(i - 1, j - 1);
        diffs.push([DIFF_EQUAL, text1[i - 1]]);
      } else if(path[i][j] === DIFF_PATH_VERTICAL){
        path_diffs(i - 1, j);
        diffs.push([DIFF_DELETE, text1[i - 1]]);
      } else if(path[i][j] === DIFF_PATH_HORIZONTAL){
        path_diffs(i, j - 1);
        diffs.push([DIFF_INSERT, text2[j - 1]]);
      }
    }

    path_diffs(text1.length, text2.length);

    return this.mergeDiffs(diffs);
  };

  /**
   * 合並diffs中的連續相同編輯
   * @param  {array}   df 差異數組
   * @return {array}      合並后的差異數組
   * @author DHB(daihuibin@weidian.com)
   */
  DiffMatch.prototype.mergeDiffs = function(df){
    if(!df || !df.length){
      return [];
    }
    var diffs = [];
    var lst_op = df[0][0];
    var lst_st = df[0][1];

    for(var i = 1; i < df.length; ++i){
      while(i < df.length && df[i][0] === lst_op){
        lst_st += df[i++][1];
      }
      diffs.push([lst_op, lst_st]);
      lst_st = '';
      if(i >= df.length){
        break;
      }
      lst_op = df[i][0];
      lst_st = df[i][1];
    }
    if(lst_st){
      diffs.push([lst_op, lst_st]);
    }
    return diffs;
  };

  /**
   * 獲取用以展示的差異html代碼
   * @return {string}   html代碼
   * @author DHB(daihuibin@weidian.com)
   */
  DiffMatch.prototype.prettyHtml = function(){
    var diffs = this.diffs;
    var html = [];
    var pattern_amp = /&/g;
    var pattern_lt = /</g;
    var pattern_gt = />/g;
    var pattern_para = /\n/g;
    var pattern_blank = / /g;
    var pattern_tab = /\t/g;
    for (var x = 0; x < diffs.length; x++) {
      var op = diffs[x][0];    // Operation (insert, devare, equal)
      var data = diffs[x][1];  // Text of change.
      var text = data.replace(pattern_amp, '&amp;').replace(pattern_lt, '&lt;')
            .replace(pattern_gt, '&gt;')
            .replace(pattern_para, '&para;<br>')
            .replace(pattern_blank, '&nbsp;')
            .replace(pattern_tab, '&nbsp;&nbsp;');
      switch (op) {
        case DIFF_INSERT:
          html[x] = '<ins style="background:#e6ffe6;">' + text + '</ins>';
          break;
        case DIFF_DELETE:
          html[x] = '<del style="background:#ffe6e6;">' + text + '</del>';
          break;
        case DIFF_EQUAL:
          html[x] = '<span>' + text + '</span>';
          break;
      }
    }
    return html.join('');
  }

  /**
   * 查找路徑圖
   * @param  {string}   text1 old string
   * @param  {string}   text2 new string
   * @return {string}   html code of path
   * @author DHB(daihuibin@weidian.com)
   */
  DiffMatch.prototype.pathHtml = function(mark_path){
    var path = this.path;
    var len1 = this.text1.length;
    var len2 = this.text2.length;

    var paths = [];
    if(mark_path){
      var text = this.text1;

      function path_lcs(i, j){
        if(i === 0 || j === 0){
          return;
        }
        paths.push(i + '_' + j);
        if(path[i][j] === DIFF_PATH_DIAGONA){
          path_lcs(i - 1, j - 1);
        } else if(path[i][j] === DIFF_PATH_VERTICAL){
          path_lcs(i - 1, j);
        } else if(path[i][j] === DIFF_PATH_HORIZONTAL){
          path_lcs(i, j - 1);
        }
      }
      path_lcs(this.text1.length, this.text2.length);

    }

    var text = '<table><tr><td>&nbsp;</td>';
    for(var i = 0; i < len2; ++i){
      text += '<td>' + (this.text2[i] !== '\n' ? this.text2[i] : '\\n') + '</td>';
    }
    text += '</tr>';
    for(var i = 1; i <= len1; ++i){
      text += '<tr><td>' + (this.text1[i-1] !== '\n' ? this.text1[i-1] : '\\n') + '</td>';
      for(var j = 1; j <= len2; ++j){
        text += '<td' + (mark_path && paths.indexOf(i+'_'+j) !== -1 ? ' style="color: red;"' : '') + '>' + path[i][j] + '</td>';
      }
      text += '</tr>';
    }
    text += '</table>'
    return text;
  };

  return DiffMatch;
})();

window.DiffMatch = DiffMatch;
// module.exports = DiffMatch;

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM