什么是最快的子字符串搜索算法?

204

好的,为了不让自己听起来像个白痴,我会更明确地陈述问题/要求:

  • 需要在C风格的空终止字符串中查找针(模式)和干草堆(文本)。未提供长度信息;如果需要,必须计算。
  • 函数应返回第一个匹配项的指针,如果未找到匹配项,则返回NULL
  • 不允许失败情况。这意味着任何具有非常量(或大常量)存储要求的算法都需要具有分配失败的回退情况(并且回退情况的性能有助于最坏情况的性能)。
  • 实现应使用C语言,但仅有算法的良好描述(或链接)而无代码也可以。

...以及我所说的“最快”是什么意思:

  • 确定性的 O(n),其中 n 是指 haystack 的长度。(但是如果将其与更健壮的算法结合起来,可以使用通常为 O(nm) 的算法(例如滚动哈希)中的思想,以获得确定性的 O(n) 结果。)
  • 从未表现出比朴素的暴力算法更差的性能(可测量的;if (!needle[1]) 等需要一些计时器是可以接受的),特别是在非常短的 needle 上,这可能是最常见的情况。(无条件的重度预处理开销不好,试图通过牺牲常规 needle 的代价来改善病态 needle 的线性系数也不好。)
  • 对于任意给定的 needle 和 haystack,在搜索时间上与任何其他广泛实现的算法相比具有可比或更好的性能(搜索时间不超过其他算法的50%)。
  • 除了这些条件外,我将“最快”的定义留给开放式的。一个好的答案应该解释为什么你认为你提出的方法是“最快”的。

我的当前实现大约比 glibc 的 Two-Way 实现慢10%到8倍(取决于输入)。

更新:我的当前最佳算法如下:

  • 对于长度为1的针,使用strchr
  • 对于长度为2-4的针,使用机器字来一次比较2-4个字节,方法如下:用位移将针预加载到16或32位整数中,并在每次迭代中从干草堆中循环旧字节/新字节。每个干草堆的字节都被读取一次,并产生与0(字符串结尾)和一个16或32位比较的检查。
  • 对于长度大于4的针,使用具有坏移位表的双向算法(例如Boyer-Moore),该算法仅应用于窗口的最后一个字节。为避免初始化1kb表的开销,这将是许多中等长度针的净损失,我保留了一个位数组(32字节),标记哪些条目在移位表中已初始化。未设置的位对应于永远不出现在针中的字节值,可以进行完整的针长移位。

我心中的重要问题是:

  • 有没有办法更好地利用坏的移位表?Boyer-Moore通过向后扫描(从右到左)最好地利用它,但Two-Way需要从左到右扫描。
  • 我找到的唯一两个适用于一般情况(没有内存不足或二次性能条件)的可行算法是Two-WayString Matching on Ordered Alphabets。但是否存在易于检测的情况,其中不同的算法将是最优的?当然,对于m<100左右,许多O(m)(其中m是针长度)的空间算法都可以使用。如果有一个仅需要线性时间的简单测试针的算法,则还可以使用最坏情况二次的算法。

额外加分项:

  • 如果假定针和草堆都是格式良好的UTF-8,您是否可以通过这种方式提高性能?(由于具有不同字节长度的字符,格式良好性在针和草堆之间强加了一些字符串对齐要求,并在遇到不匹配的头字节时允许自动的2-4字节移位。但是,除了各种算法已经给出的最大后缀计算、好后缀移位等之外,这些约束是否会给您带来更多/任何好处?)

注意:我对大多数算法都很熟悉,只是不知道它们在实践中表现如何。这里有一个很好的参考资料,以便别人不再给我关于算法的评论/答案: http://www-igm.univ-mlv.fr/~lecroq/string/index.html


字符串算法页面上列出了相当多的字符串搜索算法。您可能希望描述一下您从这个列表中考虑过哪些算法。 - Greg Hewgill
71
结尾处的链接非常有价值! - Carlos
5
我无法相信你仍然没有接受一个答案。 - user541686
2
@Mehrdad:我本来想说没有答案真正回答了问题,但你的似乎可以。在你回答的时候,我已经转而离开了进一步改进strstr的事情,所以我实际上还没有好好阅读你链接的论文,但它听起来非常有前途。谢谢你,很抱歉没有及时回复你。 - R.. GitHub STOP HELPING ICE
17个回答

