最小编辑距离是指将一个错误拼写的单词纠正正确的最小编辑次数,这里的编辑包含插入、删除、修改三种操做,每一次编辑只能改变一个字母。由于这个概念是俄罗斯科学家 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。可是这些都仍是本身想法,不容易实现。欢迎指点。
最后,免责声明,本人水平有限,若有错误,欢迎指正。