簡析Myers

Myers差分算法是由Eugene W.Myers在1986年發表的一篇論文中提出,能夠查看文末連接1javascript

咱們常用的git diff使用的就是此算法java

高質量的diff

先簡單定義下diff: 經過一系列的"編輯",將字符串a轉爲字符串b
diff結果有不少,譬如能夠將一個字符串所有刪除,再添加另外一整個字符串。能夠刪一個字符添加一個字符…… 不出意外,這個數量是個指數級的git

那什麼樣的diff是"更好的"算法

具體能夠查看"參考"連接2,這裏再也不贅述chrome

總結大概幾點條件:數組

  1. 最小的改動
  2. 刪除優先新增
  3. 新增或刪除的內容應該遵循代碼結構

因此,如今問題是,從一個字符串轉另外一個字符串同時知足上述條件oop

科學家告訴咱們,此抽象問題能夠建模爲一個圖搜索的問題優化

編輯圖

假設字符串a = 'ABCABBA' b = 'CBABAC'ui

那麼編輯圖看起來就以下所示spa

a的長度記爲N,這裏N = 7,b的長度記爲M,這裏M = 6

每一個點均可以對應一個座標(x, y)

graph

如今從(0, 0)點出發,這時字符串爲a, 經過向右移動,即增長x值,至關於從a中刪除一個字符,例如從(0, 0)到(1, 0)意味着刪除字符'A'。經過向下移動,即增長y值,至關於從b中增長一個字符,例如從(1, 0)到(1, 1)意味着插入字符'C',此時經過前面兩步移動,咱們獲得編輯後的字符串爲 'CBCABBA'

除了向右,向下移動,咱們還能夠斜向移動,例如點(2, 0),因爲a的第3個字符爲'C',b的第一個字符爲'C',即表示有相同字符,保留便可,不增長也不刪除。注意,斜向移動是無代價的,由於在其上移動是對字符串不作任何改動

一些名詞羅列及解釋

Trace

路徑中斜線邊的"匹配點",組成的序列,長度爲L

Shortest Edit Script(SES)

最短編輯腳本。僅包含兩種命令: 刪除和添加
從(0, 0)到(N, M)腳本刪除 N - L 個字符,添加了 M - L 個字符
因此對每個trace,有一個對應的編輯腳本 D = N + M - 2L

Longest Common Subsequence(LCS)

最長公共子序列。高質量diff中條件1,代表須要尋找兩個字符串的LCS
LCS大小等於Trace的長度L

論文說明了,尋找LCS的問題與尋找一條從(0, 0)到(N, M)同時有最多數量斜邊的問題(*) 是等價的
尋找SES與尋找一條從(0, 0)到(N, M)同時有最少數量的非斜邊的問題(**) 是等價的
而(*)和(**)是同一問題的兩個方面

如今考慮給圖加權重,橫向和豎向邊權重爲1,斜向邊權重爲0
那麼LCS/SES 問題就等同於在 這個權重編輯圖中 尋找一條從(0, 0)到)(N, M)代價最低的一條路徑

你想到了什麼? bfs, dijkstra, 或者是dp... ? 貌似均可行……

一個O((M+N)D)貪心算法

尋找最短編輯腳本的問題簡化爲尋找一條從(0, 0)到(N, M)有最少橫向和豎向邊數路徑的問題

graph

一些名詞羅列及解釋

snake

蛇形線
一條snake表示 橫(豎)向移動一步後接跟着的儘量多的斜向移動 組成的線
如:從(0, 1)移動到(0, 2)順着斜線一直到(2, 4),(0, 1) - (0, 2) - (2, 4)組成一條snake
如上圖深藍色的線

diagonal k

斜線k,k(斜)線
定義 k = x - y, k相同的點組成一條直線,他們是相互平行的斜線
如上圖中的棕黃色的線

d-path

移動d步的點的連線
如上圖淺藍色的線

兩個引理及證實

引理1 一個D-path的終點必然在k斜線上,其中k ∈ { -D, -D + 2, ... D -2 , D}

概括法證實:

0-path(最多包含斜線,不然就是0點)必然是在 斜線 0上

假設D-path終點在k上,k ∈ { -D, -D + 2, ... D -2 , D},那麼 (D+1)-path,由 前D-path組成,假設終點在k線上,那麼橫(豎)向移動一步後,終點必然在k+1, k-1線上,後面即便跟着斜線依然在k+1, k-1線上。

因此 (D+1)-path 終點必然在斜線 {-D ± 1, (-D + 2) ± 1 ... D ± 1} = { -D - 1, -D + 1, ... D - 1, D + 1},所以得證。

此引理說明,當D是奇數時,D-path都落在奇數k線上,D是偶數時,D-path都落在偶數k線上

引理2 0-path的最遠到達點爲(x, x),其中x ∈ min(z - 1 || az ≠ bz or z > M 或 z > N)。D-path的最遠到達點在k線上,能夠被分解爲在k-1 線上的(D-1)-path,跟着一條橫向邊,接着一條越長越好的斜邊 和 在k+1 線上的(D-1)-path,跟着一條豎向邊,接着一條越長越好的斜邊

證實看論文吧

此引理包含了一條貪婪原則: D-path能夠經過貪婪地延伸(D-1)-path的最遠到達點得到