3
您可以实现4种不同的算法。每M分钟(需经验确定),对当前真实数据运行这4种算法。在N次运行中累积统计信息(也需TBD)。然后仅使用获胜者进行下一个M分钟。记录Wins的统计信息,以便替换从未获胜的算法。将优化努力集中在最成功的例程上。特别关注硬件、数据库或数据源发生任何更改后的统计信息。如果可能,请将该信息包含在统计日志中,以免根据日志日期/时间戳进行计算。

3

我最近发现了一个很好的工具,用于衡量各种可用算法的性能:http://www.dmi.unict.it/~faro/smart/index.php

你可能会觉得它很有用。 此外,如果我需要快速选择子字符串搜索算法,我会选择Knuth-Morris-Pratt。


谢谢提供链接。这些测试看起来很有趣,适合典型情况下的时间测试,但不适合捕捉最坏情况下的时间。 - R.. GitHub STOP HELPING ICE

3

只需搜索“最快的strstr”,如果您看到感兴趣的内容,请随时向我询问。

在我看来,您对自己施加了太多限制(是的,我们都希望达到次线性甚至最大线性搜索),但这需要一个真正的程序员介入,直到那时,我认为哈希方法只是一个聪明的临时解决方案(对于较短的2..16个模式,BNDM可加强)。

以下是一个快速的示例:

将32字节的Pattern搜索进206908949字节的String中作为一行... 跳过性能(越大越好):3041%,6801754次跳过/迭代 Railgun_Quadruplet_7Hasherezade_hits / Railgun_Quadruplet_7Hasherezade_clocks:0/58 Railgun_Quadruplet_7Hasherezade 性能:3483KB/clock

将32字节的Pattern搜索进206908949字节的String中作为一行... 跳过性能(越大越好):1554%,13307181次跳过/迭代 Boyer_Moore_Flensburg_hits / Boyer_Moore_Flensburg_clocks:0/83 Boyer_Moore_Flensburg 性能:2434KB/clock

将32字节的Pattern搜索进206908949字节的String中作为一行... 跳过性能(越大越好):129%,160239051次跳过/迭代 Two-Way_hits / Two-Way_clocks:0/816 Two-Way 性能:247KB/clock

Sanmayce, 问候


3
当前最快的是EPSM,由S. Faro和O.M.Kulekci开发。 请参见https://smart-tool.github.io/smart/。 这是一种针对SIMD SSE4.2优化的“精确压缩字符串匹配”算法(x86_64和aarch64)。它在所有大小上表现稳定且最佳。 我提供的链接网站比较了199个快速字符串搜索算法,其中通常的算法(BM、KMP、BMH)非常慢。EPSM在这些平台上胜过其他所有被提及的算法。它也是最新的。 更新2020: EPSM最近为AVX进行了优化,仍然是最快的。

2
你可能需要使用多种类型的字符串进行不同的基准测试,因为这对性能有很大影响。算法将根据搜索自然语言(即使在这里仍可能因不同的形态学而有细微差别),DNA字符串或随机字符串等表现不同。
字母表大小在许多算法中都会发挥作用,指针大小也是如此。例如,Horspool在英文文本上表现良好,但在DNA上表现糟糕,因为不同的字母表大小使坏字符规则变得困难。引入良好后缀可以极大地缓解这种情况。

0

使用标准库函数strstr

char *foundit = strstr(haystack, needle);

这很快,只用了我大约5秒钟的时间打字。


29
如果你读了我的问题,你会发现我轻松地超越了它。我很喜欢你的讽刺语气,所以我不计较你的负分了。 - R.. GitHub STOP HELPING ICE

0

我不确定它是否是绝对最好的,但我使用Boyer-Moore有很好的经验。


你知道如何将Boyer-Moore的坏字符表与Two-Way结合起来吗?Glibc对长针(>32字节)进行了变体,但仅检查最后一个字节。问题在于Two-Way需要从左到右搜索针的右侧部分,而Boyer-Moore的坏字符移位在从右到左搜索时效率最高。我尝试在Two-Way中使用从左到右的方式(通过移位表或正常的Two-Way右半部分不匹配,以较长者为准),但在大多数情况下,与正常的Two-Way相比,速度会慢5-10%,并且找不到任何提高性能的情况。 - R.. GitHub STOP HELPING ICE

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