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

时间:2009-03-13 16:26:23

标签: algorithm string language-agnostic

给定长度为N的查询字符串Q,以及长度恰好为N的M个序列的列表L,找到L中具有最少错配位置的字符串的最有效算法是什么?例如:

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

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

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

10 个答案:

答案 0 :(得分:6)

除了提到最好的第一个算法的答案之外的所有答案都非常不合适。 本地敏感的哈希基本上是在做梦。这是我第一次在stackoverflow上看到很多答案。

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

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

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

Sedgewick也有样本C代码。

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

“'邻近''查询查找给定汉明距离内的所有单词 一个查询词(例如,代码是苏打的距离2)。我们为字符串中的近邻搜索提供了一种新算法,提出了一个简单的C实现,并描述了它的效率实验。“

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

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

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

语音识别中的标准解决方案使用动态编程方法,通过直接修剪的启发式方法使其有效。通过这种方式,只保留最好的50个候选人。因此,名称最好先搜索。从理论上讲,你可能没有得到最好的比赛,但通常你会得到一个很好的比赛。

以下是对后一种方法的参考:

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

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

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

答案 1 :(得分:4)

Locality sensitive hashing是所谓的渐近最佳方法的基础,正如我从review article in CACM中理解的那样。这篇文章非常多毛,我没有读完。另请参阅nearest neighbor search

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

答案 2 :(得分:2)

“最佳”方法会根据您的输入集和查询集而有很大差异。具有固定的消息长度将允许您在分类上下文中处理此问题。

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

使用这种技术,查询应为~O(k log n),k <&lt;&lt; m,其中k是特征大小的期望,m是字符串的长度,n是比较序列的数量。

保证初始设置小于O(m ^ 2 + n * t ^ 2),t <1。 m,t * k~m,其中t是项目的特征计数。这是非常合理的,不需要任何严肃的硬件。

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

答案 3 :(得分:1)

我认为您正在寻找Levenshtein edit distance

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

答案 4 :(得分:1)

您可以将每个序列视为N维坐标,将结果空间分块为知道序列中出现的序列的块,然后在查找中首先搜索搜索序列的块和所有连续块,然后根据需要向外展开。 (维护几个分块范围可能比搜索真正大块的块更合适。)

答案 5 :(得分:1)

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

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

答案 6 :(得分:1)

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

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

答案 7 :(得分: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)<1则应该更好。 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;
}

答案 8 :(得分:0)

很抱歉碰到这个旧帖子

搜索elementwise意味着搜索的O(M * N * N) - O(M)的复杂度和计算levenshtein距离的O(N * N)。

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的一种方式。将其修改为仅测试是否存在从0到X的c上的字符串和二进制搜索。

答案 9 :(得分:-1)

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

当然,如果N不是非常小,这将无效。