最小編輯距離是指將一個錯誤拼寫的單詞糾正正確的最小編輯次數,這裏的編輯包含插入、刪除、修改三種操做,每一次編輯只能改變一個字母。由於這個概念是俄羅斯科學家 Vladimir Levenshtein 在1965年提出來的,因此編輯距離又稱爲Levenshtein距離。
java
就拿‘Levenshtein’這個單詞舉例說明好了,Levenshtein做爲一我的名,很容易會被拼寫錯誤。假設如今有一個錯誤拼寫的Lavensting. 那麼他們的編輯距離是多少呢?算法
參考正確的單詞Levenshtein,能夠看出,由Lavensting糾正爲Levenshtein的步驟爲:ide
Lavensting V.S. Levenshtein測試
1. 將第二個字母a修改成e;優化
2. 在第六個字母s後面插入h;spa
3. 在第七個字母t後面插入e;code
4. 將最後一個字母g刪除。blog
這裏咱們進行了四次操做,因此Lavensting和Levenstein的編輯距離是4. 並且咱們能夠目測出這已是最小編輯距離了。繼承
那麼咱們怎麼使用最小編輯距離爲錯誤拼寫的單詞進行糾正呢?原理很簡單,也很粗暴。用單詞庫裏面的全部單詞與錯誤拼寫的單詞計算最小編輯距離,最小編輯距離最小的單詞,便極可能是正確的單詞,也就是糾正的結果。ip
接下來,接下來即是如何使用計算機求解最小編輯距離。動態規劃常常被用來做爲這個問題的解決手段之一。
筆者水平有限,動態規劃難以描述清楚,這裏給一個定義:動態規劃是經過把原問題分解爲相對簡單的子問題的方式求解複雜問題的方法。
下面是java代碼:
package com.mzule.al; public class LevenshteinDistance { public double distance(String w1,String w2){ double[][] m = new double[w1.length()+1][w2.length()+1]; for(int i=0;i<m.length;i++){ m[i][0]=i; } for(int i=0;i<m[0].length;i++){ m[0][i]=i; } for(int i=1;i<m.length;i++){ for(int j=1;j<m[0].length;j++){ m[i][j] = min(m[i][j-1]+1,m[i-1][j]+1,m[i-1][j-1]+cost(w1.charAt(i-1),w2.charAt(j-1))); } } return m[w1.length()][w2.length()]; } protected double cost(char c1,char c2) { return c1==c2?0:1; } protected double min(double i, double j, double k) { double t = i<j?i:j; return t<k?t:k; } }
上面的核心代碼是兩個for循環中的部分:
m[i][j] = min(m[i][j-1]+1,m[i-1][j]+1,m[i-1][j-1]+cost(w1.charAt(i-1),w2.charAt(j-1)));
這段代碼能夠這樣理解,例如咱們已知ab與a、a與a、a與ac的距離,計算字符串 ab與ac的距離,有三個方式,對應於編輯操做的插入、刪除、修改三種操做:
1. 在ab與a的距離基礎上+1,由於ac多了一個c;
2. 在a與ac的距離的基礎上+1,由於ab多了一個b;
3. 在a與a的距離的基礎上加上b與c的距離,b與c的距離很簡單,由於b與c不相等,爲1
測試上面的代碼運行效果:
public static void main(String[] args) { double d = new LevenshteinDistance().distance("Lavensting", "Levenshtein"); System.out.println(d); }
輸出的結果爲:4.0,和咱們以前目測的結果同樣。
好了,如今咱們能夠用這個算法去作拼寫糾錯了。
可是這個算法不夠完美,由於沒有考慮到鍵盤距離。
在上面程序中的cost方法,只是簡單的對相同的字符返回0,不一樣的字符返回1。這種狀況下,cost(‘a’,‘p’)和cost(‘a’,‘s’)的值是同樣的,都是1. 可是他們應該是不同的,由於a被誤輸入爲s的機率比誤輸入爲p的機率大得多,由於在鍵盤上,a與s是鄰居,手指很容易誤按,而p與a距離太遠,用戶輸入p基本上都是真實的想法。
因此咱們要對上面的算法進行改進,引入新的cost計算機制:
package com.mzule.al; import java.util.HashMap; import java.util.Map; public class KeyboardLevenshteinDistance extends LevenshteinDistance { private static final Map<Character, String> charSiblings; private static final double SCORE_MIS_HIT = 0.1; static { charSiblings = new HashMap<>(); charSiblings.put('q', "was"); charSiblings.put('w', "qsead"); charSiblings.put('e', "wsdfr"); charSiblings.put('r', "edfgt"); charSiblings.put('t', "rfghy"); charSiblings.put('y', "tghju"); charSiblings.put('u', "yhjki"); charSiblings.put('i', "ujklo"); charSiblings.put('o', "ikl;p"); charSiblings.put('p', "ol;'["); charSiblings.put('a', "qwsxz"); charSiblings.put('s', "qazxcdew"); charSiblings.put('d', "wsxcvfre"); charSiblings.put('f', "edcvbgtr"); charSiblings.put('g', "rfvbnhyt"); charSiblings.put('h', "tgbnmjuy"); charSiblings.put('j', "yhnm,kiu"); charSiblings.put('k', "ujm,.loi"); charSiblings.put('l', "ik,./;po"); charSiblings.put('z', "asx"); charSiblings.put('x', "zasdc"); charSiblings.put('c', "xsdfv"); charSiblings.put('v', "cdfgb"); charSiblings.put('b', "vfghn"); charSiblings.put('n', "bghjm"); charSiblings.put('m', "nhjk,"); } @Override protected double cost(char c1, char c2) { return keyboardDistance(c1, c2); } private double keyboardDistance(char c1, char c2) { if (c1 == c2) { return 0; } String s = charSiblings.get(c1); if (s != null && s.indexOf(c2) > -1) { return SCORE_MIS_HIT; } return 1; } }
上面的類繼承自LevenshteinDistance ,重寫了cost方法,根據鍵盤距離,對相鄰的字母返回0.1,不相鄰的字母返回距離1.
cost('a','s')=0.1
cost('a','p')=1
測試KeyboardLevenshteinDistance:
public static void main(String[] args) { double d = new KeyboardLevenshteinDistance().distance("thanks", "tjsmla"); System.out.println(d); }
輸出:0.5,和預期的結果同樣。tjsmla與thanks很是類似,由於在新買的鍵盤仍是不熟悉的狀況下,誤輸入thanks爲tjsmla也很正常。
上面的算法完美了嗎?of course not. 還有不少優化空間。好比:除了鍵盤距離,咱們還能夠考慮讀音距離,對於讀音類似的字母,也應該距離更近一些。好比說a與e,就很容易就混淆。對於結尾的t與te的距離應該更近一些,而不是1。可是這些都仍是本身想法,不容易實現。歡迎指點。
最後,免責聲明,本人水平有限,若有錯誤,歡迎指正。