子串算法建议

Posted

技术标签:

【中文标题】子串算法建议【英文标题】:Substring algorithm suggestion 【发布时间】:2011-04-13 07:20:17 【问题描述】:

我有一大组 (100k) 短字符串(不超过 100 个字符),我需要快速找到所有具有特定子字符串的人。

这将用作用户开始输入的搜索框,系统会立即给出“建议”(以用户输入的文本作为子字符串的字符串)。类似于 *** 中的“标签”框。

因为这将是交互式的,所以应该很快。您为此推荐什么算法或数据结构?

顺便说一句,我将使用 Delphi 2007。

提前致谢。

【问题讨论】:

感谢所有回复。我查看了 Mike 建议的后缀树。但是,考虑到我的时间限制和缺乏现有的实现,我将首先使用更简单的东西:Oren 建议的 Boyer-moore。 我刚刚尝试了 Boyer-Moore-Horspool(感谢 Oren 和 François),它比我预期的要快得多。对我的目的来说绰绰有余。 【参考方案1】:

我写了一个很长的简介,做了一堆复杂性计算和 xzibit 笑话(一棵树,这样你在查找时可以查找),但后来意识到这比我想象的要容易。浏览器一直都在这样做,而且它们不会在您每次加载页面时预先计算大表。

http://en.wikipedia.org/wiki/Boyer%E2%80%93Moore_string_search_algorithm

这意味着您将 100k 字符串组合成一个长字符串。然后,您获取查询子字符串,并遍历您的大字符串,寻找您的匹配项。但您不是按字符跳跃(这意味着您正在查看 100k*100 次迭代)。你的子字符串的长度在跳跃,所以你的子字符串越长,速度越快。

这是一个很好的例子:http://userweb.cs.utexas.edu/users/moore/best-ideas/string-searching/fstrpos-example.html

他们正在搜索字符串EXAMPLE。

这就是浏览器和文本编辑器所做的事情,而且它们不会在您每次加载页面时真正构建巨大的前缀表。

【讨论】:

+1。我一直想知道实现和理解后缀树的痛苦是否值得,它们似乎只是作为所有字符串问题的灵丹妙药而被提及。 @bronzebeard 完全同意你的看法,+1 给 Oren 提供了一些现实的解决方案。 后缀尝试非常强大,但并不那么容易理解和构建。当您拥有大量数据时,它们会发光。对于简单的建议,他们可能是矫枉过正。为奥伦 +1。 Boyer-Moore 是要走的路。我编写了一个代码来查看音频库中的数千个短字符串(基本上是所有 ID3 标签加上一些额外的数据)。速度如此之快,您可以输入并显示实时更新。 你有任何证据证明你的断言没有在浏览器中进行预处理吗?这倒是让我大吃一惊。另一方面,它可以解释为什么“真棒酒吧”如此糟糕……。只是为了说清楚:虽然这种方式是可以的,但对于 OP 正在处理的数据量来说,使用 trie 更有效。此外,实现 trie 并不困难。【参考方案2】:

您可能想要使用的数据结构是 Trie,特别是后缀 trie。阅读 this article 以获得关于它们如何工作以及如何为您的问题编写一个很好的解释。

【讨论】:

打败我。如果文章没有说清楚,你可以为整个语料构建一棵后缀树,并对其进行注释以说明该后缀属于哪个字符串。 一个很好的建议,但对于他想要的东西来说可能有点矫枉过正。 +1 表示一个没有多少人知道的数据结构。 @Runner 人不多? “Trie”是新的 JQuery :) 今天很难找到没有“用户尝试”答案的算法问题。 @Rybak - 它一定是从我身边过去了。有趣的。我以为它不为人所知。我在一年半前实施了一个,那时我肯定还没有找到太多关于它们的信息。必须再看一遍:)【参考方案3】:

虽然您当然可以使用更好的数据结构来加快速度,但此时蛮力可能完全足够了。做一个快速测试:

[编辑:更改代码以搜索子字符串,再次编辑以缩短它搜索的子字符串与搜索的子字符串相比。]

#include <algorithm>
#include <iostream>
#include <vector>
#include <string>
#include <time.h>

std::string rand_string(int min=20, int max=100)  
    size_t length = rand()% (max-min) + min;
    std::string ret;

    for (size_t i=0; i<length; i++)
        ret.push_back(rand() % ('z' - 'a') + 'a');
    return ret; 