這就符合高質量diff的條件1,3,匹配儘量多的相同字符,就不會出現這樣的狀況

code structure

舉個例子

爲了理解前面的引理及名詞,如今求d = 3時,即d-path的各個終點座標

edit graph

將上圖轉換爲下表

k = -3, 只能從 k = -2 向下移動,即(2, 4)向下移動至(2, 5)經斜線至(3, 6)

k = -1

​ 能夠由k=-2向右移動,即(2, 4)向右移動至(3, 4)經斜線至(4, 5)

​ 也可由k=0向下移動,即(2, 2)向下移動至(2, 3)

​ 由於一樣在k = -1線上,(4, 5)比(2, 3)更遠,因此咱們選擇k=-2這條

k = 1

​ 能夠由k = 0向右移動,即(2, 2)向右移動至(3, 2)經斜線至(5, 4)

​ 也可由k = 2向下移動,即(3, 1)向下移動至(3, 2)經斜線至(5, 4)

​ 咱們會挑選起點x值更大一些的,由於刪除優先嘛,因此選擇k = 2這條

k = 3, 只能從 k = 2 向右移動,即(3, 1)向右移動至(4, 1)經斜線至(5, 2)

簡單實現

僞代碼

一些說明:

  • V數組包含D-path的最遠到達點, V[-D], V[-D+2]...V[D-2], V[D]
  • v[k]中存儲的值,爲在k線上最遠達到點的橫軸座標值x,由於y能夠經過x - k計算
  • v[1] = 0,設置一個起點爲(0, -1),用於查找0-path的最遠到達點

js: 直接粘貼到chrome控制檯試試?

function myers(stra, strb) {
  let n = stra.length
  let m = strb.length

  let v = {
    '1': 0
  }
  let vs = {
    '0': { '1': 0 }
  }
  let d

  loop:
  for (d = 0; d <= n + m; d++) {
    let tmp = {}

    for (let k = -d; k <= d; k += 2) {
      let down = k == -d || k != d && v[k + 1] > v[k - 1]
      let kPrev = down ? k + 1 : k - 1

      let xStart = v[kPrev]
      let yStart = xStart - kPrev

      let xMid = down ? xStart : xStart + 1
      let yMid = xMid - k

      let xEnd = xMid
      let yEnd = yMid

      while(xEnd < n && yEnd < m && stra[xEnd] === strb[yEnd]) {
        xEnd++
        yEnd++
      }
      
      v[k] = xEnd
      tmp[k] = xEnd

      if (xEnd == n && yEnd == m) {
        vs[d] = tmp
        let snakes = solution(vs, n, m, d)
        printRes(snakes, stra, strb)

        break loop
      }
    }

    vs[d] = tmp
  }
}

function solution(vs, n, m, d) {
  let snakes = []
  let p = { x: n, y: m }
  
  for (; d > 0; d--) {
    let v = vs[d]
    let vPrev = vs[d-1]
    let k = p.x - p.y

    let xEnd = v[k]
    let yEnd = xEnd - k
    
    let down = k == -d || k != d && vPrev[k + 1] > vPrev[k - 1]
    let kPrev = down ? k + 1 : k - 1
    
    let xStart = vPrev[kPrev]
    let yStart = xStart - kPrev
    
    let xMid = down ? xStart : xStart + 1
    let yMid = xMid - k
    
    snakes.unshift([xStart, xMid, xEnd])

    p.x = xStart
    p.y = yStart
  }

  return snakes
}

function printRes(snakes, stra, strb) {
  let grayColor = 'color: gray'
  let redColor = 'color: red'
  let greenColor = 'color: green'
  let consoleStr = ''
  let args = []
  let yOffset = 0
  
  snakes.forEach((snake, index) => {
    let s = snake[0]
    let m = snake[1]
    let e = snake[2]
    let large = s
    
    if (index === 0 && s !== 0) {
      for (let j = 0; j < s; j++) {
        consoleStr += `%c${stra[j]}`
        args.push(grayColor)
        yOffset++
      }
    }
    
    // 刪除
    if (m - s == 1) {
      consoleStr += `%c${stra[s]}`
      args.push(redColor)
      large = m
    // 添加
    } else {
      consoleStr += `%c${strb[yOffset]}`
      args.push(greenColor)
      yOffset++
    }
    
    // 不變
    for (let i = 0; i < e - large; i++) {
      consoleStr += `%c${stra[large+i]}`
      args.push(grayColor)
      yOffset++
    }
  })

  console.log(consoleStr, ...args)
}

let s1 = 'ABCABBA'
let s2 = 'CBABAC'
myers(s1, s2)
複製代碼

時間複雜度: 指望爲O(M+N+D^2),最壞狀況爲爲O((M+N)D)
我的補充說明一下,這個最壞狀況發生在兩字符串a, b幾乎相等的狀況,發生的機率很小

空間複雜度: O(D^2)

優化

論文中給出了O(D)空間複雜度的一個優化,之後抽空再寫吧。有興趣的朋友能夠在參考中查看

參考

[1] http://xmailserver.org/diff2.pdf

[2] http://cjting.me/misc/how-git-generate-diff/

[3] https://blog.jcoglan.com/2017/02/12/the-myers-diff-algorithm-part-1/

[4] https://www.codeproject.com/articles/42279/investigating-myers-diff-algorithm-part-of

相關文章
相關標籤/搜索