使用動態規劃 實現字符級Diff & Patch

文章開頭先上demo,只需鍵入任意內容的兩個字符串,頁面上就能自動計算並呈現字符串之間的差分。html

demo地址:string-diff-demo.herokuapp.comgit

源碼地址:github.com/lqt0223/str…github

動態規劃

動態規劃(dynamic programming)是你們在算法學習中都會遇到的話題之一。我我的對於它的理解是:算法

  • 動態規劃針對的是規模較大的問題 
  • 但就像遞歸那樣,問題的base case是有解的
  • 而且同一個問題的較大規模版本的解,能夠經過一組規則,從已有的較小規模的同一問題的解中推導而出
  • 動態規劃與遞歸不一樣的是,後者是利用了方法定義了在其過程當中調用本身,「聲明式」(declaratively)地造成了整個求解的過程;前者須要創建動態規劃矩陣,用以記錄每一問題規模下的解,並須要顯式地執行循環來不斷擴大問題規模和求解,這是「命令式」(imperatively)地形式了求解的過程

能夠被動態規劃解決的問題,常見的有:數組

  • 揹包問題(knapsack problem):給定一個容量有限的揹包,與一組帶有重量和價值的物品,如何選擇其中的幾個放入揹包使得價值的和最大
  • 最長公共序列問題(longest common sequence problem,下文簡稱爲LCS問題):求兩個字符串中,最長的公共序列。公共序列指的是在兩個字符串中都出現的序列,這個序列在原字符串中不必定是連續的
  • 子集和問題(subset sum problem):給定一個整數集合,是否存在它的非空子集使子集內的數字和爲0
  • ...

最長公共序列問題與Diff & Patch算法的關係

曾經,我本身在codewar等網站上作算法題時,不少次刷到"longest common sequence"或者相似的題目,也經過一些算法書瞭解到這類問題的一個比較易於理解的算法是動態規劃。但我一直不太明白這類問題的實際應用何在。直到最近看到了下面的論文:瀏覽器

An O(ND) Difference Algorithm and Its Variations - EUGENE W. MYERS數據結構

此文我只讀了其中的1-2節,總結一下它的內容實際上是:使用圖算法,求解兩個字符串之間的LCS,以及最短編輯步驟(shortest edit script,如下簡稱SES,指的是從字符串A變換至字符串B,所須要的步驟。步驟是針對字符串的操做,例如刪除某一位置上的字符、在某一位置上插入字符等)。app

今後文可知:LCS和SES是對偶問題(dual problem),這兩個問題只不過是一個優化問題的兩個方面。即,當咱們尋找兩個字符串的公共子序列時,若是已經找到了最優解(最長公共子序列),那麼在此最優解情形下的兩個字符串之間的編輯步驟,也就是最短編輯步驟。通俗地講,求解LCS的過程當中,咱們就能夠獲得SES工具

因爲SES描述了從一個字符串到另外一個字符串的一系列操做步驟,這就相似於各種數據比較工具產生的差量數據。因而咱們知道了,LCS問題的實際應用之一,就是數據的比較、差量計算和差量更新。學習

使用矩陣轉化LCS和SES問題

因爲是用動態規劃來求解LCS和SES問題,咱們須要用到矩陣(二維數組)來記錄最優解的一些信息。

這一小節主要是說明在使用矩陣求解以上問題的過程當中,矩陣有哪些性質,以及這些性質對應着LCS或SES問題的什麼方面。這些內容也是對於上一小節中提到的論文第2節內容的概括和簡化。

若是以前沒有接觸過使用動態規劃求解LCS問題的話,能夠看一下下面的視頻,從而對於這一求解過程有一個基本概念。

Longest Common Subsequence - Tushar Roy - Youtube

整體來講,使用矩陣轉化並求解LCS和SES問題須要如下三個階段:

  1. 初始化階段。假設字符串A長度爲m,字符串B長度爲n,則初始化一個m + 1 * n + 1的矩陣,將矩陣的第一列和第一行都初始化爲0(矩陣中後續須要填入的是LCS的長度,因此在初始化時,第一行或第一列表示兩個字符串中的任意一個爲空的狀況,須要填入0)
  2. 推算階段。從左至右從上到下,根據必定的推演規則,填寫矩陣。即,不斷地求解字符串A或B的前綴之間的LCS的長度
  3. 回溯(backtracking)階段。當矩陣填滿時,位於矩陣最右下角的值便是字符串A和B的LCS的長度。若是須要進一步找出LCS是什麼,則須要從矩陣的右下角出發,按必定的規則,找到一個到達矩陣左上角的路徑,保證通過路徑時,LCS的長度值每次減少0或1。

