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

时间:2010-05-23 11:29:37

标签: algorithm search string fuzzy-search

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

6 个答案:

答案 0 :(得分:30)

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

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

<强>问题

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

  1. 预计输入的所有字母都在关键字
  2. 预计输入中的字母在关键字
  3. 中的顺序相同
  4. 返回的关键字列表应以一致(可再现的)顺序显示
  5. 算法应不区分大小写
  6. <强>分析

    前两个要求可以这样总结:对于输入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)]
    

    我本可以使用单行,但认为它会使代码模糊不清;)

    此解决方案适用于增量情况(即,当您匹配用户类型并因此保持重建时),因为当用户添加角色时,您可以简单地重新过滤刚刚计算的结果。因此:

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

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

答案 1 :(得分:9)

Levenshtein的“编辑距离”算法肯定适用于您要做的事情:它们会测量两个单词或地址或电话号码,诗篇,独白和学术文章之间的匹配程度,您对结果进行排名并选择最佳匹配。

更轻量级的appproach是计算公共子串:它不如Levenshtein好,但它提供了可用的结果,并且可以快速运行,可以访问快速的“InString”函数。

几年前我在Excellerando发布了一个Excel'模糊查找',使用'FuzzyMatchScore'功能,据我所知,确切地说你需要什么:

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

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

Public Function SumOfCommonStrings( _
                            ByVal s1 As String, _
                            ByVal s2 As String, _
                            Optional Compare As VBA.VbCompareMethod = vbTextCompare, _
                            Optional iScore As Integer = 0 _
                                ) As Integer

Application.Volatile False

' N.Heffernan 06 June 2006 
' THIS CODE IS IN THE PUBLIC DOMAIN


' Function to measure how much of String 1 is made up of substrings found in String 2

' This function uses a modified Longest Common String algorithm.
' Simple LCS algorithms are unduly sensitive to single-letter
' deletions/changes near the midpoint of the test words, eg:
' Wednesday is obviously closer to WedXesday on an edit-distance
' basis than it is to WednesXXX. So it would be better to score
' the 'Wed' as well as the 'esday' and add up the total matched

' Watch out for strings of differing lengths:
'
'    SumOfCommonStrings("Wednesday", "WednesXXXday")
'
' This scores the same as:
'
'     SumOfCommonStrings("Wednesday", "Wednesday")
'
' So make sure the calling function uses the length of the longest
' string when calculating the degree of similarity from this score.


' This is coded for clarity, not for performance.

Dim arr() As Integer    ' Scoring matrix
Dim n As Integer        ' length of s1
Dim m As Integer        ' length of s2
Dim i As Integer        ' start position in s1
Dim j As Integer        ' start position in s2
Dim subs1 As String     ' a substring of s1
Dim len1 As Integer     ' length of subs1

Dim sBefore1            ' documented in the code
Dim sBefore2
Dim sAfter1
Dim sAfter2

Dim s3 As String


SumOfCommonStrings = iScore

n = Len(s1)
m = Len(s2)

If s1 = s2 Then
    SumOfCommonStrings = n
    Exit Function
End If

If n = 0 Or m = 0 Then
    Exit Function
End If

's1 should always be the shorter of the two strings:
If n > m Then
    s3 = s2
    s2 = s1
    s1 = s3
    n = Len(s1)
    m = Len(s2)
End If

n = Len(s1)
m = Len(s2)

' Special case: s1 is n exact substring of s2
If InStr(1, s2, s1, Compare) Then
    SumOfCommonStrings = n
    Exit Function
End If

For len1 = n To 1 Step -1

    For i = 1 To n - len1 + 1

        subs1 = Mid(s1, i, len1)
        j = 0
        j = InStr(1, s2, subs1, Compare)

        If j > 0 Then

            ' We've found a matching substring...
            iScore = iScore + len1            

          ' Now clip out this substring from s1 and s2...
          ' And search the fragments before and after this excision:


            If i > 1 And j > 1 Then
                sBefore1 = left(s1, i - 1)
                sBefore2 = left(s2, j - 1)
                iScore = SumOfCommonStrings(sBefore1, _
                                            sBefore2, _
                                            Compare, _
                                            iScore)
            End If


            If i + len1 < n And j + len1 < m Then
                sAfter1 = right(s1, n + 1 - i - len1)
                sAfter2 = right(s2, m + 1 - j - len1)
                iScore = SumOfCommonStrings(sAfter1, _
                                            sAfter2, _
                                            Compare, _
                                            iScore)
            End If


            SumOfCommonStrings = iScore
            Exit Function

        End If

    Next


Next


End Function


Private Function Minimum(ByVal a As Integer, _
                         ByVal b As Integer, _
                         ByVal c As Integer) As Integer
Dim min As Integer

  min = a

  If b < min Then
        min = b
  End If

  If c < min Then
        min = c
  End If

  Minimum = min

End Function

答案 2 :(得分:3)

到目前为止我发现了两种算法:

  1. LiquidMetal
  2. Better Ido Flex-Matching

答案 3 :(得分: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”:

  1. 初始化一个新映射,其中键将是匹配的字符串的索引,以及到目前为止通过该字符串读取的偏移值。
  2. 从搜索字符串“o”中获取第一个字符,并在查找表中查找。
  3. 由于索引0和1处的字符串与“o”匹配,因此请将它们放入地图{0 => 1, 1 => 1}
  4. 现在搜索消耗输入字符串中的下一个字符“e”并将其放入表中。
  5. 这里有3个字符串匹配,但我们知道我们只关心字符串0和1。
  6. 检查是否有任何偏移&gt;目前的补偿。如果没有,请从地图中删除项目,否则更新偏移量:{0 => 9, 1 => 3}
  7. 现在通过查看我们累积的地图中的键,我们知道哪些字符串与模糊搜索匹配。

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

    有趣的是,你可以有效地存储Levenstein距离以及每场比赛,假设你只关心插入,而不是替换或删除。虽然也许并不难加入这种逻辑。

答案 4 :(得分:2)

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

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

答案 5 :(得分:0)

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

这些算法可让您选择听起来彼此相似的单词,并且是查找拼写错误的单词的好方法。