數據對齊-編輯距離算法詳解(Levenshtein distance)

總結一句話:編輯距離就是從一個字符串變到另一個字符串所須要最小的步驟html

一:簡介

在信息論、語言學和計算機科學中,Levenshtein distance是用於測量兩個字符串之間差別的字符串度量。非正式的說就是兩個單詞之間的Levenshtein distance是將一個單詞更改成另外一個單詞所需的單字符編輯(插入,刪除或替換)的最小步驟。java

它以蘇聯數學家弗拉基米爾·萊文斯坦(Vladimir Levenshtein)的名字命名,做者在1965年提出的這個算法。c++

Levenshtein distance也能夠稱爲編輯距離,儘管該術語也能夠表示更大的距離度量系列。算法

Levenshtein distance與成對字符串對齊密切相關。數組

這裏面主要內容爲我對Levenshtein distance的英文翻譯,也加了一些個人想法~網絡

二:算法定義

1:定義

在兩個字符串a和b之間的Levenshtein distance由下面 定義: post

在這裏插入圖片描述

其中 測試

在這裏插入圖片描述
當ai = bj時等於0,其餘狀況下等於1,
在這裏插入圖片描述
表明a的前i個字節到b的前j個字節的距離。

其中相對於a變化到b字符串來講:spa

  • 在這裏插入圖片描述
    :表明a刪除一個字節去匹配b
  • 在這裏插入圖片描述
    :表明a添加一個字節去匹配b
  • 在這裏插入圖片描述
    :表明匹配或者不匹配,這取決於各個符號是否相同

2:a small case

咱們計算一下kitten和sitting之間的編輯距離翻譯

  • kitten → sitten (替換 "k" -> "s")
  • sitten → sittin (替換 "e" -> "i")
  • sittin → sitting (插入"g").

上面的變化過程所須要的步數就是最小的步數,因此他們之間的編輯距離就是"3"

3:算法的上下界限

Levenshtein distance數值包含幾個上下界限

  • 距離最小是兩個字符串之間的長度的差值
  • 距離最大是兩個字符串中較長字符串的長度
  • 當且僅當字符串相同時長度爲0
  • 當字符串的長度相同時,距離的最大長度是 Hamming distance (下面會介紹一下)
  • 兩個字符串之間的距離小於等於與另一個字符串距離之和(三角形等式 a+b<c)

Hamming distance 是兩個相同長度的字符串從頭開始分別比對兩個字符串對應字符位置的值是否相同,不相同則距離加1,最後獲得的結果就是 Hamming distance 例如abcd、abhg的距離爲2,abcd、bcda的距離是4

三:應用場景

1:數據對齊

筆者在作一個關聯網絡項目時,後臺有兩種特別數據:地址和公司,這兩種數據都是用 戶本身輸入的數據,因此特色就是同一個地點可能有多種不一樣的字符串,就好比同一個地點:「北京市朝陽區IT產業園「,在後臺數據中可能有「北京朝陽區IT產業園」或者「北京朝陽區it園」等一系列數據,咱們又不能去作模糊查詢(由於節點數據和邊關係爲千萬級的,模糊查詢可能會匹配到大量的節點返回致使返回大量的數據影響項目穩定),咱們就採用了數據對齊的方式解決這個問題,當用戶輸入一個地址時,咱們經過編輯距離算法就能夠獲取到其餘相關的數據顯示出來,就能夠達到一個比較好的效果。具體的實現步驟就不在此介紹了。

2:拼寫糾錯

筆者所在公司就有一個公司內部提供的拼寫糾錯的組件,其中就有一部分使用了編輯距離算法。下面是組件的簡單介紹:

糾錯主要解決 query 中輸入錯誤的狀況,好比 query 爲拼音,query中包含同音錯別字或不一樣音的別字,漏字的狀況等等。 本糾錯主要基於兩種規則:拼音糾錯和編輯距離糾錯。 離線主要生成兩個詞典,即拼音詞典和編輯距離詞典。來源詞典主要來自於 cmc 數據,小區數據,topquery,以及白名單數據等。經過 ****腳本 生成拼音詞典和編輯距 離詞典。腳本執行完以後,會在 ***目錄 下生成詞典數據。拼音詞典的生成主要是未來源詞典中的詞轉換爲拼音,編輯距離詞典的生成主要是省略某個字或者某個拼音的字母生成的。生成字典的代碼在 tool 下。 在線糾錯邏輯 經過 make 編譯代碼能夠生成 so 目錄下的動態連接庫。 對外提供的是 java RPC 服務,經過 java jni 連接 c++動態連接庫。

