最快的子串搜索算法是啥?

Posted

技术标签:

【中文标题】最快的子串搜索算法是啥?【英文标题】:What is the fastest substring search algorithm?最快的子串搜索算法是什么? 【发布时间】:2011-03-12 03:31:16 【问题描述】:

好的,所以我听起来不像个白痴,我将更明确地说明问题/要求:

Needle(模式)和 haystack(要搜索的文本)都是 C 样式的以空值结尾的字符串。没有提供长度信息;如果需要,必须进行计算。 函数应返回指向第一个匹配项的指针,如果未找到匹配项,则返回 NULL不允许出现故障情况。这意味着任何具有非常量(或大常量)存储要求的算法都需要有一个用于分配失败的回退情况(并且回退处理中的性能会导致最坏情况下的性能)。 使用 C 语言实现,尽管在没有代码的情况下对算法(或指向此类的链接)进行良好描述也可以。

...以及我所说的“最快”:

确定性O(n) 其中n = 干草堆长度。 (但如果将通常为 O(nm) 的算法(例如滚动哈希)与更强大的算法相结合以提供确定性的 O(n) 结果,则可以使用它们的想法。 从不执行(可衡量;if (!needle[1]) 等的几个时钟都可以)比天真的蛮力算法更差,尤其是在可能最常见的情况下非常短的针头上。 (无条件的大量预处理开销是不好的,因为试图以牺牲可能的针头为代价来提高病理针头的线性系数也是如此。) 与任何其他广泛实施的算法相比,给定任意针和草垛,性能相当或更好(搜索时间不差 50%)。 除了这些条件之外,我将对“最快”的定义保持开放式。一个好的答案应该能解释为什么您认为您建议的方法“最快”。

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

更新:我目前的最优算法如下:

对于长度为 1 的针,请使用 strchr。 对于长度为 2-4 的指针,使用机器字一次比较 2-4 个字节,如下所示:在 16 位或 32 位整数中预加载指针并进行移位,然后循环从大海捞针中取出旧字节/循环输入新字节在每次迭代中。 haystack 中的每个字节都只读取一次,并会针对 0(字符串结尾)和 16 位或 32 位比较进行检查。 对于长度 >4 的针,使用带有错误移位表(如 Boyer-Moore)的双向算法,该移位表仅适用于窗口的最后一个字节。为了避免初始化 1kb 表的开销,这对于许多中等长度的针来说将是一个净损失,我保留了一个位数组(32 字节)来标记移位表中的哪些条目被初始化。未设置的位对应于从未出现在针中的字节值,因此可以进行全针长度移位。

留在我脑海中的大问题是:

有没有办法更好地利用坏班次表? Boyer-Moore 通过向后(从右到左)扫描来充分利用它,但双向需要从左到右扫描。 对于一般情况(没有内存不足或二次性能条件),我发现的唯一两个可行的候选算法是Two-Way 和String Matching on Ordered Alphabets。但是是否存在易于检测的不同算法最佳的情况?当然,空间算法中的许多O(m)(其中m 是针长度)可以用于m<100 左右。如果有一个简单的针测试可证明只需要线性时间,那么也可以使用最坏情况二次算法。

奖励积分:

您能否通过假设 needle 和 haystack 都是格式良好的 UTF-8 来提高性能? (对于不同字节长度的字符,格式良好会在针头和干草堆之间施加一些字符串对齐要求,并在遇到不匹配的头字节时允许自动 2-4 字节移位。但是这些约束是否会给您带来很多/超出什么最大后缀计算、良好的后缀移位等已经为您提供了各种算法?)

注意:我很了解大多数算法,只是不知道它们在实践中的表现如何。这是一个很好的参考,所以人们不会一直给我关于算法的参考作为 cmets/answers:http://www-igm.univ-mlv.fr/~lecroq/string/index.html

【问题讨论】:

Algorithms on Strings 列出了相当多的字符串搜索算法。您可能想要描述您考虑过此列表中的哪些算法。 最后那个链接是金! 我不敢相信你还没有接受答案。 @Mehrdad:我正要说没有任何答案可以真正解决所问的问题,但您的似乎是。在你回答的时候,我已经继续前进,并留下了对strstr 的进一步改进作为以后的事情,所以我实际上还没有开始正确阅读你链接的论文,但听起来确实很有希望。感谢并抱歉没有回复您。 【参考方案1】:

我不知道这是否绝对是最好的,但我对 Boyer-Moore 有很好的体验。

【讨论】:

您知道将 Boyer-Moore 的坏班次表与双向工作表结合起来的方法吗? Glibc 对长针(>32 字节)做了一个变体,但只检查最后一个字节。问题是 Two-Way 需要从左到右搜索针的右侧部分,而 Boyer-Moore 的坏移位在从右到左搜索时最有效。我尝试在双向中从左到右使用它(通过移位表或正常双向右半不匹配,以较长者为准),但在大多数情况下,与正常双向相比,我得到了 5-10% 的减速和找不到任何可以提高性能的案例。【参考方案2】:

建立一个包含可能的针和干草堆的测试库。分析几种搜索算法的测试,包括蛮力。选择对您的数据表现最好的那个。

Boyer-Moore 使用坏字符表和好的后缀表。

Boyer-Moore-Horspool 使用了错误的字符表。

Knuth-Morris-Pratt 使用部分匹配表。

Rabin-Karp 使用运行哈希。

它们都在不同程度上用开销来减少比较,因此现实世界的性能将取决于针头和干草堆的平均长度。初始开销越多,输入越长越好。用很短的针,蛮力可能会赢。

编辑:

另一种算法可能最适合查找碱基对、英语短语或单个单词。如果所有输入都存在一种最佳算法,它就会被公开。

想想下面这张小桌子。每个问号可能有不同的最佳搜索算法。

                 short needle     long needle
short haystack         ?               ?
long haystack          ?               ?

这实际上应该是一个图表,每个轴上的输入范围从短到长。如果您在这样的图上绘制每个算法,每个算法都会有不同的签名。一些算法在模式中存在大量重复,这可能会影响搜索基因等用途。影响整体性能的其他一些因素是多次搜索相同的模式并同时搜索不同的模式。

如果我需要一个样本集,我想我会抓取像谷歌或***这样的网站,然后从所有结果页面中删除 html。对于搜索站点,输入一个单词,然后使用建议的搜索短语之一。如果适用,请选择几种不同的语言。使用网页,所有的文本都是短到中等的,所以合并足够多的页面以获得更长的文本。您还可以找到公共领域的书籍、法律记录和其他大量文本。或者只是通过从字典中挑选单词来生成随机内容。但分析的重点是针对您要搜索的内容类型进行测试,因此请尽可能使用真实世界的样本。

我含糊其辞。对于针,我认为短是 8 个字符以下,中是 64 个字符以下,长是 1k 以下。对于大海捞针,我认为短小于 2^10,中小于 2^20,最长不超过 2^30 个字符。

【讨论】:

您对测试库有什么好的建议吗?我在 SO 上问的上一个问题与此有关,但我从未得到任何真正的答案。 (除了我自己的......)它应该是广泛的。即使我对 strstr 应用程序的想法是搜索英文文本,其他人可能正在搜索碱基对序列中的基因...... 它比短/长要复杂一些。对于针来说,与大多数算法的性能相关的大问题是:长度?有没有周期性?针是否包含所有唯一字符(无重复)?还是所有相同的字符?大海捞针中是否有大量字符从未出现过?是否有可能不得不处理想要利用最坏情况性能来削弱您的系统的攻击者提供的针?等等。 这个库和测试代码与 smart 一起存在,您列出的旧算法都没有表现良好。乐华纸也太旧了。 BM、KMP、BMH、KP 大约有 80 项改进。目前最好的(截至 2020 年)在所有情况下都是 EPSM【参考方案3】:

http://www-igm.univ-mlv.fr/~lecroq/string/index.html 你指向的链接是 一些最知名和研究的优秀来源和摘要 字符串匹配算法。

大多数搜索问题的解决方案包括 在预处理开销、时间和 空间要求。没有单 算法在所有情况下都是最优的或实用的。

如果你的目标是为字符串搜索设计一个特定的算法,那么忽略 剩下的我要说的,如果你想开发一个通用的字符串搜索服务 例程然后尝试以下操作:

花一些时间回顾一下具体的优势和劣势 您已经引用的算法。进行 审查的目的是找到一组 涵盖字符串搜索范围和范围的算法 感兴趣。然后,构建一个基于分类器的前端搜索选择器 函数以针对给定输入的最佳算法。这样你就可以 采用最有效的算法来完成这项工作。这尤其 当算法对某些搜索非常好但降级很差时有效。为了 例如,对于长度为 1 的针,蛮力可能是最好的,但是 随着针长度的增加,sustik-moore algoritim 可能会变得更有效(相对于小字母),然后对于更长的针和更大的字母,KMP 或 Boyer-Moore 算法可能会更好。这些只是说明可能策略的示例。

多重算法方法并不是一个新想法。我相信它已被少数人使用 商业排序/搜索包(例如大型机上常用的 SYNCSORT 实现 几种排序算法,并使用启发式方法为给定输入选择“最佳”算法)

每种搜索算法都有几种变体 可以对其性能产生重大影响,因为, 例如,这个paper 说明了。

对您的服务进行基准测试,以对需要额外搜索策略的区域进行分类或更有效地进行分类 调整您的选择器功能。这种方法并不快速或容易,但如果 做得好可以产生非常好的结果。

【讨论】:

感谢您的回复,尤其是我以前从未见过的 Sustik-Moore 链接。多算法方法肯定在广泛使用。 Glibc 基本上是做 strchr、Two-Way without bad character shift table 或 Two-Way with bad character shift table,这取决于 needle_len 是 1、32。我目前的方法是一样的,只是我总是使用班次表;我用一个位集上的 32 字节 memset 替换了这样做所需的 1kb memset,该位集用于标记表中哪些元素已被初始化,即使对于微小的针,我也能获得好处(但不是开销)。 想了想,我真的很好奇 Sustik-Moore 的预期应用是什么。使用小字母表,您将永远不会做出任何重大转变(字母表的所有字符几乎肯定会出现在指针的末端附近)并且有限自动机方法非常有效(小型状态转换表)。所以我无法想象 Sustik-Moore 可能是最佳的任何场景...... 很好的回应——如果我可以为这个特定的答案加注星标,我会的。 @R.. sustik-moore 算法背后的理论是,当针相对较大且字母相对较小(例如,搜索 DNA 序列)时,它应该为您提供更大的平均移位量.在这种情况下,更大只是意味着大于基本 Boyer-Moore 算法在相同输入下产生的结果。很难说这相对于有限自动机方法或其他一些 Boyer-Moore 变体(其中有很多)更有效。这就是为什么我强调要花一些时间研究候选算法的具体优势/劣势。 嗯,我想我只是在从 Boyer-Moore 的坏角色转变的意义上思考转变。不过,随着 BM good 后缀转换的改进,Sustik-Moore 可能在 DNA 搜索方面优于 DFA 方法。整洁的东西。【参考方案4】:

您可能还希望对多种类型的字符串进行不同的基准测试,因为这可能会对性能产生很大影响。算法将根据搜索自然语言(即使在这里仍然可能由于不同的形态学而存在细粒度的区别)、DNA 字符串或随机字符串等来执行不同的操作。

字母大小将在许多算法中发挥作用,针的大小也是如此。例如,由于字母大小不同,Horspool 在英文文本上做得很好,但在 DNA 上做得不好,这让坏字符规则很难受。引入 good-suffix 可以大大缓解这种情况。

【讨论】:

【参考方案5】:

一个非常好的问题。只需添加一些微小的部分...

    有人在谈论 DNA 序列匹配。但是对于 DNA 序列,我们通常做的是为大海捞针构建一个数据结构(例如后缀数组、后缀树或 FM-index)并匹配许多针。这是一个不同的问题。

    如果有人想对各种算法进行基准测试,那就太好了。压缩和后缀数组的构建有非常好的基准,但我还没有看到字符串匹配的基准。潜在的干草堆候选人可能来自SACA benchmark。

    几天前我正在测试 the page you recommended 的 Boyer-Moore 实现(编辑:我需要一个像 memmem() 这样的函数调用,但它不是标准函数,所以我决定实现它) .我的基准测试程序使用随机 haystack。似乎该页面中的 Boyer-Moore 实现比 glibc 的 memmem() 和 Mac 的 strnstr() 快几倍。如果您有兴趣,实现是here,基准代码是here。这绝对不是一个现实的基准,但它是一个开始。

【讨论】:

如果你有一些好的针头和 SACA 基准测试中的干草堆候选者一起测试,请将它们作为答案发布到我的 other question,如果没有得到更好的答案,我会标记它接受。 关于你的记忆和 Boyer-Moore,Boyer-Moore(或者说是 Boyer-Moore 的增强功能之一)很可能在随机数据上表现最好。随机数据具有极低的周期性概率和导致二次最坏情况的长时间部分匹配。我正在寻找一种将 Boyer-Moore 和 Two-Way 结合起来的方法,或者有效地检测 Boyer-Moore 何时“使用安全”,但到目前为止我还没有取得任何成功。顺便说一句,我不会使用 glibc 的 memmem 作为比较。我对与 glibc 基本相同的算法的实现要快几倍。 正如我所说,这不是我的实现。感谢 Christian Charras 和 Thierry Lecroq。我可以想象为什么随机输入不利于基准测试,我确信 glibc 选择算法是有原因的。我也猜 memmem() 没有有效地实现。我会尝试。谢谢。【参考方案6】:

使用标准库strstr:

char *foundit = strstr(haystack, needle);

速度非常快,只花了我大约 5 秒的时间打字。

【讨论】:

如果您阅读我的问题,您会发现我很容易超越它。我很喜欢你的讽刺,不过我会跳过 -1。【参考方案7】:

这里是Python's search implementation,在整个核心中使用。 cmets 表明它使用 压缩的 boyer-moore delta 1 表

我自己对字符串搜索进行了一些相当广泛的实验,但它是针对多个搜索字符串的。 Horspool 和 Bitap 的汇编实现通常可以对抗 Aho-Corasick 这样的算法,因为它们的模式计数较低。

【讨论】:

【参考方案8】:

更快的“搜索单个匹配字符”(ala strchr)算法。

重要提示:

这些函数使用“(前导|尾随)零的数量/计数”gcc 编译器内在-__builtin_ctz。这些函数可能只在具有执行此操作的指令的机器(即 x86、ppc、arm)上才能快速运行。

这些函数假定目标体系结构可以执行 32 位和 64 位未对齐加载。如果您的目标架构不支持这一点,您将需要添加一些启动逻辑来正确对齐读取。

这些函数与处理器无关。如果目标 CPU 有向量指令,你可能会做得更好。例如,下面的 strlen 函数使用 SSE3,可以简单地修改为 XOR 扫描的字节以查找除 0 之外的字节。在运行 Mac OS X 10.6 (x86_64) 的 2.66GHz Core 2 笔记本电脑上执行的基准测试:

strchr 为 843.433 MB/s findFirstByte64 为 2656.742 MB/s strlen 为 13094.479 MB/s

... 32 位版本:

#ifdef __BIG_ENDIAN__
#define findFirstZeroByte32(x) ( uint32_t _x = (x); _x = ~(((_x & 0x7F7F7F7Fu) + 0x7F7F7F7Fu) | _x | 0x7F7F7F7Fu); (_x == 0u)   ? 0 : (__builtin_clz(_x) >> 3) + 1; )
#else
#define findFirstZeroByte32(x) ( uint32_t _x = (x); _x = ~(((_x & 0x7F7F7F7Fu) + 0x7F7F7F7Fu) | _x | 0x7F7F7F7Fu);                    (__builtin_ctz(_x) + 1) >> 3; )
#endif

unsigned char *findFirstByte32(unsigned char *ptr, unsigned char byte) 
  uint32_t *ptr32 = (uint32_t *)ptr, firstByte32 = 0u, byteMask32 = (byte) | (byte << 8);
  byteMask32 |= byteMask32 << 16;
  while((firstByte32 = findFirstZeroByte32((*ptr32) ^ byteMask32)) == 0)  ptr32++; 
  return(ptr + ((((unsigned char *)ptr32) - ptr) + firstByte32 - 1));

...和 ​​64 位版本:

#ifdef __BIG_ENDIAN__
#define findFirstZeroByte64(x) ( uint64_t _x = (x); _x = ~(((_x & 0x7F7F7F7F7f7f7f7full) + 0x7F7F7F7F7f7f7f7full) | _x | 0x7F7F7F7F7f7f7f7full); (_x == 0ull) ? 0 : (__builtin_clzll(_x) >> 3) + 1; )
#else
#define findFirstZeroByte64(x) ( uint64_t _x = (x); _x = ~(((_x & 0x7F7F7F7F7f7f7f7full) + 0x7F7F7F7F7f7f7f7full) | _x | 0x7F7F7F7F7f7f7f7full);                    (__builtin_ctzll(_x) + 1) >> 3; )
#endif

unsigned char *findFirstByte64(unsigned char *ptr, unsigned char byte) 
  uint64_t *ptr64 = (uint64_t *)ptr, firstByte64 = 0u, byteMask64 = (byte) | (byte << 8);
  byteMask64 |= byteMask64 << 16;
  byteMask64 |= byteMask64 << 32;
  while((firstByte64 = findFirstZeroByte64((*ptr64) ^ byteMask64)) == 0)  ptr64++; 
  return(ptr + ((((unsigned char *)ptr64) - ptr) + firstByte64 - 1));

Edit 2011/06/04 OP 在 cmets 中指出该解决方案存在“无法克服的错误”:

它可以读取过去寻找的字节或空终止符,这可以访问未映射的页面或没有读取权限的页面。除非它们对齐,否则您根本不能在字符串函数中使用大量读取。

这在技术上是正确的,但几乎适用于任何对大于单个字节的块进行操作的算法,包括 cmets 中 OP 建议的 the method:

典型的strchr 实现并不幼稚,但比您提供的要高效得多。使用最广泛的算法见文末:http://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord

它本身也与 alignment 本身无关。诚然,这可能会导致在使用的大多数常见架构上讨论的行为,但这更多与微架构实现细节有关 - 如果未对齐的读取跨越 4K 边界(再次,典型),那么该读取将导致程序如果下一个 4K 页面边界未映射,则终止错误。

但这不是答案中给出的算法中的“错误”——这种行为是因为像 strchrstrlen 这样的函数不接受 length 参数来限制搜索的大小。搜索char bytes[1] = 0x55;,出于我们讨论的目的,它恰好位于 4K VM 页面边界的最末端,下一页未映射,strchr(bytes, 0xAA)(其中strchr 是一个字节-at- a-time implementation) 将以完全相同的方式崩溃。 strchr 相关表亲 strlen 同上。

如果没有length 参数,就无法判断何时应该退出高速算法并返回到逐字节算法。一个更有可能的“错误”是读取“超过分配的大小”,根据各种 C 语言标准,这在技术上会导致 undefined behavior,并且会被 valgrind 之类的东西标记为错误。

总而言之,任何对大于字节块的操作都会更快,因为这回答了代码和 OP 指出的代码,但必须具有字节准确的读取语义,如果存在没有length 参数来控制“最后一次读取”的极端情况。

此答案中的代码是一个内核,如果目标 CPU 具有类似 ctz 的快速指令,则该内核能够快速找到自然 CPU 字大小块中的第一个字节。添加诸如确保它仅在正确对齐的自然边界或某种形式的length 边界上运行的东西是微不足道的,这将允许您切换出高速内核并进入较慢的逐字节检查.

OP 还在 cmets 中声明:

至于你的 ctz 优化,它只对 O(1) 尾操作有影响。它可以提高小字符串的性能(例如strchr("abc", 'a');,但肯定不能用于任何主要大小的字符串。

这句话是否正确在很大程度上取决于所讨论的微架构。使用规范的 4 阶段 RISC 流水线模型,几乎可以肯定是正确的。但是对于当代的乱序超级标量 CPU 来说,这是否是真的很难说,因为它的核心速度完全可以使内存流速度相形见绌。在这种情况下,“可以退役的指令数”相对于“可以流式传输的字节数”存在很大差距不仅是合理的,而且很普遍,因此您拥有“对于可以流式传输的每个字节,可以退出的指令数”。如果它足够大,ctz + shift 指令可以“免费”完成。

【讨论】:

"对于长度为 1 的针,使用strchr。"- 您要求最快的子字符串搜索算法。查找长度为 1 的子串只是一种特殊情况,也可以进行优化。如果您将当前的特殊情况代码替换为长度为 1 (strchr) 的子字符串,则事情会(可能取决于strchr 的实现方式)变得更快。上述算法比典型的幼稚 strchr 实现快近 3 倍。 OP 说字符串被正确地以空值终止,所以你对char bytes[1] = 0x55; 的讨论是无关紧要的。对于任何事先不知道长度的单词读取算法,您对此的评论非常相关。 这个问题不适用于我引用的版本,因为你只在对齐的指针上使用它——至少正确的实现是这样做的。 @R,它与“对齐指针”无关。假设,如果您的架构支持具有字节级粒度的 VM 保护,并且每个 malloc 分配在任一侧“充分填充”并且VM系统对该分配实施字节粒度保护。 .. 指针是否对齐(假设微不足道的 32 位 int 自然对齐)没有实际意义——对齐的读取仍然有可能读取超过分配的大小。 ANY 读取过去的分配大小是undefined behavior @johne:+1 评论。从概念上讲你是对的,但现实情况是字节粒度保护的存储和执行都非常昂贵,以至于它们不存在,也永远不会存在。如果您知道底层存储是从 mmap 的等价物获得的页面粒度映射,那么对齐就足够了。【参考方案9】:

我知道这是一个老问题,但大多数糟糕的班次表都是单字符的。如果它对您的数据集有意义(例如,特别是如果它是书面文字),并且如果您有可用空间,则可以通过使用由 n-gram 而不是单个字符组成的坏移位表来获得显着的加速。

【讨论】:

【参考方案10】:

看到我们的技术报告在本次讨论中被引用,我感到很惊讶;我是上面名为 Sustik-Moore 的算法的作者之一。 (我们的论文中没有使用该术语。)

我想在此强调,对我而言,该算法最有趣的特点是证明每个字母最多检查一次非常简单。对于较早的 Boyer-Moore 版本,他们证明每个字母最多检查 3 次,之后最多检查 2 次,并且这些证明涉及更多(参见论文中的引用)。因此,我也看到了展示/研究这个变体的教学价值。

在本文中,我们还描述了在放宽理论保证的同时针对效率的进一步变化。这是一篇简短的论文,我认为这些材料对于普通高中毕业生来说应该是可以理解的。

我们的主要目标是让其他可以进一步改进它的人注意这个版本。字符串搜索有如此多的变化,仅凭我们一个人不可能想到这个想法可以带来的所有好处。 (固定文本和改变模式、固定模式不同文本、预处理可能/不可能、并行执行、在大文本中查找匹配子集、允许错误、接近匹配等等等)

【讨论】:

您是否碰巧知道可用的 C 或 C++ 实现?我正在考虑将其用于一些 dna 基序搜索(确切的基序匹配)。如果没有,也许我会尝试自己开发一个实现并提交以提升算法 由于没有已知的可用实现,Sustik-Moore/2BLOCK 算法似乎不太可能在实践中使用,并且在像"The Exact String Matching Problem: a Comprehensive Experimental Evaluation"这样的摘要论文的结果中继续被省略【参考方案11】:

只要搜索“fastest strstr”,如果你看到感兴趣的东西就问我。

在我看来,您对自己施加了太多限制(是的,我们都希望在最大搜索器处实现亚线性),但是需要真正的程序员介入,直到那时我认为散列方法只是一个漂亮的-边缘解决方案(BNDM 很好地加强了较短的 2..16 模式)。

只是一个简单的例子:

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

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

将 Pattern(32bytes) 搜索到 String(206908949bytes) 作为一行... 跳过性能(越大越好):129%,160239051 次跳过/迭代 Two-Way_hits/Two-Way_clocks:0/816 双向性能:247KB/时钟

桑梅斯, 问候

【讨论】:

【参考方案12】:

您在问题中提到的双向算法(顺便说一句,这令人难以置信!)最近已得到改进,可以一次有效地处理多字节单词:Optimal Packed String Matching。

我还没有阅读整篇论文,但似乎它们依赖于几个新的、特殊的 CPU 指令(包括在例如 SSE 4.2 中)它们的时间复杂度声明为 O(1),但如果它们不是他们可以在 O(log log w) 时间内对 w 位单词进行模拟,这听起来还不错。

【讨论】:

【参考方案13】:

例如,您可以实现 4 种不同的算法。每 M 分钟(根据经验确定)在当前真实数据上运行所有 4 分钟。累积 N 次运行的统计信息(也待定)。然后在接下来的 M 分钟内只使用获胜者。

记录获胜的统计数据,以便您可以用新算法替换永远不会获胜的算法。将优化工作集中在最成功的例程上。对硬件、数据库或数据源进行任何更改后,请特别注意统计信息。如果可能,请将该信息包含在统计日志中,这样您就不必从日志日期/时间戳中找出它。

【讨论】:

【参考方案14】:

发表于 2011 年,我相信它很可能是 Dany Breslauer、Roberto Grossi 和 Filippo Mignosi 的 "Simple Real-Time Constant-Space String Matching" 算法。

更新:

2014 年,作者发布了这项改进:Towards optimal packed string matching。

【讨论】:

哇,谢谢。我正在看报纸。如果结果比我的更好,我一定会接受你的回答。 它是我已经在使用的“双向”算法的变体,因此调整我的代码以使用它实际上可能很容易。不过,我必须更详细地阅读这篇论文才能确定,而且我需要评估所做的更改是否与我使用的“坏字符表”兼容,这大大加快了常见情况。 你还没有接受@Mehrdad 的回答! :-) 好吧,公平地说,这是一个仅限链接的答案,这里不应该允许。 @DavidWallace:什么?它有论文标题和作者。即使链接失效,您也可以找到论文。你希望我做什么,为算法编写伪代码?是什么让你认为我理解算法?【参考方案15】:

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

您可能会发现它很有用。 另外,如果我必须快速调用子字符串搜索算法,我会选择 Knuth-Morris-Pratt。

【讨论】:

感谢您的链接。这些测试对于典型情况的时间看起来很有趣,但对于捕捉最坏情况的时间却没有。【参考方案16】:

最快的子字符串搜索算法将取决于上下文:

    字母大小(例如 DNA 与英文) 针长

2010 年的论文 "The Exact String Matching Problem: a Comprehensive Experimental Evaluation" 提供了包含 51 种算法(具有不同的字母大小和针长)的运行时表,因此您可以根据您的上下文选择最佳算法。

所有这些算法都有 C 实现,还有一个测试套件,在这里:

http://www.dmi.unict.it/~faro/smart/algorithms.php

【讨论】:

不,2010 年的论文错过了最新最好的论文,即 epsm,“精确打包字符串匹配”。 epsm 在所有情况下都是最好的。 faro 的新网站目前已损坏,您可以在 Internet 存档中看到它。 @rurban: 2010 年的论文错过了 2013 年的论文? @user541686:完全正确。最新最好的还不知道。 op 提到了那篇旧论文,它错过了最新和最好的。【参考方案17】:

目前最快的是 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 进行了优化,并且仍然是最快的。

【讨论】:

以上是关于最快的子串搜索算法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

Java中最快的子字符串搜索方法是啥

相似子串快速搜索

算法导论-动态规划学习笔记day01

搜索排序数组的最快搜索算法

最快排序和搜索算法的最简代码实现_转

此字谜子串搜索算法中的数学