一个快速、高效的Levenshtein算法实现
Levenshtein算法,用于计算两个字符串之间的Levenshtein距离。而Levenshtein距离又称为编辑距离,是指两个字符串之间,由一个转换成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。
概述
Levenshtein距离用来描述两个字符串之间的差异。我在一个网络爬虫程序里面使用这个算法来比较两个网页之间的版本,如果网页的内容有足够多的变动,我便将它更新到我的数据库。
说明
原来的算法是创建一个大小为StrLen1*StrLen2的矩阵。如果所有字符串加起来是1000个字符那么长的话,那么这个矩阵就会是1M;如果字符 串是10000个字符,那么矩阵就是100M。如果元素都是整数(这里是指数字,Int32)的话,那么矩阵就会是4*100M == 400MB这么大,唉……
现在的算法版本只使用2*StrLen个元素,这使得后面给出的例子成为2*10,000*4 = 80 KB。其结果是,不但内存占用更少,而且速度也变快了!因为这使得内存分配只需要很少的时间来完成。当两个字符串的长度都是1k左右时,新算法的效率是旧 算法的两倍!
示例
原来的版本将会创建一个矩阵[6+1, 5+1],而我的新算法将会创建两个向量[6+1](黄色元素)。在这两个算法版本中,字符串的顺序是无关紧要、无所谓的,也就是说,它也可以是矩阵[5+1, 6+1]和两个向量[5+1]。
新的算法
步骤
步骤 | 说明 |
---|---|
1 | 设置n为字符串s的长度。("GUMBO") 设置m为字符串t的长度。("GAMBOL") 如果n等于0,返回m并退出。 如果m等于0,返回n并退出。 构造两个向量v0[m+1] 和v1[m+1],串联0..m之间所有的元素。 |
2 | 初始化 v0 to 0..m。 |
3 | 检查 s (i from 1 to n) 中的每个字符。 |
4 | 检查 t (j from 1 to m) 中的每个字符 |
5 | 如果 s[i] 等于 t[j],则编辑代价为 0; 如果 s[i] 不等于 t[j],则编辑代价为1。 |
6 | 设置单元v1[j]为下面的最小值之一: a、紧邻该单元上方+1:v1[j-1] + 1 b、紧邻该单元左侧+1:v0[j] + 1 c、该单元对角线上方和左侧+cost:v0[j-1] + cost |
7 | 在完成迭代 (3, 4, 5, 6) 之后,v1[m]便是编辑距离的值。 |
本小节将演示如何计算"GUMBO"和"GAMBOL"两个字符串的Levenshtein距离。
步骤1、2
v0 | v1 | |||||
G | U | M | B | O | ||
0 | 1 | 2 | 3 | 4 | 5 | |
G | 1 | |||||
A | 2 | |||||
M | 3 | |||||
B | 4 | |||||
O | 5 | |||||
L | 6 |
步骤3-6,当 i = 1
v0 | v1 | |||||
G | U | M | B | O | ||
0 | 1 | 2 | 3 | 4 | 5 | |
G | 1 | 0 | ||||
A | 2 | 1 | ||||
M | 3 | 2 | ||||
B | 4 | 3 | ||||
O | 5 | 4 | ||||
L | 6 | 5 |
步骤3-6,当 i = 2
v0 | v1 | |||||
G | U | M | B | O | ||
0 | 1 | 2 | 3 | 4 | 5 | |
G | 1 | 0 | 1 | |||
A | 2 | 1 | 1 | |||
M | 3 | 2 | 2 | |||
B | 4 | 3 | 3 | |||
O | 5 | 4 | 4 | |||
L | 6 | 5 | 5 |
步骤3-6,当 i = 3
v0 | v1 | |||||
G | U | M | B | O | ||
0 | 1 | 2 | 3 | 4 | 5 | |
G | 1 | 0 | 1 | 2 | ||
A | 2 | 1 | 1 | 2 | ||
M | 3 | 2 | 2 | 1 | ||
B | 4 | 3 | 3 | 2 | ||
O | 5 | 4 | 4 | 3 | ||
L | 6 | 5 | 5 | 4 |
步骤3-6,当 i = 4
v0 | v1 | |||||
G | U | M | B | O | ||
0 | 1 | 2 | 3 | 4 | 5 | |
G | 1 | 0 | 1 | 2 | 3 | |
A | 2 | 1 | 1 | 2 | 3 | |
M | 3 | 2 | 2 | 1 | 2 | |
B | 4 | 3 | 3 | 2 | 1 | |
O | 5 | 4 | 4 | 3 | 2 | |
L | 6 | 5 | 5 | 4 | 3 |
步骤3-6,当 i = 5
v0 | v1 | |||||
G | U | M | B | O | ||
0 | 1 | 2 | 3 | 4 | 5 | |
G | 1 | 0 | 1 | 2 | 3 | 4 |
A | 2 | 1 | 1 | 2 | 3 | 4 |
M | 3 | 2 | 2 | 1 | 2 | 3 |
B | 4 | 3 | 3 | 2 | 1 | 2 |
O | 5 | 4 | 4 | 3 | 2 | 1 |
L | 6 | 5 | 5 | 4 | 3 | 2 |
步骤7
编辑距离就是矩阵右下角的值,v1[m] == 2。由"GUMBO"变换为"GAMBOL"的过程对于我来说是很只管的,即通过将"A"替换为"U",并在末尾追加"L"这样子(实际上替换的过程是由移除和插入两个操作组合而成的)。
改良
如果您确信你的字符串永远不会超过2^16(65536)个字符,那么你可以使用ushort来表示而不是int,如果字符串少于2^8个,还可以使用byte。我觉得这个算法用非托管代码实现的话可能会更快,但我没有试过。
参考文献
public static int levenshtein_new(String str1,String str2){ int n = str1.length(); int m = str2.length(); if(n==0){ return m; } else if(m==0){ return n; } else{ int v0[] = new int[m+1]; int v1[] = new int[m+1]; for(int i=0;i<m+1;i++){ v0[i] = i; } for(int i=0;i<m+1;i++){ v1[i] = 0; } int cost = 0; for(int i=1;i<n+1;i++){ v1[0] = i; for(int j=1;j<m+1;j++){ if(str1.charAt(i-1) == str2.charAt(j-1)){ cost = 0; } else{ cost = 1; } int a = v0[j]+1; int b = v1[j-1]+1; int c = v0[j-1]+cost; v1[j] = min(a,b,c); } for(int k=0;k<m+1;k++){ v0[k]=v1[k]; } } return v1[m]; }