Myers差分算法是由Eugene W.Myers在1986年發表的一篇論文中提出,能夠查看文末連接1。javascript
咱們常用的git diff
使用的就是此算法java
先簡單定義下diff: 經過一系列的"編輯",將字符串a轉爲字符串b
diff結果有不少,譬如能夠將一個字符串所有刪除,再添加另外一整個字符串。能夠刪一個字符添加一個字符…… 不出意外,這個數量是個指數級的git
那什麼樣的diff是"更好的"算法
具體能夠查看"參考"連接2,這裏再也不贅述chrome
總結大概幾點條件:數組
因此,如今問題是,從一個字符串轉另外一個字符串同時知足上述條件oop
科學家告訴咱們,此抽象問題能夠建模爲一個圖搜索的問題優化
假設字符串a = 'ABCABBA'
b = 'CBABAC'
ui
那麼編輯圖看起來就以下所示spa
a的長度記爲N,這裏N = 7,b的長度記爲M,這裏M = 6
每一個點均可以對應一個座標(x, y)
如今從(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... ? 貌似均可行……
尋找最短編輯腳本的問題簡化爲尋找一條從(0, 0)到(N, M)有最少橫向和豎向邊數路徑的問題
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,匹配儘量多的相同字符,就不會出現這樣的狀況
爲了理解前面的引理及名詞,如今求d = 3時,即d-path的各個終點座標
將上圖轉換爲下表
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)
僞代碼
一些說明:
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