“模糊匹配”字符串的算法

Posted

技术标签:

【中文标题】“模糊匹配”字符串的算法【英文标题】:Algorithms for "fuzzy matching" strings 【发布时间】:2011-02-22 21:45:29 【问题描述】:

通过模糊匹配,我并不是指 Levenshtein 距离或类似的字符串,而是它在 TextMate/Ido/Icicles 中的使用方式:给定字符串列表,找到包含搜索字符串中所有字符的字符串,但是可能与其他角色之间,更喜欢最合适的。

【问题讨论】:

您对问题的描述也很模糊。根据我对这些算法的了解,Levenshtein 距离也可能会起作用,只是太慢了。也许你能更好地描述它? 【参考方案1】:

Levenshtein 的“编辑距离”算法肯定会在您尝试做的事情上发挥作用:它们会衡量两个单词或地址或电话号码、诗篇、独白和学术文章的匹配程度,让您您对结果进行排名并选择最佳匹配。

一种更轻量级的方法是计算公共子字符串:它不如 Levenshtein,但它提供了可用的结果,并且可以在可以访问快速“InString”函数的慢速语言中快速运行。

几年前,我使用“FuzzyMatchScore”函数在 Excellerando 中发布了 Excel“模糊查找”,据我所知,这正是您所需要的:

http://excellerando.blogspot.com/2010/03/vlookup-with-fuzzy-matching-to-get.html

当然,它是在 Visual Basic for Applications 中。小心行事,十字架和大蒜:

公共函数 SumOfCommonStrings( _ ByVal s1 作为字符串,_ ByVal s2 作为字符串,_ 可选的比较为 VBA.VbCompareMethod = vbTextCompare, _ 可选 iScore 作为整数 = 0 _ ) 作为整数 Application.Volatile False ' N.Heffernan 2006 年 6 月 6 日 '此代码在公共领域 ' 测量字符串 1 中有多少是由字符串 2 中的子字符串组成的函数 ' 此函数使用修改后的最长公共字符串算法。 ' 简单的 LCS 算法对单字母过于敏感 ' 测试词中点附近的删除/更改,例如: ' 在编辑距离上,星期三显然更接近星期三 ' 比它是 WednesXXX 的基础。所以最好打分 ' 'Wed' 和 'esday' 相加总匹配 ' 注意不同长度的字符串: ' ' SumOfCommonStrings("Wednesday", "WednesXXXday") ' ' 这与以下得分相同: ' ' SumOfCommonStrings("星期三", "星期三") ' ' 所以请确保调用函数使用最长的长度 ' 从这个分数计算相似度时的字符串。 ' 这是为了清晰而不是为了性能而编码的。 Dim arr() As Integer ' 评分矩阵 Dim n As Integer ' s1 的长度 Dim m As Integer ' s2 的长度 Dim i As Integer ' 在 s1 中的起始位置 Dim j As Integer ' 在 s2 中的起始位置 Dim subs1 As String ' s1 的子串 Dim len1 As Integer ' subs1 的长度 Dim sBefore1 ' 记录在代码中 暗淡 sBefore2 昏暗 sAfter1 昏暗后2 将 s3 调暗为字符串 SumOfCommonStrings = iScore n = 长度 (s1) m = 长度 (s2) 如果 s1 = s2 那么 SumOfCommonStrings = n 退出函数 万一 如果 n = 0 或 m = 0 那么 退出函数 万一 's1 应该始终是两个字符串中较短的一个: 如果 n > m 然后 s3 = s2 s2 = s1 s1 = s3 n = 长度 (s1) m = 长度 (s2) 万一 n = 长度 (s1) m = 长度 (s2) ' 特殊情况:s1 是 s2 的 n 个精确子串 如果 InStr(1, s2, s1, 比较) 那么 SumOfCommonStrings = n 退出函数 万一 对于 len1 = n 到 1 步 -1 对于 i = 1 到 n - len1 + 1 subs1 = Mid(s1, i, len1) j = 0 j = InStr(1, s2, subs1, 比较) 如果 j > 0 然后 ' 我们找到了匹配的子字符串... iScore = iScore + len1 ' 现在从 s1 和 s2 中剪掉这个子串... ' 并搜索此切除前后的片段: 如果我 > 1 和 j > 1 然后 sBefore1 = 左(s1, i - 1) sBefore2 = 左(s2, j - 1) iScore = SumOfCommonStrings(sBefore1, _ s之前2,_ 比较, _ iScore) 万一 如果 i + len1 < n 和 j + len1 < m 然后 sAfter1 = right(s1, n + 1 - i - len1) sAfter2 = 对(s2, m + 1 - j - len1) iScore = SumOfCommonStrings(sAfter1, _ s后2,_ 比较, _ iScore) 万一 SumOfCommonStrings = iScore 退出函数 万一 下一个 下一个 结束功能 私有函数最小值(ByVal a As Integer, _ ByVal b 作为整数,_ ByVal c As Integer) As Integer 将最小值作为整数 最小值 = 一个 如果b<分钟 然后 最小值 = b 万一 如果 c <分钟 然后 最小值 = c 万一 最小值 = 最小值 结束功能

