找到与输入最相似的字符串的最快方法?

Posted

技术标签:

【中文标题】找到与输入最相似的字符串的最快方法?【英文标题】:Fastest way to find most similar string to an input? 【发布时间】:2009-03-13 16:26:22 【问题描述】:

给定一个长度为 N 的查询字符串 Q,和一个长度正好为 N 的 M 个序列的列表 L,找到 L 中与 Q 不匹配位置最少的字符串的最有效算法是什么?例如:

Q = "ABCDEFG";
L = ["ABCCEFG", "AAAAAAA", "TTAGGGT", "ZYXWVUT"];
answer = L.query(Q);  # Returns "ABCCEFG"
answer2 = L.query("AAAATAA");  #Returns "AAAAAAA".

显而易见的方法是扫描 L 中的每个序列,使搜索花费 O(M * N)。有没有办法在亚线性时间内做到这一点?我不在乎将 L 组织到某个数据结构中是否有很大的前期成本,因为它会被查询很多次。此外,任意处理并列分数也可以。

编辑:澄清一下,我正在寻找汉明距离。

【问题讨论】:

另见***.com/questions/5861718/… 【参考方案1】:

除了提到最好的第一个算法的答案之外,所有答案都大相径庭。 本地敏感哈希基本上是在做梦。这是我第一次在 *** 上看到如此多的答案。

首先,这是一个困难但标准的问题,很多年前就已经解决了 以不同的方式。

一种方法使用 trie,例如预设的 由塞奇威克在这里:

http://www.cs.princeton.edu/~rs/strings/

Sedgewick 也有示例 C 代码。

我引用了 Bentley 和 Sedgewick 题为“用于排序和搜索字符串的快速算法”的论文:

“‘‘近邻’’查询定位给定汉明距离内的所有单词 一个查询词(例如,代码距离苏打水2)。我们给出了一种在字符串中进行近邻搜索的新算法,给出了一个简单的 C 实现,并描述了它的效率实验。”

第二种方法是使用索引。将字符串拆分为字符 n-gram 和索引 使用倒排索引(谷歌搜索 Lucene 拼写检查器,看看它是如何完成的)。 使用索引拉出潜在候选人,然后对候选人进行汉明距离或编辑距离。这是保证效果最好的方法(并且相对简单)。

三分之一出现在语音识别领域。那里的查询是一个 wav 信号,而数据库是一组字符串。有一个“表”将信号片段与单词片段相匹配。目标是找到最匹配的词来表示。这个问题被称为单词对齐。

在发布的问题中,将查询部分与数据库部分匹配存在隐含成本。 例如,删除/插入/替换可能有不同的成本,甚至 不匹配的不同成本说“ph”和“f”。

语音识别中的标准解决方案使用动态编程方法,该方法通过直接修剪的启发式方法变得高效。这样,只保留最好的,比如说 50 个候选人。因此,名称最佳优先搜索。从理论上讲,您可能不会得到最佳匹配,但通常会得到一个很好的匹配。

这里是对后一种方法的参考:

http://amta2010.amtaweb.org/AMTA/papers/2-02-KoehnSenellart.pdf

使用后缀数组和 A* 解析的快速近似字符串匹配。

这种方法不仅适用于单词,也适用于句子

【讨论】:

【参考方案2】:

Locality sensitive hashing 似乎是已知的渐近最佳方法的基础,正如我从 review article in CACM 中理解的那样。说的文章很毛茸茸,我没有通读。另见nearest neighbor search。

将这些引用与您的问题联系起来:它们都处理度量空间中的一组点,例如 n 维向量空间。在您的问题中,n 是每个字符串的长度,每个坐标上的值是可以出现在字符串中每个位置的字符。

【讨论】:

【参考方案3】:

“最佳”方法会因您的输入集和查询集而有很大差异。拥有固定的消息长度可以让您在分类上下文中处理此问题。

信息论决策树算法(例如 C4.5)将提供最佳的整体性能保证。为了从这种方法中获得最佳性能,您必须首先根据互信息将字符串索引聚类为特征。请注意,您需要修改分类器以返回最后一个分支的所有叶节点,然后计算每个叶节点的部分编辑距离。编辑距离只需要计算树最后一次分裂所代表的特征集。

使用这种技术,查询应该是~O(k log n),k

在此的初始设置保证小于 O(m^2 + n*t^2), t

由于固定的 m 约束,这些非常好的性能数字是可能的。享受吧!

【讨论】:

【参考方案4】:

我认为您正在寻找Levenshtein edit distance。

有一个few questions here on SO about this already,我想你能找到一些好的答案。

【讨论】:

并非如此。他正在寻找最快的方法来从它们的列表中找到编辑距离最短的字符串。 @Chaos:最快的方法是查看列表中每个字符串的编辑距离(Levensthein 或其他算法,在这里无关紧要),然后取距离最短的第一个.不然怎么办? 当然你可以走捷径获得完美匹配,但仅此而已。 肯定有更快的方法。【参考方案5】:

您可以将每个序列视为一个 N 维坐标,将生成的空间分块为知道其中出现什么序列的块,然后在查找时首先搜索搜索序列的块和所有连续块,然后根据需要向外扩展。 (维护多个分块范围可能比搜索真正的大块组更可取。)