通過三個階段後,矩陣會變成相似下圖的形式。

圖中是字符串A爲"abcabba",字符串B爲"cbabac"時,使用動態規劃求解LCS和SES造成的矩陣。由此矩陣咱們能夠得出如下關於兩個字符串之間的LCS和SES的相關答案:

  1. 兩個字符串的LCS長度爲4(即矩陣最右下角位置所填入的值)
  2. 兩個字符串的LCS爲"caba"(這是完成回溯後,經過觀察紅色箭頭所造成的路徑而得來;觀察上圖可知,回溯階段時,每一次遇到須要向左上角移動的狀況下,該座標對應的字符串A內的某一字符與字符串B內的某一字符相同,即這個字符能夠做爲LCS的組成字符之一)
  3. 回溯時的每一次移動均可以映射爲SES中的某一步:
    1. 向左上角移動,意味着找到了組成LCS的一個字符串,對於SES來講,表示不須要操做
    2. 向左移動,對於SES來講,意味着在字符串A中的指定位置刪除字符
    3. 向上移動
      1. 若是是在矩陣的第1列(也就是所有被初始化爲0的最左邊一列)向上移動,對於SES來講,意味着在字符串A的頭部添加字符
      2. 若是是在矩陣的其餘列向上移動,對於SES來講,意味着在字符串A的指定位置的後面添加字符

例:字符串A爲"abcabba",字符串B爲"cbabac"時,如何知道通過什麼樣的步驟,能夠最快地將字符串A變爲字符串B呢?咱們可使用上面的規則,將紅色路徑翻譯成咱們須要的SES

  1. 刪除字符串A的第一、2個字符(最左上角的兩個向左箭頭)
  2. 在字符串A的第3個位置添加字符"b"(從左上至右下的第四個向上箭頭) 
  3. 刪除字符串A的第6個字符(從左上至右下的倒數第三個向左箭頭)
  4. 在字符串A的第7個位置添加字符"c"(最右下角的向上箭頭)

通過上述操做後咱們就能夠將字符串A變換爲字符串B

SES的同時操做問題

上一節的末尾給出了從"abcabba"到"cbabac"的SES,也許你試着用草稿紙或者其餘工具來使用這段SES,但卻沒法順利地完成字符串的轉換。這是由於:SES所表示的編譯步驟,須要被同時操做。這個說法比較抽象,下面使用"abcabba"到"cbabac"例子,說明SES的正確用法:

原字符串

a b c a b b a
複製代碼
  1. 刪除字符串A的第一、2個字符(最左上角的兩個向左箭頭)(這裏用*標記將要被刪除的字符)

    * * c a b b a
    複製代碼
  2. 在字符串A的第3個位置添加字符"b"(從左上至右下的第四個向上箭頭)

    * * c a b b a
          b
    複製代碼
  3. 刪除字符串A的第6個字符(從左上至右下的倒數第三個向左箭頭)

    * * c a b * a
          b
    複製代碼
  4. 在字符串A的第7個位置添加字符"c"(最右下角的向上箭頭)

    * * c a b * a
          b       c
    複製代碼
  5. 將以上相似於hashTable的結構還原爲一個字符串,規則爲:遇到須要刪除的字符時則忽略,遇到縱向伸展的list時將其連綴爲一個子字符串,最後將全部子字符串按順序鏈接,即獲得"cbabac"

由此可知,SES的同時操做,指的是任何一個操做步驟,都不該該影響到字符串最初的字符排列。咱們能夠用這種縱向的數據結構,從新整理字符串操做,並在最後轉換成目標字符串。

差分可視化

如上一小節所示,SES的應用之一就是直接執行,其結果就是生成目標字符串。

咱們也能夠結合原字符串和SES,生成DOM String,在瀏覽器中將原字符串到目標字符串的差分呈現出來。本文開頭的demo便是對於這種應用方式的展現。

後記

不只是字符級的diff & patch,若是在不考慮算法空間複雜度的狀況下,動態規劃也能夠簡單地實現單詞級、行級的diff & patch。

學習和實現這個算法給我最大的體會是:

  • 使用圖形化的表示和求解過程來轉化問題,能讓一些看似複雜的問題變得直觀和簡單(例如使用矩陣來記錄和求解LCS)
  • 一些已經掌握的算法和算法思想,通過再思考,有時能獲得意想不到的更大的收穫
相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息