【讨论】:

【参考方案2】:

我最近不得不解决同样的问题。我的解决方案涉及对具有连续匹配字母的字符串进行高度评分,并按顺序排除不包含键入字母的字符串。

我在这里详细记录了算法:http://blog.kazade.co.uk/2014/10/a-fuzzy-filename-matching-algorithm.html

【讨论】:

【参考方案3】:

我实际上正在为 Emacs 构建类似于 Vim 的 Command-T 和 ctrlp 插件,只是为了好玩。我刚刚与一些聪明的同事就如何最有效地做到这一点进行了富有成效的讨论。目标是减少消除不匹配文件所需的操作次数。所以我们创建了一个嵌套映射,在顶层,每个键都是一个出现在搜索集中某处的字符,映射到搜索集中所有字符串的索引。然后,这些索引中的每一个都映射到该特定字符出现在搜索字符串中的字符偏移列表。

在伪代码中,对于字符串:

控制器 型号 查看

我们会像这样构建一个地图:


  "c" => 
           0 => [0]
         ,
  "o" => 
           0 => [1, 5],
           1 => [1]
         ,
  "n" => 
           0 => [2]
         ,
  "t" => 
           0 => [3]
         ,
  "r" => 
           0 => [4, 9]
         ,
  "l" => 
           0 => [6, 7],
           1 => [4]
         ,
  "e" => 
           0 => [9],
           1 => [3],
           2 => [2]
         ,
  "m" => 
           1 => [0]
         ,
  "d" => 
           1 => [2]
         ,
  "v" => 
           2 => [0]
         ,
  "i" => 
           2 => [1]
         ,
  "w" => 
           2 => [3]
         

所以现在你有一个这样的映射:


  character-1 => 
    word-index-1 => [occurrence-1, occurrence-2, occurrence-n, ...],
    word-index-n => [ ... ],
    ...
  ,
  character-n => 
    ...
  ,
  ...

现在搜索字符串“oe”:

    初始化一个新映射,其中键是匹配字符串的索引,而值是到目前为止通过该字符串读取的偏移量。 使用搜索字符串“o”中的第一个字符并在查找表中进行查找。 由于索引 0 和 1 处的字符串与“o”匹配,请将它们放入映射 0 => 1, 1 => 1。 现在搜索使用输入字符串中的下一个字符“e”并在表中查找。 这里有 3 个字符串匹配,但我们知道我们只关心字符串 0 和 1。 检查是否有任何偏移> 当前偏移。如果不是,则从我们的地图中删除项目,否则更新偏移量:0 => 9, 1 => 3

现在通过查看我们累积的映射中的键,我们知道哪些字符串与模糊搜索匹配。

理想情况下,如果搜索是在用户键入时执行的,您将跟踪累积的结果哈希并将其传递回您的搜索函数。我认为这将比迭代所有搜索字符串并在每个字符串上执行完整的通配符搜索要快得多。