主要糾錯邏輯以下:首先對 query 解析,判斷是全拼音或包含中文。若全是拼音,則會直接走對應的拼音糾錯召回結果,若是不能經過拼音解決,再走編輯距離召回,解決是否漏字母的狀況;如果部分中文或全中文的 query,則先進行拼音糾錯,解決同音錯別字問題,若無召回,則先進行分詞,將先後相鄰 term 拼接在一塊兒進行拼音和編輯距離的召回。

四:其餘的編輯距離算法

還有不少流行的編輯距離算法,他們和Levenshtein distance算法不一樣是使用了不一樣種類的方式去變換字符串

  • Damerau–Levenshtein distance: 能夠對字符串進行插入、刪除、替換、相鄰兩個字符之間的交換
  • longest common subsequence (LCS) distance :只容許對字符串進行插入、刪除、替換
  • Hamming distance : 容許對字符串進行替換,只可用於計算兩個相同長度字符串的編輯距離
  • Jaro distance :只容許對字符串進行交換

編輯距離一般定義爲使用一組特定容許的編輯操做來計算的可參數化度量,併爲每一個操做分配成本(多是無限的)

五:算法實現

1:遞歸實現

這種算法實現比較簡單,就是根據上述介紹的上下界限就能夠得出邏輯了

//實現方法
private static int distance(String a, int len_a, String b, int len_b) {
    //遞歸回歸點
    if (len_a == 0)
        return len_b;
    if (len_b == 0)
        return len_a;
    
    int cos;
    if (a.charAt(len_a-1) == b.charAt(len_b-1))
        cos = 0;
    else
        cos = 1;

    int re1 = distance(a, len_a - 1, b, len_b) + 1;
    int re2 = distance(a, len_a, b, len_b - 1) + 1;
    int re3 = distance(a, len_a - 1, b, len_b - 1) + cos;
    //返回在a中刪除一個字符、在b中刪除一個字符、ab中均刪除一個字符得到結果中取最小值
    return re1 < re2 ? (re1 < re3 ? re1 : re3) : (re2 < re3 ? re2 : re3);
}
//測試
public static void main(String[] args) {
    String a = "kitten";
    String b = "sitting";
    int re = distnace(a, a.length(), b, b.length());
    System.out.println(re);
    //輸出:3
}
複製代碼

這種方式時間複雜度比較高,效率比較低,重複計算了好多字符串,下面採用動態規劃算法實現。

2:動態規劃實現

算法原理部分就借用https://www.cnblogs.com/sumuncle/p/5632032.html 博主的部分文章吧=.=

算法基本原理:假設咱們可使用d[ i , j ]個步驟(可使用一個二維數組保存這個值),表示將串s[ 1…i ] 轉換爲 串t [ 1…j ]所須要的最少步驟個數

那麼,在最基本的狀況下,即在i等於0時,也就是說串s爲空,那麼對應的d[0,j] 就是 增長j個字符,使得s轉化爲t,在j等於0時,也就是說串t爲空,那麼對應的d[i,0] 就是 減小 i個字符,使得s轉化爲t。

而後咱們考慮通常狀況,加一點動態規劃的想法,咱們要想獲得將s[1..i]通過最少次數的增長,刪除,或者替換操做就轉變爲t[1..j],那麼咱們就必須在以前能夠以最少次數的增長,刪除,或者替換操做,使得如今串s和串t只須要再作一次操做或者不作就能夠完成s[1..i]到t[1..j]的轉換。所謂的「以前」分爲下面三種狀況:

  • 1)咱們能夠在k個操做內將 s[1…i] 轉換爲 t[1…j-1]
  • 2)咱們能夠在k個操做裏面將s[1..i-1]轉換爲t[1..j]
  • 3)咱們能夠在k個步驟裏面將 s[1…i-1] 轉換爲 t [1…j-1]

