一个字符有什么好的搜索算法吗?


23

我知道几种基本的字符串匹配算法,例如KMP或Boyer-Moore,但是所有这些算法都在搜索之前就对模式进行了分析,但是,如果有一个字符,就没有太多要分析的了。那么,有什么比天真搜索更好的算法来比较文本的每个字符呢?


13
您可以向其抛出SIMD指令,但是没有什么比O(n)更好的了。
CodesInChaos

7
对于同一字符串中的单个搜索还是多个搜索?
Christophe

KMP绝对不是我将其称为“基本”字符串匹配算法的东西……我什至也不知道它是如此之快,但这在历史上很重要。如果您想要基本的东西,请尝试Z算法。
Mehrdad

假设搜索算法没有看过一个字符位置。这样一来,就无法区分在该位置具有针形字符的字符串和在该位置具有不同字符的字符串。
user253751 '16

Answers:


29

可以理解,最坏的情况是O(N),存在一些非常好的微优化。

天真的方法对每个字符执行字符比较和文本结尾比较。

使用前哨(即目标字符在文本末尾的副本)可将比较次数减少到每个字符1次。

在微博级别,有:

#define haszero(v)      ( ((v) - 0x01010101UL) & ~(v) & 0x80808080UL )
#define hasvalue(x, n)  ( haszero((x) ^ (~0UL / 255 * (n))) )

知道单词(x)中的任何字节是否具有特定值(n)。

v - 0x01010101UL只要对应的in字节v为零或大于,子表达式就对任何字节中的高位进行求值0x80

该子表达式~v & 0x80808080UL计算以字节为单位设置的高位,其中的字节v未设置其高位(因此该字节小于0x80)。

通过对这两个子表达式(haszero)进行“与”运算,结果是设置了高位,其中的字节v为零,这是因为由于0x80第二个子表达式(4月27日, 1987年,艾伦·迈克罗夫特(Alan Mycroft)。

现在,我们可以对一个要测试的值(x)与一个用我们感兴趣的字节值填充的单词()进行异或n。由于将值与自身进行XOR运算会导致字节为零,否则为非零,因此我们可以将结果传递给haszero

这通常在典型的strchr实现中使用。

(Stephen M Bennet于2009年12月13日提出了这一建议。有关更多详细信息,请参见著名的Bit Twiddling Hacks)。


聚苯乙烯

对于1111旁边的的任何组合,此代码均无效0

hack通过了蛮力测试(请耐心等待):

#include <iostream>
#include <limits>

bool haszero(std::uint32_t v)
{
  return (v - std::uint32_t(0x01010101)) & ~v & std::uint32_t(0x80808080);
}

bool hasvalue(std::uint32_t x, unsigned char n)
{
  return haszero(x ^ (~std::uint32_t(0) / 255 * n));
}

bool hasvalue_slow(std::uint32_t x, unsigned char n)
{
  for (unsigned i(0); i < 32; i += 8)
    if (((x >> i) & 0xFF) == n)
      return true;

  return false;
}

int main()
{
  const std::uint64_t stop(std::numeric_limits<std::uint32_t>::max());

  for (unsigned c(0); c < 256; ++c)
  {
    std::cout << "Testing " << c << std::endl;

    for (std::uint64_t w(0); w != stop; ++w)
    {
      if (w && w % 100000000 == 0)
        std::cout << w * 100 / stop << "%\r" << std::flush;

      const bool h(hasvalue(w, c));
      const bool hs(hasvalue_slow(w, c));

      if (h != hs)
        std::cerr << "hasvalue(" << w << ',' << c << ") is " << h << '\n';
    }
  }

  return 0;
}

对于答案的大量支持,使假设一个字符=一个字节,这已不再是标准

谢谢你的发言。

答案只不过是一篇关于多字节/可变宽度编码的文章:-)(公平地说,这不是我的专业领域,我不确定这是OP所寻找的东西)。

无论如何,在我看来,上述想法/技巧在某种程度上可以适用于MBE(尤其是自同步编码):

  • Johan的评论所述,黑客可以“轻松地”扩展为适用于双字节或其他任何内容(当然,您不能对其进行过多扩展);
  • 在多字节字符串中定位字符的典型函数:
  • 前哨技术可以稍加预见地使用。

1
这是SIMD操作的穷人版本。
罗斯兰

