假设您有一个包含10,000个电子邮件地址的列表,并且您希望找到此列表中某些最接近的“邻居” - 定义为与列表中其他电子邮件地址可疑接近的电子邮件地址
我知道如何计算两个字符串之间的Levenshtein distance(感谢this question),这将为我提供将一个字符串转换为另一个字符串所需的操作数。 / p>
假设我将“怀疑接近另一个电子邮件地址”定义为Levenshtein得分低于N的两个字符串。
除了将每个可能的字符串与列表中的每个其他可能字符串进行比较之外,是否有更有效的方法来查找分数低于此阈值的字符串对?换句话说,这种问题可以比O(n^2)
更快地解决吗?
对于这个问题,Levenshtein的算法选择是否很差?
答案 0 :(得分:7)
是的 - 您可以使用BK-Tree在O(log n)时间内找到字符串给定距离内的所有字符串。当levenshtein距离为1时,涉及生成距离为n的每个字符串的替代解决方案可能会更快,但是工作量会在较长距离内迅速失控。
答案 1 :(得分:4)
你可以在O(kl)
中使用Levenshtein,其中k
是你的最大距离,l是最大字符串。
基本上当你知道如何计算基本的Levenshtein时,很容易弄清楚距离主对角线的k
以外的每个结果都必须大于k
。因此,如果计算宽度为2k + 1
的主对角线就足够了。
如果您有10000个电子邮件地址,则无需更快的算法。计算机可以用O(N^2)
足够快地计算。
Levenshtein非常善于解决这类问题。
您可能会考虑在比较之前使用soundex转换电子邮件。你可能会得到更好的结果。
答案 2 :(得分:4)
此问题称为群集,是较大的重复数据删除问题的一部分(您可以在其中确定群集的哪个成员是“正确的”) ,也称为 merge-purge 。
我曾经读过一些关于这个主题的研究论文(名称在下面),基本上,作者在排序的字符串列表中使用有限大小的滑动窗口。他们只会比较(使用edit distance算法)N * N字符串在窗口内,从而降低计算复杂度。如果任何两个字符串看起来相似,则将它们组合成集群(通过将记录插入单独的集群表中)。
第一次通过列表之后是第二遍,其中字符串颠倒,然后才进行排序。这样,具有不同头部的字符串有另一个机会足够接近,可以作为同一窗口的一部分进行评估。在第二遍中,如果一个字符串看起来足够接近窗口中的两个(或更多)字符串,并且这些字符串已经是它们自己的集群的一部分(通过第一次传递找到),那么这两个集合将合并(通过更新群集表),当前字符串将添加到新合并的群集中。这种聚类方法称为 union-find 算法。
然后他们通过用顶部X 基本上独特的原型列表替换窗口来改进算法。每个新字符串将与每个顶级X原型进行比较。如果字符串看起来足够接近其中一个原型,那么它将被添加到原型的集群。如果没有一个原型看起来足够相似,那么该字符串将成为一个新的原型,将最旧的原型推出前X列表。 (有一种启发式逻辑用于决定原型集群中的哪些字符串应该用作代表整个集群的新原型)。同样,如果字符串看起来类似于几个原型,那么它们的所有集群都将被合并。
我曾经为名称/地址记录的重复数据删除实施了这个算法,列表的大小大约是1000到5000万条记录,并且它的工作非常快(并且发现重复也很好)。
总体而言,对于此类问题,最棘手的部分当然是找到相似性阈值的正确值。我们的想法是捕获所有重复数据,而不会产生太多误报。具有不同特征的数据往往需要不同的阈值。编辑距离算法的选择也很重要,因为有些算法对于OCR错误更好,而其他算法更适合打字错误,而其他算法更适合语音错误(例如通过电话获取名称时)。
一旦实现了聚类算法,测试它的一个好方法是获取一个独特样本列表,并且人为地改变每个样本以产生其变体,同时保留所有变体都有的事实来自同一个父母。然后将该列表混洗并馈送到算法。将原始聚类与重复数据删除算法生成的聚类进行比较,可以得到效率得分。
Hernandez M. 1995,大型数据库的合并/清除问题。
Monge A. 1997,一种用于检测近似重复数据库记录的高效的与域无关的算法。
答案 3 :(得分:2)
我认为你不能做得比O(n ^ 2)更好,但是你可以做一些较小的优化,这可能足以使你的应用程序可用:
答案 4 :(得分:1)
10,000个电子邮件地址听起来不是太多。对于较大空间中的相似性搜索,您可以使用shingling和min-hashing。该算法实现起来有点复杂,但在大空间上效率更高。
答案 5 :(得分:1)
在扭转问题的情况下,可以做得更好。
我想这里你的10.000地址非常“固定”,否则你将不得不添加更新机制。
想法是使用Levenshtein距离,但是在'反向'模式下,在Python中:
class Addresses:
def __init__(self,addresses):
self.rep = dict()
self.rep[0] = self.generate_base(addresses)
# simple dictionary which associate an address to itself
self.rep[1] = self.generate_level(1)
self.rep[2] = self.generate_level(2)
# Until N
generate_level
方法会生成上一组中所有可能的变体,减去之前级别已存在的变化。它将“origin”保留为与键关联的值。
然后,你只需要在各种集合中查找你的单词:
def getAddress(self, address):
list = self.rep.keys()
list.sort()
for index in list:
if address in self.rep[index]:
return (index, self.rep[index][address]) # Tuple (distance, origin)
return None
这样做,你计算一次各种集合(需要一些时间......但是你可以将它序列化并永久保存)。
然后查找比O(n ^ 2)更有效,尽管它确实很难,因为它取决于生成的集合的大小。
答案 6 :(得分:0)
假设您有3个字符串:
1 - “abc” 2 - “bcd” 3 - “cde”
1& 1之间的L距离2是2(减去'a',加'd')。 2和2之间的L距离3是2(减去'b',加'e')。
您的问题是我们是否可以推断1和1之间的L距离。 3通过使用上面的2个比较。答案是否定的。
1& 1之间的L距离3是3(替换每个字符),由于前2个计算的分数,无法推断出这一点。分数未显示是否执行了删除,插入或替换操作。
所以,我会说Levenshtein是一个不错的选择。
答案 7 :(得分:0)
如果你真的在比较电子邮件地址,那么一个明显的方法是将levenshtein算法与域映射相结合。我可以想到,当我使用相同的域名多次注册某些内容时,可以考虑电子邮件地址的用户名部分的变体。