2018-04-12 21:20:30
编辑距离是针对二个字符串(例如英文字)的差异程度的量化量测,量测方式是看至少需要多少次的处理才能将一个字符串变成另一个字符串。编辑距离可以用在自然语言处理中,例如拼写检查可以根据一个拼错的字和其他正确的字的编辑距离,判断哪一个(或哪几个)是比较可能的字。DNA也可以视为用A、C、G和T组成的字符串,因此编辑距离也用在生物信息学中,判断二个DNA的类似程度。Unix 下的 diff 及 patch 即是利用编辑距离来进行文本编辑对比的例子。
常用的编辑距离算法有:
- Levenshtein距离,在莱文斯坦距离中,可以删除、加入、取代字符串中的任何一个字元,也是较常用的编辑距离定义,常常提到编辑距离时,指的就是莱文斯坦距离。
- LCS(最长公共子序列)距离,只允许删除、加入字元。
一、最长公共子序列 LCS
最长公共子序列问题是很经典的动态规划问题,问题描述如下:
LCS是Longest Common Subsequence的缩写,即最长公共子序列。一个序列,如果是两个或多个已知序列的子序列,且是所有子序列中最长的,则为最长公共子序列。
子序列: 一个序列A = a1,a2,……an,中任意删除若干项,剩余的序列叫做A的一个子序列。也可以认为是从序列A按原顺序保留任意若干项得到的序列。
例如:对序列 1,3,5,4,2,6,8,7来说,序列3,4,8,7 是它的一个子序列。对于一个长度为n的序列,它一共有2^n 个子序列,有(2^n – 1)个非空子序列。
请注意:子序列不是子集,它和原始序列的元素顺序是相关的。
时间复杂度:对于一般性的LCS问题(即任意数量的序列)是属于NP-hard。但当序列的数量确定时,问题可以使用动态规划(Dynamic Programming)在多项式时间内解决。
public int LCS(String s1, String s2) { if (s1.length() == 0 || s2.length() == 0) return 0; int len1 = s1.length(); int len2 = s2.length(); int[][] dp = new int[len1 + 1][len2 + 1]; for (int i = 0; i <= len2; i++) dp[0][i] = 0; for (int i = 0; i <= len1; i++) dp[i][0] = 0; for (int i = 1; i <= len1; i++) { for (int j = 1; j <= len2; j++) { int same = s1.charAt(i - 1) == s2.charAt(j - 1) ? 1 : 0; dp[i][j] = Math.max(Math.max(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1] + same); } } return dp[len1][len2]; }
二、莱文斯坦距离 LevenshteinDistcance
莱文斯坦距离,又称Levenshtein距离,是编辑距离的一种。指两个字串之间,由一个转成另一个所需的最少编辑操作次数。允许的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。
例如将kitten一字转成sitting:
- sitten (k→s)
- sittin (e→i)
- sitting (→g)
俄罗斯科学家弗拉基米尔·莱文斯坦在1965年提出这个概念。
为了进一步的度量两个字符串的相似程度,我们在得到最少的操作次数后,进行如下的计算:
1 - res / max(len1, len2)
和LCS几乎一样,依然是使用动态规划算法进行求解,以下分别是伪代码和Java实现:
int LevenshteinDistcance(string str1[1..lenStr1], string str2[1..lenStr2]) int d[0..lenStr1, 0..lenStr2] int i, j, cost for i = 0 to lenStr1 d[i, 0] := i for j = 0 to lenStr2 d[0, j] := j for i = 1 to lenStr1 for j = 1 to lenStr2 if str1[i] = str2[j] cost := 0 else cost := 1 d[i, j] := min( d[i-1, j ] + 1, // 删除 d[i , j-1] + 1, // 插入 d[i-1, j-1] + cost // 替換 ) return d[lenStr1, lenStr2]
public double levenshteinDistcance(String s1, String s2) { if (s1.length() == 0) return s2.length(); if (s2.length() == 0) return s1.length(); int len1 = s1.length(); int len2 = s2.length(); int[][] d = new int[len1 + 1][len2 + 1]; for (int i = 0; i <= len1; i++) d[i][0] = i; for (int i = 0; i <= len2; i++) d[0][i] = i; for (int i = 1; i <= len1; i++) { for (int j = 1; j <= len2; j++) { int same = s1.charAt(i - 1) == s2.charAt(j - 1) ? 0 : 1; d[i][j] = Math.min(Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1), d[i - 1][j - 1] + same); } } // 得到最少的操作次数后,计算公式为: // 1 - res / max(len1, len2) return 1 - d[len1][len2] * 1.0 / Math.max(len1, len2); }