【讨论】:

【参考方案6】:

您是否正在寻找字符串之间的Hamming distance(即相同位置的不同字符数)?

或者字符“之间”的距离(例如英文字母的 ASCII 值之间的差异)对您也很重要?

【讨论】:

+1 好吧,再次阅读这个问题时,它更有可能是 Hamming 而不是 Levensthein。【参考方案7】:

目标序列上的一些best-first search 会比 O(M * N) 做得更好。其基本思想是您将候选序列中的第一个字符与目标序列的第一个字符进行比较,然后在您的第二次迭代中只与具有最少不匹配数量的序列进行下一个字符比较,等等。在您的第一个示例中,您将第二次与 ABCCEFG 和 AAAAAAA 进行比较,仅第三次和第四次比较 ABCCEFG,第五次比较所有序列,之后仅比较 ABCCEFG。当您到达候选序列的末尾时,具有最低错配计数的目标序列集就是您的匹配集。

(注意:在每个步骤中,您都在与搜索的 that 分支的下一个字符进行比较。渐进式比较都不会跳过字符。)

【讨论】:

如果您有 baaa 和 abbb 作为选项并寻找 aaaa, 将不起作用。它会在第一次迭代中抛出正确的答案。 不正确。像深度优先搜索这样的东西可以做到这一点; BFS 不会。它不会在第二次迭代中查看正确答案,但它会在第三次和第四次查看它,并正确识别它。 你的错误之处在于你认为它正在把东西扔掉。不是;它正在将它们移到优先级队列中。【参考方案8】:

我想不出一个小于 O(N * M) 的通用、精确的算法,但是如果你有足够小的 M 和 N,你可以使用 (N + M) 来制作一个算法位并行操作。

例如,如果 N 和 M 都小于 16,您可以使用 N * M 64 位整数的查找表 (16*log2(16) = 64),并在一次遍历字符串中执行所有操作,其中计数器中的每组 4 位对匹配的字符串之一计数 0-15。显然,您需要 M log2(N+1) 位来存储计数器,因此可能需要为每个字符更新多个值,但通常单遍查找可能比其他方法更快。所以它实际上是 O( N * M log(N) ),只是具有较低的常数因子 - 使用 64 位整数会在其中引入 1/64,所以如果 log2(N)

#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <inttypes.h>

size_t match ( const char* string, uint64_t table[][128] ) ;

int main ()

    const char* data[] =  "ABCCEFG", "AAAAAAA", "TTAGGGT", "ZYXWVUT" ;
    const size_t N = 7;
    const size_t M = 4;

    // prepare a table
    uint64_t table[7][128] =  0 ;

    for ( size_t i = 0; i < M; ++i )
        for ( size_t j = 0; j < N; ++j )
            table[j][ (size_t)data[i][j] ] |= 1 << (i * 4);

    const char* examples[] =  "ABCDEFG", "AAAATAA", "TTAGQQT", "ZAAGVUT" ;

    for ( size_t i = 0; i < 4; ++i ) 
        const char* q = examples[i];
        size_t result = match ( q, table );

        printf("Q(%s) -> %zd %s\n", q, result, data[result]);
    


size_t match ( const char* string, uint64_t table[][128] )

    uint64_t count = 0;

    // scan through string once, updating all counters at once
    for ( size_t i = 0; string[i]; ++i )
        count += table[i][ (size_t) string[i] ];

    // find greatest sub-count within count
    size_t best = 0;
    size_t best_sub_count = count & 0xf;

    for ( size_t i = 1; i < 4; ++i ) 
        size_t sub_count = ( count >>= 4 ) & 0xf;

        if ( sub_count > best_sub_count ) 
            best_sub_count = sub_count;
            best = i;
        
    

    return best;

【讨论】:

【参考方案9】:

抱歉打扰了这个老帖子

逐元素搜索意味着复杂度为 O(M*N*N) - O(M) 用于搜索,O(N*N) 用于计算 levenshtein 距离。

OP 正在寻找一种有效的方法来找到最小的汉明距离 (c),而不是字符串本身。如果你在 c 上有一个上限(比如 X),你可以在 O(log(X)*M*N) 中找到最小的 c。

正如 Stefan 所指出的,您可以快速找到给定汉明距离内的字符串。此页面http://blog.faroo.com/2015/03/24/fast-approximate-string-matching-with-large-edit-distances/ 讨论了使用 Tries 的一种方式。修改这个,只测试c从0到X是否有这样的字符串和二分查找。

【讨论】:

【参考方案10】:

如果前期成本无关紧要,您可以为每个可能的输入计算最佳匹配,并将结果放入哈希映射中。

当然,如果 N 不是非常小,这将不起作用。

【讨论】:

以上是关于找到与输入最相似的字符串的最快方法?的主要内容,如果未能解决你的问题,请参考以下文章

用最快的方法找到字符串中某一个字符串的个数

如何匹配具有多个相似字符串的字符串以找到最接近的匹配项[关闭]

Java中的相似性字符串比较

对于选择中的每个位置,将多行分组为一个字符串 postgres

最快的内容查找算法-----暴雪的Hash算法

在 Swift 中追加字符以形成字符串的最快、最精简的方法