寻找与输入最相似的字符串的最快方法是什么?

15

给定长度为 N 的查询字符串 Q 和长度恰好为 N 的 M 个序列的列表 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组织成某些数据结构的大量前期成本,因为它将被查询很多次。同时,任意处理并列分数都可以。

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


请参见https://dev59.com/s2025IYBdhLWcg3wr4B6。 - Raedwald
10个回答

11
除了提到最佳第一算法的答案之外,其他答案都偏离了主题。局部敏感哈希基本上是一种幻想。这是我在stackoverflow上看到的答案与问题相差甚远的第一次。
首先,这是一个难题,但已经在多年前通过不同的方式得到了解决。
一种方法使用trie树,例如Sedgewick在此处提出的那样:

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

Sedgewick还有样例C代码。
我引用Bentley和Sedgewick在论文"Fast Algorithms for Sorting and Searching Strings"中的话:
“‘‘近邻’’查询定位给定汉明距离内的所有单词(例如,code距离soda为2)。我们提供了一种新的字符串近邻搜索算法,提供了一个简单的C实现,并描述了其效率的实验。”
第二种方法是使用索引。将字符串拆分为字符n-gram并使用反向索引进行索引(谷歌Lucene拼写检查器以了解如何执行此操作)。使用索引拉取潜在的候选项,然后对候选项运行汉明距离或编辑距离。这是保证最好(相对简单)的方法。
第三种方法出现在语音识别领域。查询是wav信号,数据库是一组字符串。有一个"表"可以将信号的部分与单词的部分匹配。目标是找到与信号最匹配的单词。这个问题称为单词对齐。
在所发布的问题中,匹配查询部分和数据库部分存在隐含成本。例如,可能具有不同的删除/插入/替换成本,甚至可能具有不同的不匹配成本,比如"ph"和"f"不匹配的成本。
语音识别中的标准解决方案使用动态规划方法,通过指导修剪来使其高效。这样,只保留最佳的50个候选项。因此,称为最佳优先搜索。理论上,您可能无法获得最佳匹配,但通常会获得良好的匹配。
这是关于后一种方法的参考资料。

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

快速近似字符串匹配算法:基于后缀数组和A*解析。该方法不仅适用于单词,还适用于句子。

4

本地敏感哈希是目前已知的渐进最佳方法,据我理解来自CACM的评论文章。该文章相当复杂,我没全读懂。还可以参考最近邻搜索

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


2
"最佳"方法将根据您的输入集和查询集而显著变化。拥有固定的消息长度将使您能够在分类上下文中处理此问题。信息论决策树算法(例如C4.5)将提供最佳的性能保证。为了从该方法中获得最佳性能,您必须首先基于相互信息将字符串索引聚类成特征。请注意,您需要修改分类器以返回最后一个分支处的所有叶节点,然后计算每个叶节点的部分编辑距离。编辑距离仅需要针对树的最后一个分裂所表示的特征集进行计算。使用这种技术,查询应该约为O(k log n),其中k << m是特征大小的期望值,m是字符串长度,n是比较序列的数量。此初始设置保证小于O(m ^ 2 + n * t ^ 2),其中t << m,t * k ~ m是项目的特征计数。这非常合理,不需要任何严重的硬件。由于固定的m约束,这些非常好的性能数字是可能的。享受吧!"

1

4
不是很。他正在寻找在一系列字符串中查找编辑距离最短的字符串的最快方法。 - chaos
@Chaos:最快的方法是查看列表中每个字符串的编辑距离(Levensthein或其他算法,这里并不重要),然后选择第一个距离最短的字符串。还有其他方法吗? - Tomalak
当然,你可以在完全匹配的情况下使用快捷方式,但仅限于此。 - Tomalak
3
有更快的方法。 - chaos

1
你可以将每个序列视为一个N维坐标,将得到的空间分成块,这些块知道其中出现的序列,然后在查找时首先搜索搜索序列所在的块和所有相邻的块,必要时向外扩展。(保持几个范围的分块可能比进入搜索真正大的块更可取。)

1
在目标序列上执行某种最佳优先搜索算法,比O(M * N)要好得多。基本思想是将候选序列的第一个字符与目标序列的第一个字符进行比较,然后在第二次迭代中,只对具有最少不匹配数的序列进行下一个字符比较,以此类推。在第一个示例中,您会发现第二次比较是针对ABCCEFG和AAAAAAA,第三和第四次比较只涉及ABCCEFG,第五次比较时涉及所有序列,之后只涉及ABCCEFG。当您到达候选序列的末尾时,具有最低不匹配计数的目标序列集合就是您的匹配集。

(注意:在每个步骤中,您都在与搜索分支的下一个字符进行比较。没有任何逐步比较跳过字符。)


如果你的选项中有“baaa”和“abbb”,而你又在寻找“aaaa”,那么它将无法工作。它会在第一次迭代中排除正确答案。 - Jens Schauder
错误的。类似深度优先搜索的算法可以做到这一点;广度优先搜索则不行。它不会在第二次迭代中查看正确答案,但它会在第三和第四次中查看并正确识别它。 - chaos
你的错误在于认为它是将东西扔掉。实际上,它只是将它们移动到了优先级队列的下方。 - chaos

1
你是否正在寻找字符串之间的汉明距离(即等效位置上不同字符的数量)?
或者,字符之间的距离(例如英文字母的ASCII值之间的差异)对你也很重要吗?

嗯,再次阅读问题后,更可能是 Hamming 距离而不是 Levenshtein 距离。 - Tomalak

0

我无法想出一个通用的、精确的算法,其时间复杂度小于O(N * M)。但是,如果你的M和N足够小,你可以使用比特并行操作来实现一个性能为(N + M)的算法。

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

#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;
}

0

抱歉打扰了这个旧帖子

逐个搜索元素的意思是复杂度为O(M*N*N) - O(M)用于搜索和O(N*N)用于计算Levenshtein距离。

原帖作者正在寻找一种有效的方法来查找最小汉明距离(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的方法。将其修改为仅测试是否存在这样的字符串,并在0到X之间进行二分查找。


-1

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

当然,如果N不是非常小,则无法使用此方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接