針對第1種狀況,咱們只須要在最後將 t[j] 加上s[1..i]就完成了匹配,這樣總共就須要k+1個操做。 針對第2種狀況,咱們只須要在最後將s[i]移除,而後再作這k個操做,因此總共須要k+1個操做。 針對第3種狀況,咱們只須要在最後將s[i]替換爲 t[j],使得知足s[1..i] == t[1..j],這樣總共也須要k+1個操做。而若是在第3種狀況下,s[i]恰好等於t[j],那咱們就能夠僅僅使用k個操做就完成這個過程。

最後,爲了保證獲得的操做次數老是最少的,咱們能夠從上面三種狀況中選擇消耗最少的一種最爲將s[1..i]轉換爲t[1..j]所須要的最小操做次數。 算法基本步驟:

  • (1)構造 行數爲m+1 列數爲 n+1 的矩陣 , 用來保存完成某個轉換須要執行的操做的次數,將串s[1..n] 轉換到 串t[1…m] 所須要執行的操做次數爲matrix[n][m]的值;
  • (2)初始化matrix第一行爲0到n,第一列爲0到m。Matrix[0][j]表示第1行第j-1列的值,這個值表示將串s[1…0]轉換爲t[1..j]所須要執行的操做的次數,很顯然將一個空串轉換爲一個長度爲j的串,只須要j次的add操做,因此matrix[0][j]的值應該是j,其餘值以此類推。
  • (3)檢查每一個從1到n的s[i]字符;
  • (4)檢查每一個從1到m的s[i]字符;
  • (5)將串s和串t的每個字符進行兩兩比較,若是相等,則讓cost爲0,若是不等,則讓cost爲1(這個cost後面會用到);
  • (6)
    • a、若是咱們能夠在k個操做裏面將s[1..i-1]轉換爲t[1..j],那麼咱們就能夠將s[i]移除,而後再作這k個操做,因此總共須要k+1個操做。
    • b、若是咱們能夠在k個操做內將 s[1…i] 轉換爲 t[1…j-1] ,也就是說d[i,j-1]=k,那麼咱們就能夠將 t[j] 加上s[1..i],這樣總共就須要k+1個操做。
    • c、若是咱們能夠在k個步驟裏面將 s[1…i-1] 轉換爲 t [1…j-1],那麼咱們就能夠將s[i]轉換爲 t[j],使得知足s[1..i] == t[1..j],這樣總共也須要k+1個操做。(這裏加上cost,是由於若是s[i]恰好等於t[j],那麼就不須要再作替換操做,便可知足,若是不等,則須要再作一次替換操做,那麼就須要k+1次操做)
    • 由於咱們要取得最小操做的個數,因此咱們最後還須要將這三種狀況的操做個數進行比較,取最小值做爲d[i,j]的值;
    • d、而後重複執行3,4,5,6,最後的結果就在d[n,m]中;
private static int distance(String a, String b) {
    int[][] dis = new int[a.length()+1][b.length()+1];
    for (int i = 1; i <= a.length(); i++)
        dis[i][0] = i;
    for (int j = 1; j <= b.length(); j++)
        dis[0][j] = j;
    int cas;
    for (int j = 1; j <= b.length(); j++) {
        for (int i = 1; i <= a.length(); i++) {
            if (a.charAt(i-1) == b.charAt(j-1))
                cas = 0;
            else
                cas = 1;
            int re = Math.min(dis[i - 1][j] + 1, dis[i][j - 1] + 1);
            dis[i][j] = Math.min(re, dis[i - 1][j - 1] + cas);
        }
    }
    return dis[a.length() - 1][b.length() - 1];
}

public static void main(String[] args) {
    String a = "kitten";
    String b = "sitting";
    int re = distance(a, b);
    System.out.println(re);
    //輸出:3
}
複製代碼

若是轉載此博文,請附上本文連接,謝謝合做~ :juejin.im/user/5c3036…

若是感受這篇文章對您有所幫助,請點擊一下「喜歡」或者「關注」博主,您的喜歡和關注將是我前進的最大動力!

相關文章
相關標籤/搜索