问题描述
我正在使用Levenshtein distance来获取C#实现,该实现不仅可以判断两个字符串是否相似,而且还可以在更大的字符串中找到相似的字符串(
为此,我尝试遵循this excellent post底部的建议,但是遇到了一些问题。
首先,我采用了this implementation,并对其进行了更改以满足我的其他要求。在this other post的启发下,我还添加了一些诊断转储支持,以使我更好地理解算法。
我的实现返回一个对象,该对象具有分数,(如果需要)索引和长度,以及对用于诊断目的的计算矩阵的引用:
public class LevenshteinMatch
{
public int score { get; }
public int Index { get; }
public int Length { get; }
public int[,] Matrix { get; set; }
public LevenshteinMatch(int score,int index = 0,int length = 0)
{
score = score;
Index = index;
Length = length;
}
public override string ToString()
{
return $"{score} @{Index}x{Length}";
}
}
这是我的实现:如果distance
为false,则sub
方法“正常”工作;否则,它将找到类似的子字符串。 DumpMatrix
只是诊断助手方法。
public static class Levenshtein
{
public static string DumpMatrix(int[,] d,string a,string b)
{
if (d == null) throw new ArgumentNullException(nameof(d));
if (a == null) throw new ArgumentNullException(nameof(a));
if (b == null) throw new ArgumentNullException(nameof(b));
// # k i t t e n
// 00 01 02 03 04 05 06
// # 00 .. .. .. .. .. .. ..
// s 01 .. .. .. .. .. .. ..
// ...etc (sitting)
StringBuilder sb = new StringBuilder();
int n = a.Length;
int m = b.Length;
// b-legend
sb.Append(" # ");
for (int j = 0; j < m; j++) sb.Append(b[j]).Append(" ");
sb.AppendLine();
sb.Append(" 00 ");
for (int j = 1; j < m; j++) sb.AppendFormat("{0:00}",j).Append(' ');
sb.AppendFormat("{0:00} ",m).AppendLine();
// matrix
for (int i = 0; i <= n; i++)
{
// a-legend
if (i == 0)
{
sb.Append("# 00 ");
}
else
{
sb.Append(a[i - 1])
.Append(' ')
.AppendFormat("{0:00}",i)
.Append(' ');
}
// row of values
for (int j = 0; j <= m; j++)
sb.AppendFormat("{0,2} ",d[i,j]);
sb.AppendLine();
}
return sb.ToString();
}
private static LevenshteinMatch BuildMatch(string a,string b,int[,] d)
{
int n = a.Length;
int m = b.Length;
// take the min rightmost score instead of the bottom-right corner
int min = 0,rightMinIndex = -1;
for (int j = m; j > -1; j--)
{
if (rightMinIndex == -1 || d[n,j] < min)
{
min = d[n,j];
rightMinIndex = j;
}
}
// corner case: perfect match,just collect m chars from score=0
if (min == 0)
{
return new LevenshteinMatch(min,rightMinIndex - n,n);
}
// collect all the lowest scores on the bottom row leftwards,// up to the length of the needle
int count = n,leftMinIndex = rightMinIndex;
while (leftMinIndex > -1)
{
if (d[n,leftMinIndex] == min && --count == 0) break;
leftMinIndex--;
}
return new LevenshteinMatch(min,leftMinIndex - 1,rightMinIndex + 1 - leftMinIndex);
}
public static LevenshteinMatch distance(string a,bool sub = false,bool withmatrix = false)
{
if (a is null) throw new ArgumentNullException(nameof(a));
if (b == null) throw new ArgumentNullException(nameof(b));
int n = a.Length;
int m = b.Length;
int[,] d = new int[n + 1,m + 1];
if (n == 0) return new LevenshteinMatch(m);
if (m == 0) return new LevenshteinMatch(n);
for (int i = 0; i <= n; i++) d[i,0] = i;
// if matching substring,leave the top row to 0
if (!sub)
{
for (int j = 0; j <= m; j++) d[0,j] = j;
}
for (int j = 1; j <= m; j++)
{
for (int i = 1; i <= n; i++)
{
if (a[i - 1] == b[j - 1])
{
d[i,j] = d[i - 1,j - 1]; // no operation
}
else
{
d[i,j] = Math.Min(Math.Min(
d[i - 1,j] + 1,// a deletion
d[i,j - 1] + 1),// an insertion
d[i - 1,j - 1] + 1 // a substitution
);
}
}
}
LevenshteinMatch match = sub
? BuildMatch(a,b,d)
: new LevenshteinMatch(d[n,m]);
if (withmatrix) match.Matrix = d;
return match;
}
}
为了更完整,这里是使用它的演示控制台程序。这只会提示用户输入匹配模式(是否为子字符串)和两个字符串,然后调用distance
方法,转储结果矩阵,并在需要时显示子字符串。
internal static class Program
{
private static string ReadLine(string defaultLine)
{
string s = Console.ReadLine();
return string.IsNullOrEmpty(s) ? defaultLine ?? s : s;
}
private static void Main()
{
Console.WriteLine("Fuzzy Levenshtein Matcher");
string a = "sitting",b = "kitten";
bool sub = false;
LevenshteinMatch match;
while (true)
{
Console.Write("sub [y/n]? ");
string yn = Console.ReadLine();
if (!string.IsNullOrEmpty(yn)) sub = yn == "y" || yn == "Y";
Console.Write(sub? $"needle ({a}): " : $"A ({a}): ");
a = ReadLine(a);
Console.Write(sub? $"haystack ({b}): " : $"B ({b}): ");
b = ReadLine(b);
match = Levenshtein.distance(a,sub,true);
Console.WriteLine($"{a} - {b}: {match}");
Console.WriteLine(Levenshtein.DumpMatrix(match.Matrix,a,b));
if (sub) Console.WriteLine(b.Substring(match.Index,match.Length));
}
}
}
现在,对于子字符串匹配,这在“ c abba c”中的“ aba”情况下有效。这是矩阵:
aba - c abba c: 1 @3x3
# c a b b a c
00 01 02 03 04 05 06 07 08
# 00 0 0 0 0 0 0 0 0 0
a 01 1 1 1 0 1 1 0 1 1
b 02 2 2 2 1 0 1 1 1 2
a 03 3 3 3 2 1 1 1 2 2
在其他情况下,例如“ ego sum abbas Cucaniensis”中的“ abas”,我无法从最下面的行中收集最低分数:
abas - ego sum abbas Cucaniensis: 1 @-2x15
# e g o s u m a b b a s C u c a n i e n s i s
00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
# 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
a 01 1 1 1 1 1 1 1 1 1 0 1 1 0 1 1 1 1 1 0 1 1 1 1 1 1 1
b 02 2 2 2 2 2 2 2 2 2 1 0 1 1 1 2 2 2 2 1 1 2 2 2 2 2 2
a 03 3 3 3 3 3 3 3 3 3 2 1 1 1 2 2 3 3 3 2 2 2 3 3 3 3 3
s 04 4 4 4 4 4 3 4 4 4 3 2 2 2 1 2 3 4 4 3 3 3 3 4 3 4 3
此处底行只有一个分数= 1。在完美匹配(分数= 0)的情况下,我的代码仅从最右边的最低得分中获取了左N个字符(其中N是针的长度)。但是在这里我的分数大于0。可能是我误解了上面帖子中的提示,因为我是这种算法的新手。有人能建议在干草堆中找到针的索引和长度的正确方法吗?
解决方法
您从最下面一行的最高分开始:(13,4)的1分
然后您会发现可能使您到达那里的前任状态和过渡:
- (12,4)-不可能,因为它具有更高的差异
- (13,3)-不可能,因为它具有更高的差异
- (12,3)-相同的区别和字符匹配,因此可行
从(12,3)开始,按照相同的步骤到达(11,2),然后到达(10,1)
在(10,1)处字母不匹配,因此您不可能来自(9,0)。您可以将(10,0)用于相似的字符串“ bas”,也可以将(9,1)然后将(8,0)用于相似的字符串“ abbas”,两者的距离均为1。