class substr 
    std::string seek;
public:
    substr(std::string x) : seek(x) 

    bool operator()(std::string const &y)  return y.find(seek) != std::string::npos; 
;

int main()  
    std::vector<std::string> values;

    for (int i=0; i<100000; i++)
        values.push_back(rand_string());

    std::string seek = rand_string(5, 10);

    const int reps = 10;

    clock_t start = clock();
    std::vector<std::string>::iterator pos;
    for (int i=0; i<reps; i++)
         pos = std::find_if(values.begin(), values.end(), substr(seek));
    clock_t stop = clock();

    std::cout << "Search took: " << double(stop-start)/CLOCKS_PER_SEC/reps << " seconds\n";
    if (pos == values.end())
        std::cout << "Value wasn't found\n";
    else
        std::cout << "Value was found\n";
    return 0;

在我的机器上(大约 4 岁——几乎没有按照目前的标准速度恶魔)每次搜索运行大约 3 10 毫秒。这对于交互式用户来说已经足够快了——如果字符串数量是原来的 10 倍,它仍然可以。

【讨论】:

我不精通 STL,但 IIRC std::find 会在容器中搜索 exact 元素的出现。 Fernando 对子字符串很感兴趣。 @Nikita:非常正确。它并没有太大的区别,但我已经编辑了代码来测试正确的东西。即便如此,我们仍在谈论搜索的个位数毫秒。尽管我用 C++ 编写了测试代码,但我希望使用 Delphi 的结果大致相同。速度可能会相差几个百分点,但我们必须看到 10 倍的差异才能接近显着,看到这一点我会非常感到惊讶。 +1,在复杂化之前应该先尝试暴力破解。 即使字符串的数量和大小相当大,我也只能认为多线程的情况下,这可能会更快。 @Jerry 您在这里的时间估计不准确,因为“seek”字符串平均与“values”字符串一样长。尝试搜索“abcde”之类的内容(更真实的用户输入示例)。虽然,结果仍然相当不错(对我来说随机输入 20 毫秒)。不是 0.1 毫秒,但通常也足够好。【参考方案4】:

我讨厌不同意 Mike 和他的支持者的观点,但是后缀树(他的链接中描述的数据结构)很难实现。在 Pascal/Delphi 中找到可靠的实现可能也很困难。

Suffix Arrays 提供相同的功能,但更简单。权衡是O(m * logn) 复杂度,其中m 是搜索标记的长度,n 是数据集的大小(在本例中为 100kb)。

如果有人不知道,后缀树和后缀数组都允许您在长文本 t 中找到所有出现的子字符串 s

Fernando 的问题可以简化为这个问题,方法是使用一些分隔符将一组初始字符串连接成一个字符串。例如,初始设置为["text1", "text2", "some text"],则结果字符串t 将为"text1|text2|some text"。 现在,我们不需要在每个单词中单独搜索字符串"text",我们只需要在大字符串t中找到它的所有出现。

我还推荐 Oren 的答案,他提出了另一种现实的方法。

【讨论】:

trie 和后缀数组有很大的不同。虽然后缀数组可以建立在字符串集合之上,但是尝试做同样的事情更加自然和高效——搜索是 O(m) 而不是 O(log n + m),并且对于长度为 100 的 100k 字符串,这可以有所作为。最后,有效地构建一个 trie 比有效地构建一个后缀数组要容易得多——尽管即使是蛮力快速排序方法在实践中也能产生可接受的性能。但除此之外,为后缀数组点赞!【参考方案5】:

This Delphi implementation of Boyer-Moore-Horspool 可能会满足您的需求。 免责声明:我没有尝试此代码...

【讨论】:

【参考方案6】:

您可能正在寻找的是n-gram。它用于查找与您的子字符串相关的最可能的单词。非常有趣的东西,虽然对于您正在寻找的东西来说可能有点过头了,但还是很高兴知道。

【讨论】:

以上是关于子串算法建议的主要内容,如果未能解决你的问题,请参考以下文章

算法快速排序

算法归并排序

C ++的“map”容器是否对字符串的连续子串应用Rabin-Karp算法?

算法哈希表 ( 两数之和 )

算法快速排序与归并排序对比

算法双指针算法 ( 有效回文串 II )