今天讲编辑距离。
编辑距离为我们提供了一种量化这两种关于字符串相似度的直觉的方法。更正式地说,两个字符串之间的最小编辑距离定义为将一个字符串转换为另一个字符串所需的最小编辑操作(插入、删除、替换等操作)数量。
如上,图中第一行字符串和第二行字符串分别是对比的两串,两行字符串中间的竖线表示的是这两串字符挨个字符进行对比并对齐,对齐的过程中肯定有字符不一样的,那这个时候,第三行字符表示该字符对比后是需要删除(d)还是替换(s)还是插入(i)。
我们如何找到最小编辑距离?我们可以把它看作一个搜索任务,在这个任务中,我们正在搜索从一个字符串到另一个字符串的最短路径——一个编辑序列。
所有可能的编辑空间是巨大的,所以我们不能天真地搜索。然而,许多不同的编辑路径最终会处于相同的状态(字符串),所以我们可以只记住每次看到状态的最短路径,而不是推荐所有这些路径。我们可以通过使用动态规划来做到这一点。动态规划是一类算法的名称,首先由Bellman(1957)提出,它应用表驱动方法通过组合子问题的解来解决问题。自然语言处理中一些最常用的算法利用了动态规划,例如Viterbi算法(第8章)和CKY算法(第13章)。
最小编辑距离(Levenshtein Distance)是指将一个字符串转换成另一个字符串所需的最少操作次数。这里的操作包括:插入一个字符、删除一个字符、替换一个字符。
下面是对应的Java代码:
public class EditDistance {
public static int minDistance(String word1, String word2) {
int n = word1.length();
int m = word2.length();
// 定义状态数组dp[i][j]为word1[0:i-1]和word2[0:j-1]之间的编辑距离
int[][] dp = new int[n+1][m+1];
// 初始化数组
for (int i = 0; i <= n; i++) {
dp[i][0] = i;
}
for (int j = 0; j <= m; j++) {
dp[0][j] = j;
}
// 动态规划求解
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (word1.charAt(i-1) == word2.charAt(j-1)) {
dp[i][j] = dp[i-1][j-1];
} else {
dp[i][j] = Math.min(dp[i-1][j], Math.min(dp[i][j-1], dp[i-1][j-1])) + 1;
}
}
}
return dp[n][m];
}
public static void main(String[] args) {
String word1 = "kitten";
String word2 = "sitting";
int distance = minDistance(word1, word2);
System.out.println(distance); // 输出结果为3
}
}
在上面的代码中,我们使用了一个二维数组 dp
来记录状态。其中,dp[i][j]
表示将字符串 word1
的前 i
个字符转换成字符串 word2
的前 j
个字符所需的最小编辑距离。
首先,我们初始化 dp
数组的第一行和第一列,分别表示将空串转换成 word1
和 word2
所需的距离。
然后,我们利用动态规划的思想,从左到右、从上到下地遍历 dp
数组,根据当前字符是否相同来更新 dp[i][j]
的值。如果相同,则不需要进行任何操作,直接继承上一个状态的编辑距离;否则,我们可以进行三种操作中的一种(插入、删除、替换),取这三种操作中编辑距离最小的那个值加 1,作为当前状态的编辑距离。
最后,返回 dp[n][m]
就是将 word1
转换成 word2
所需的最小编辑距离。