在“文本比较算法Ⅰ——LD算法”中,介绍了编辑距离的计算。
在“文本比较算法Ⅱ——Needleman/Wunsch算法”中,介绍了最长公共子串的计算。
在给定的字符串A和字符串B,LD(A,B)表示编辑距离,LCS(A,B)表示最长公共子串的长度。
如何来度量它们之间的相似度呢?
不妨设S(A,B)来表示字符串A和字符串B的相似度。那么,比较合理的相似度应该满足下列性质。
性质一:0≤S(A,B)≤100%,0表示完全不相似,100%表示完全相等
性质二:S(A,B)=S(B,A)
目前,网上介绍的各种相似度的计算,都有各自的不尽合理的地方。
计算公式一:S(A,B)=1/(LD(A,B)+1)
能完美的满足性质二。
当LD(A,B)=0时,S(A,B)=100%,不过无论LD(A,B)取任何值,S(A,B)>0,不能满足性质一。
计算公式二:S(A,B)=1-LD(A,B)/Len(A)
当Len(B)>Len(A)时,S(A,B)<0。不满足性质一。
有人会说,当S(A,B)<0时,强制指定S(A,B)=0就解决问题了。
问题是,S(A,B)=1-LD(A,B)/Len(A),而S(B,A)=1-LD(B,A)/Len(B)。当Len(A)≠Len(B)时,S(A,B)≠S(B,A)。不满足性质二
还有一个例子可以说明问题
A="BC",B="CD",C="EF"
S(A,B)=1-LD(A,B)/Len(A)=1-2/2=0
S(A,C)=1-LD(A,C)/Len(A)=1-2/2=0
A和B的相似度与A和C的相似度是一样的。不过很明显的是B比C更接近A
计算公式三:S(A,B)=LCS(A,B)/Len(A)
这个公式能完美的满足的性质一
不过当Len(A)≠Len(B)时,S(A,B)≠S(B,A)。不满足性质二
用一个例子说明问题
A="BC",B="BCD",C="BCEF"
S(A,B)=LCS(A,B)/Len(A)=2/2=100%
S(A,C)=LCS(A,C)/Len(A)=2/2=100%
A和B的相似度与A和C的相似度是一样的。不过很明显的是B比C更接近A
上面是网上能找到的三个计算公式,从上面的分析来看都有各自的局限性。
我们看一个例子:
A=GGATCGA,B=GAATTCAGTTA,LD(A,B)=5,LCS(A,B)=6
他们的匹配为:
A:GGA_TC_G__A
B:GAATTCAGTTA
可以看出上面蓝色的部分表示的是LCS部分,黑色表示的是LD部分。
因此,给出一个新的公式
S(A,B)=LCS(A,B)/(LD(A,B)+LCS(A,B))
这个公式能解决上述三个公式的种种不足。
而LD(A,B)+LCS(A,B)表示两个字符串A、B的最佳匹配字串的长度。这个是唯一的。
还有注意的是LD(A,B)+LCS(A,B)和Max(Len(A),Len(B))这两个并不完全相等。
实现代码如下:
package algorithm;
import java.io.IOException;
/**
* Levenshtein Distance 算法实现
* 可以使用的地方:DNA分析 拼字检查 语音辨识 抄袭侦测
* 相似度公式:S(A,B)=LCS(A,B)/(LD(A,B)+LCS(A,B))
* LCS为最长公共子串长度,LD是编辑距离
*/
public class Levenshtein {
public static void main(String[] args) throws IOException {
//要比较的两个字符串
String str1 = "【性教育】当孩子能听懂言语时,家长应把性教育贯穿在日常生活中,如在洗澡、着装、修整发型及玩具选择等方面要有明确的性别区分。还可通过书报、画册、影视、讲故事等进行引导,使孩子对性别产生一种自然的认识,从而使他们接受、认识生命本质,使性自认得以完成。";
String str2 = "【对孩子进行适当的性教育】当孩子能听懂言语时,家长应把性教育贯穿在日常生活中,如在洗澡、着装、修整发型及玩具选择等方面要有明确的性别区分。还可通过书报、画册、影视、讲故事等进行引导,使孩子对性别产生一种自然的认识,从而使他们接受、认识生命本质,使性自认得以完成。";
System.out.println(levenshtein(str1, str2));
}
/**
* DNA分析 拼字检查 语音辨识 抄袭侦测
* <p/>
* 加入了LCS
*/
public static float levenshtein(String str1, String str2) {
//计算两个字符串的长度。
int len1 = str1.length();
int len2 = str2.length();
//建立上面说的数组,比字符长度大一个空间
/**
* 若ai=bj,则LD(i,j)=LD(i-1,j-1)
* 若ai≠bj,则LD(i,j)=Min(LD(i-1,j-1),LD(i-1,j),LD(i,j-1))+1
*/
int[][] dif = new int[len1 + 1][len2 + 1];
/**
* 若ai=bj,则LCS(i,j)=LCS(i-1,j-1)+1
* 若ai≠bj,则LCS(i,j)=Max(LCS(i-1,j-1),LCS(i-1,j),LCS(i,j-1))
*/
int[][] lcs = new int[len1 + 1][len2 + 1];
//赋初值,步骤B。
for (int a = 0; a <= len1; a++) {
dif[a][0] = a;
lcs[a][0] = 0;
}
for (int a = 0; a <= len2; a++) {
dif[0][a] = a;
lcs[0][a] = 0;
}
//计算两个字符是否一样,计算左上的值
int temp;
for (int i = 1; i <= len1; i++) {
for (int j = 1; j <= len2; j++) {
if (str1.charAt(i - 1) == str2.charAt(j - 1)) {
temp = 0;
} else {
temp = 1;
}
//取三个值中最小的
dif[i][j] = min(dif[i - 1][j - 1] + temp, dif[i][j - 1] + 1,
dif[i - 1][j] + 1);
if (temp == 0)
lcs[i][j] = lcs[i - 1][j - 1] + 1;
else
lcs[i][j] = max(lcs[i - 1][j - 1], lcs[i][j - 1], lcs[i - 1][j]);
}
}
//取数组右下角的值,同样不同位置代表不同字符串的比较
// System.out.println("差异步骤:" + dif[len1][len2]);
//计算相似度
/**
* 这个计算公式有弊端,假设A="BC",B="CD",C="EF"
S(A,B)=1-LD(A,B)/Max(Len(A),Len(B))=1-2/2=0
S(A,C)=1-LD(A,C)/Max(Len(A),Len(C))=1-2/2=0
A和B的相似度与A和C的相似度是一样的。不过很明显的是B比C更接近A
*/
// float similarity = 1 - (float) dif[len1][len2] / Math.max(str1.length(), str2.length());
float similarity = (float) lcs[len1][len2] / (dif[len1][len2] + lcs[len1][len2]);
return similarity;
}
//得到最小值
private static int min(int... is) {
int min = Integer.MAX_VALUE;
for (int i : is) {
if (min > i) {
min = i;
}
}
return min;
}
//得到最大值
private static int max(int... is) {
int max = Integer.MIN_VALUE;
for (int i : is) {
if (max < i) {
max = i;
}
}
return max;
}
}