【问题标题】:Finding similar names better than O(n^2)找到比 O(n^2) 更好的相似名称
【发布时间】:2018-05-11 18:23:29
【问题描述】:

假设我有一个名字列表。不幸的是,有一些重复,但其中哪些是重复的并不明显。

Tom Riddle
Tom M. Riddle
tom riddle
Tom Riddle, PhD.

我正在考虑使用 Levenshtein distance,而且肯定还有其他算法可以同时比较 2 个名称。

但在名称列表中,无论字符串距离算法如何,我最终都会生成一个比较输出网格 (n^2)。

如何避免O(n^2) 的情况?

【问题讨论】:

  • 倒排索引是一个很好的解决方案。但是对于西方名字,也许简单地用非字母字符(例如空格)分割就足够了,然后计算两个名字的相似度,就像 Lucene 所做的那样,ElasticsearchSolr 已经提供了易于使用的解决方案这样的要求。
  • 您想让“Thomas Riddle”和“Tomas Riddle”匹配“Tom Riddle”吗?那么“谜语,汤姆”呢?

标签: algorithm fuzzy-search


【解决方案1】:

简介

您要执行的操作称为Fuzzy search。让我引导你完成这个主题。

首先,设置 n-grams (Wikipedia) 的倒排索引 (Wikipedia)。也就是像"hello"这样的词拆分成,比如3-grams:

"$$h", "$he", "hel", "ell", "llo", "lo$", "o$$"

并有一个映射,将每个 n-gram 映射到包含它的单词列表:

"$$h" -> ["hello", "helloworld", "hi", "huhu", "hey"]
"$he" -> ["hello", "helloworld", "hey"]
...
"llo" -> ["hello", "helloworld", "llowaddup", "allo"]
...

您数据库中的所有单词现在都由它们的 n-gram 索引。这就是为什么它被称为 inverted 索引。

这个想法是,给定一个查询,计算该查询与数据库中的所有单词共有多少个 n-gram。这可以快速计算。之后,您可以使用它来跳过计算大量记录的昂贵编辑距离。这显着提高了速度。这是所有搜索引擎(或多或少)使用的标准方法。

让我首先通过完全匹配的例子来解释一般方法。之后我们稍微修改一下,就可以进行模糊匹配了。


完全匹配

在查询时,计算查询的 n-gram,获取列表并计算交集。

如果你得到"hello",你计算克数并得到:

"$$h", "$he", "hel", "ell", "llo", "lo$", "o$$"

您获取所有这些 n-gram 的所有列表:

List result;
foreach (String nGram) in (query.getNGrams()) {
    List words = map.get(nGram);
    result = result.intersect(words);
}

交集包含与这些字母完全匹配的所有单词,仅"hello"

请注意,使用散列可以更快地计算完全匹配,例如 HashSet


模糊匹配

不要与列表相交,而是合并它们。为了有效地合并,您应该使用任何k-way merge algorithm,不过它要求倒排索引中的单词列表事先排序,因此请确保在构造时对其进行排序。

您现在得到一个包含至少一个与查询相同的 n-gram 的所有单词的列表。

我们已经大大减少了可能的记录集。但我们可以做得更好。为每个单词维护与查询相同的 n-gram 数量。您可以轻松地在合并列表时做到这一点。

考虑以下阈值:

max(|x|, |y|) - 1 - (delta - 1) * n

x 是您的查询,y 是您要比较的候选词。 n 是您使用的 n-grams 的值,例如 3 如果 3-gramdelta 是你允许多少错误的值。

如果计数低于该值,则直接知道编辑距离为

ED(x, y) > delta

所以你只需要考虑计数超过上述阈值的单词。仅针对您计算编辑距离的那些词ED(x, y)

因此,我们极大地减少了可能的候选集,并且仅在少量记录上计算昂贵的编辑距离。


示例

假设您收到查询"hilari"。让我们使用3-grams。我们得到

"$$h", "$hi", "hil", "ila", "lar", "ari", "ri$", "i$$"

我们搜索倒排索引,合并具有这些共同词的单词列表,得到"hillary""haemophilia""solar"。连同这些词,我们计算了它们共有多少克:

"hillary"      -> 4 ("$$h", "hi", "hil", "lar")
"haemophilia"  -> 2 ("$$h", "hil")
"solar"        -> 1 ("lar")

对照阈值检查每个条目。让delta 成为2。我们得到:

4 >= max(|"hilari"|, |"hillary"|) - 4     = 3
2 <  max(|"hilari"|, |"haemophilia"|) - 4 = 6
1 <  max(|"hilari"|, |"solar"|) - 4       = 2

只有"hillary" 高于阈值,丢弃其余的。计算所有剩余记录的编辑距离:

ED("hilari", "hillary") = 2

不超过delta = 2,所以我们接受。

【讨论】:

    【解决方案2】:

    这将很难。接受你会犯错误,不要让完美成为美好的敌人。

    首先删除敬语(先生,夫人,先生,博士,博士,小,老,)。删除常见的名字(基于名字列表)和首字母缩写,并将所有字符转换为大写。为剩下的任何内容创建一个签名 - 使用 Soundex 或类似的东西,或者简单地删除所有元音和双辅音。按签名排序以将相似的名称放在一起,然后仅对具有相同签名的名称运行完整比较。这将排序的时间复杂度降低到 O(n log n) 加上每组 的一点 O(k²) >k 个签名。

    【讨论】:

    • “创建签名”?是的——这就是整个问题。相比之下,其余的就简单了。
    【解决方案3】:

    其他答案已将此视为抽象字符串问题。如果这就是你所追求的,那么我认为他们会给出很好的建议。我将假设您想使用有关名称如何工作的特定知识,例如,“Mr. Thomas Riddle, Esq”和“Riddle, Tom”将匹配“Tom Riddle”,但“Tom Griddle” "不会。

    一般来说,对于这类问题,您需要定义某种规范化函数并寻找规范化为同一事物的术语。在这种情况下,您的名称的规范表示似乎应该包括名字和姓氏的小写版本,去掉任何标题,并使用昵称到正式名称映射“取消昵称”(假设您希望“Tom”和“Thomas”匹配)。这个函数会产生 "Tom Riddle" -> {first: "tom", last: "riddle"}, "Riddle, Tom" -> {first: "tom", last: "riddle"}, "Tom Riddle, Esq" -> {first: "tom", last: "riddle"}, 等等,但是 "Tom Griddle" -> @987654324 @。

    一旦有了名称规范化函数,您就可以创建一个映射(例如 hashmap 或 BST),将规范名称与非规范名称列表相关联。对于每个未规范化的名称,在映射中找到与其规范形式对应的列表并将其插入其中。完成后,所有包含多个元素的列表都是您的重复项。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2023-04-10
      • 1970-01-01
      • 2014-06-13
      • 2010-09-23
      • 1970-01-01
      • 2019-10-23
      • 1970-01-01
      相关资源
      最近更新 更多