有趣的是,假设您只关心插入,而不关心替换或删除,您还可以有效地将 Levenstein 距离与每个匹配项一起存储。虽然加入这个逻辑也许并不难。

【讨论】:

【参考方案4】:

我终于明白你在找什么了。这个问题很有趣,但是看看你发现的 2 种算法,人们似乎对规范有很大不同的看法;)

我认为更清楚地说明问题和要求会很有用。

问题

我们正在寻找一种加快输入速度的方法,它允许用户只输入他们实际想要的关键字的几个字母,并建议他们从中选择一个列表。

    预计输入的所有字母都在关键字中 期望输入中的字母在关键字中的顺序一致 返回的关键字列表应以一致(可复制)的顺序呈现 算法应该不区分大小写

分析

前两个要求可以总结如下:对于输入axg,我们正在寻找匹配这个正则表达式[^a]*a[^x]*x[^g]*g.*的单词

第三个要求是故意放宽的。单词出现在列表中的顺序需要保持一致......但是很难猜测评分方法是否会比字母顺序更好。如果列表非常长,那么评分方法可能会更好,但是对于短列表,眼睛更容易在以明显方式排序的列表中查找特定项目。

此外,字母顺序在打字过程中具有一致性的优点:即添加一个字母并不会完全重新排序列表(对眼睛和大脑来说很痛苦),它只是过滤掉不再匹配的项目。

处理 unicode 字符没有精确性,例如 à 是否类似于 a 或其他字符?由于我知道目前没有任何语言在其关键字中使用此类字符,所以我暂时不介绍了。

我的解决方案:

对于任何输入,我都会构建前面表达的正则表达式。它适用于 Python,因为该语言已经具有不区分大小写的匹配功能。

然后,我将匹配我的(按字母顺序排序的)关键字列表,并输出它经过过滤。

在伪代码中:

WORDS = ['Bar', 'Foo', 'FooBar', 'Other']

def GetList(input, words = WORDS):
  expr = ['[^' + i + ']*' + i for i in input]
  return [w for w in words if re.match(expr, w, re.IGNORECASE)]

我本可以使用单行,但认为它会掩盖代码;)

此解决方案非常适合增量情况(即,当您匹配用户类型并因此不断重建时),因为当用户添加字符时,您可以简单地重新过滤刚刚计算的结果。因此:

要么字符少,所以匹配很快,列表的长度无关紧要 要么有很多字符,这意味着我们正在过滤一个短列表,因此如果匹配需要更长的元素时间也没关系

我还应该注意,这个正则表达式不涉及回溯,因此非常有效。它也可以建模为一个简单的状态机。

【讨论】:

另一个要求是算法支持或要求匹配连续字母或具有特定属性的字母的单词,例如大写(用于区分驼峰大小写的查找)或分隔符之后(为 Environment.Data.Lookup 键入 edl,或 wsd 用于 windows/system32/drivers)。这只是一个想法,我不知道这在实践中是否合适。 @Emile:这将大大增加搜索空间,尤其是文件系统上的匹配会严重影响速度。我认为增量方法(首先匹配Environment,然后匹配Data,然后Lookup,文件系统同上)可能就足够了。这意味着您有上下文相关的自动完成功能,这涉及更多并且需要完整的语法分析。【参考方案5】:

目前我发现的两种算法:

    LiquidMetal Better Ido Flex-Matching

【讨论】:

【参考方案6】:

如果您的文本主要是英语,那么您可以尝试各种 Soundex 算法 1.Classic soundex 2.Metafone

这些算法可以让您选择发音相似的单词,并且是查找拼写错误单词的好方法。

【讨论】:

这被“我不是指 Levenshtein 距离或类似的类似字符串”所涵盖。

以上是关于“模糊匹配”字符串的算法的主要内容,如果未能解决你的问题,请参考以下文章

“模糊匹配”字符串的算法

python模糊匹配库能否定制匹配关系

C#字符串模糊匹配的问题

java模糊匹配 字符串匹配某个字符串

java 用字符串模糊匹配另一个字符串

java中是如何实现基于文字标题的模糊匹配的,下面的代码是实现这个功能的吗?