@卢斯兰绝对!对于有效的比特旋转黑客来说,通常就是这种情况。
manlio '16

2
好答案。从可读性的角度来看,我不明白为什么要0x01010101UL一行一行地写~0UL / 255。它给人的印象是它们必须是不同的值,否则,为什么要用两种不同的方式编写它?
2016年

3
这很酷,因为它一次检查4个字节,但是由于#defines会扩展为,因此它需要多条(8?)指令( (((x) ^ (0x01010101UL * (n)))) - 0x01010101UL) & ~((x) ^ (0x01010101UL * (n)))) & 0x80808080UL )。单字节比较不会更快吗?
杰德·沙夫

1
@DocBrown,可以轻松地使代码适用于双字节(即半字)或半字节或其他任何东西。(考虑到我提到的警告)。
约翰-恢复莫妮卡

20

任何搜索给定文本中单个字符的每次出现的文本搜索算法都必须至少读取一次文本的每个字符,这是显而易见的。并且由于这足以进行一次搜索,因此就没有更好的算法了(当考虑运行时间顺序时,在这种情况下称为“线性”或O(N),其中N是字符数搜索)。

但是,对于实际的实现,肯定会有很多微优化可能,它们不会整体上更改运行时间顺序,而是会降低实际运行时间。而且,如果目标不是要找到单个字符的所有出现,而只是找到第一个字符,那么您当然可以在第一次出现时停止。尽管如此,即使对于这种情况,最坏的情况仍然是您要查找的字符是文本中的最后一个字符,因此,此目标的最坏情况运行时间顺序仍然是O(N)。


8

如果您的“干草堆”被搜索了不止一次,那么基于直方图的方法将非常快。建立直方图后,您只需要查找指针即可找到答案。

如果您只需要知道搜索的模式是否存在,则可以使用一个简单的计数器来提供帮助。它可以扩展为包括在干草堆中找到每个字符的位置或首次出现的位置。

string haystack = "agtuhvrth";
array<int, 256> histogram{0};
for(character: haystack)
     ++histogram[character];

if(histogram['a'])
    // a belongs to haystack

1

如果您需要多次搜索同一字符串中的字符,那么一种可行的方法是将字符串分成多个较小的部分(可能是递归的),并对每个部分使用Bloom过滤器。

因为Bloom筛选器可以肯定地告诉您字符是否不在该筛选器“表示”的字符串部分中,所以在搜索字符时可以跳过某些部分。

例如:对于以下字符串,可以将其分为4个部分(每个11个字符长),并为每个部分填充一个Bloom过滤器(可能长4个字节),并包含该部分的字符:

The quick brown fox jumps over the lazy dog 
          |          |          |          |

您可以加快字符搜索的速度:例如,a对于bloom过滤器使用好的哈希函数,它们会告诉您-非常有可能-您不必在第一部分,第二部分或第三部分中都进行搜索。因此,您不必检查33个字符,而只需要检查16个字节(对于4个Bloom过滤器)。仍然O(n)是一个常数(分数)因子(为了使其有效,您需要选择更大的部分,以最小化为搜索字符计算哈希函数的开销)。

使用类似树的递归方法应该可以使您接近O(log n)

The quick brown fox jumps over the lazy dog 
   |   |   |   |   |   |   |   |---|-X-|   |  (1 Byte)
       |       |       |       |---X---|----  (2 Byte)
               |               |-----X------  (3 Byte)
-------------------------------|-----X------  (4 Byte)
---------------------X---------------------|  (5 Byte)

在此配置中,需要检查(再次,假设我们很幸运并且没有从其中一个过滤器中得到误报)进行检查

5 + 2*4 + 3 + 2*2 + 2*1 bytes

进入最后一部分(其中需要检查3个字符,直到找到为止a)。

使用良好的(如上所述更好的)细分方案,您应该会获得很好的结果。(注意:如示例所示,树的根处的Bloom过滤器应大于叶子附近的过滤器,以获得较低的误报率)


亲爱的唐纳德,请解释一下您为什么认为我的回答没有帮助。
Daniel Jour

1

如果要多次搜索字符串(典型的“搜索”问题),则解决方案可以为O(1)。解决方案是建立索引。

例如:

映射,其中键是字符,值是字符串中该字符的索引列表。

这样,单个地图查找就可以提供答案。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.