场景
在搜索引擎项目中,我用到了最短编辑距离算法,用于对用户输入的查询进行纠错,从而优化查询结果。比如说,我们在输入英文单词的时候,由于疏忽或者记忆不准确,会有拼写错误的情况。以单词beautiful 为例,假设我们在搜索引擎中输入beauitful(我故意拼错了),看看会发生什么。
如下图所示,虽然我把这个单词拼错了,但是查询结果提示“including results for beautiful”,也就是说,它似乎知道我的查询输入拼写错误,并根据某种算法,给我推荐了一个与之最近似的单词(大概率就是本应正确拼写的单词)。在这里,就用到了最短编辑距离算法。这个场景,也称为模糊搜索。
最短编辑距离
什么是最短编辑距离呢?假定有两个字符串s1和s2,允许对字符串进行以下三种操作:
1. 插入一个字符
2. 删除一个字符
3. 替换一个字符
将字符串s1转换成字符串s2的最少操作次数就是字符串s1和字符串s2之间的最短编辑距离。两个字符串的最短编辑距离最短,意味着两个字符串越相似。
例1 :s1 = "geek",s2 = "gesek"
我们只需要在s1中插入一个字符,就可以把s1转换为s2,因此,这两个字符串的最短编辑距离就是1
例2:s1 = "cat",s2 = "cut"
我们只需要在s1中替换一个字符,就可以把s1转换为s2,因此,这两个字符串的最短编辑距离就是1
例3:s1 = "sunday",s2 = "saturday"
由于第1个字符和最后3个字符是一样的,因此,我们只要考虑“un”和"atur"即可。首先,把'n'替换成'r',再插入'a'、't',因此最短编辑距离是3
以上面例3进行说明,我们从字符串的最后一位开始比较,由于最后一位都是'y',因此,不需要任何操作,也就是说,两者的最短编辑距离等价于"sunda"和"saturda"的最短编辑距离,即d("sunday", "saturday") = d("sunda", "saturda")。因此,如果在比较的过程中遇到了相同的字符,那么二者的最短编辑距离就等价于除了这个字符之外,剩余字符的最短编辑距离,即d(i, j) = d(i-1, j-1)。
如果比较的字符不一致,比方说,现在已经比较到了"sun"和"satur",根据允许的操作,我们有以下3种操作:
(1)插入:在s1末尾插入一个字符'r'(即"sunr"),由于此时末尾字符都是'r',因此就变成了比较"sun"和"satu"的编辑距离,即d("sun", "satur") = d("sun", "satu") + 1,也可以写成d(i, j) = d(i, j-1) + 1。+1 表示当前进行了一次字符操作。
(2)删除:删除s1的最后一个字符,并考察s1剩下的部分与s2的距离。即d("sun", "satur") = d("su", "satur") + 1,也可以写成d(i, j) = d(i-1, j) + 1。
(3)替换:把s1的最后一个字符替换为s2的最后一个字符,即变成了"sur",因此即d("sun", "satur") = d("su", "satu") + 1,也可以写成d(i, j) = d(i-1, j-1) + 1。
基于上述分析,我们就可以很快写出递归的代码。如下:
static int min(int x,int y,int z) { if (x<=y && x<=z) return x; if (y<=x && y<=z) return y; else return z; } static int editDist(String str1 , String str2 , int m ,int n) { // If first string is empty, the only option is to // insert all characters of second string into first if (m == 0) return n; // If second string is empty, the only option is to // remove all characters of first string if (n == 0) return m; // If last characters of two strings are same, nothing // much to do. Ignore last characters and get count for // remaining strings. if (str1.charAt(m-1) == str2.charAt(n-1)) return editDist(str1, str2, m-1, n-1); // If last characters are not same, consider all three // operations on last character of first string, recursively // compute minimum cost for all three operations and take // minimum of three values. return 1 + min ( editDist(str1, str2, m, n-1), // Insert editDist(str1, str2, m-1, n), // Remove editDist(str1, str2, m-1, n-1) // Replace ); }
但是我们都知道递归会存在大量的重复计算,因此,显然不是最优解。在这里,我们可以利用动态规划的思想来进行优化。
假设dp[i][j]表示s1[i]与s2[j]的最短编辑距离,根据之前的分析,可以写出如下代码:
static int editDistDP(String str1, String str2, int m, int n) { // Create a table to store results of subproblems int dp[][] = new int[m+1][n+1]; // Fill d[][] in bottom up manner for (int i=0; i<=m; i++) { for (int j=0; j<=n; j++) { // If first string is empty, only option is to // insert all characters of second string if (i==0) dp[i][j] = j; // Min. operations = j // If second string is empty, only option is to // remove all characters of second string else if (j==0) dp[i][j] = i; // Min. operations = i // If last characters are same, ignore last char // and recur for remaining string else if (str1.charAt(i-1) == str2.charAt(j-1)) dp[i][j] = dp[i-1][j-1]; // If the last character is different, consider all // possibilities and find the minimum else dp[i][j] = 1 + min(dp[i][j-1], // Insert dp[i-1][j], // Remove dp[i-1][j-1]); // Replace } } return dp[m][n]; }
时间复杂度:O(m*n)
空间复杂度:O(m*n)
参考: