在字符串中搜索另一个字符串的字谜?

时间:2013-02-02 23:59:51

标签: c++ string algorithm hash anagram

我正在尝试从字符串text中找到字符串pattern的字符串。

我的问题: Rabin-Karp algorithm是否可以针对此目的进行调整?还是有更好的算法?

我尝试过一种蛮力算法,在我的情况下不起作用,因为文本和模式每个都可以达到一百万个字符。

更新:我听说有一种使用O(1)空间的最坏情况O(n 2 )算法。有谁知道这个算法是什么?

更新2:作为参考,这里是Rabin-Karp算法的伪代码:

function RabinKarp(string s[1..n], string sub[1..m])
    hsub := hash(sub[1..m]);  hs := hash(s[1..m])
    for i from 1 to n-m+1
       if hs = hsub
          if s[i..i+m-1] = sub
              return i
       hs := hash(s[i+1..i+m])
    return not found

这使用滚动哈希函数来允许计算O(1)中的新哈希, 所以在最坏的情况下整体搜索是O(nm),但是在最好的情况下具有良好的散列函数是O(m + n)。是否存在滚动哈希函数,在搜索字符串的字符时会产生few collisions

4 个答案:

答案 0 :(得分:8)

一种选择是保持一个滑动窗口,其中包含窗口中包含的字母的直方图。如果该直方图最终等于应该找到其字谜的字符串的字符直方图,那么您知道您正在查看的是匹配并可以输出它。如果没有,你知道你拥有的东西不可能是匹配。

更具体地说,创建一个关联数组从字符到其频率的映射。如果要搜索字符串P的字谜,请阅读第一个| P |从文本字符串T到A的字符并适当地构建直方图。您可以向前滑动窗口并在O(1)关联数组操作中更新A,方法是递减与​​窗口中第一个字符关联的频率,然后递增与滑入窗口的新字符关联的频率。

如果当前窗口和模式窗口的直方图非常不同,那么您应该能够相当快地比较它们。具体来说,假设您的字母表是Σ。在最坏的情况下,比较两个直方图会花费时间O(|Σ|),因为您必须使用参考直方图检查直方图A中的每个字符/频率对。但是,在最好的情况下,您会立即找到导致A与参考直方图不匹配的字符,因此您不需要查看整个字符。

理论上,这种方法的最坏情况运行时是O(| T ||Σ| + | P |),因为你必须做O(n)工作来构建初始直方图,然后必须做最坏的 - T中每个字符的Σ工作情况。但是,我希望在实践中这可能要快得多。

希望这有帮助!

答案 1 :(得分:8)

计算模式的散列,该散列不依赖于模式中字母的顺序(例如,使用每个字母的字符代码的总和)。然后在"滚动"中应用相同的哈希函数时尚的文本,如Rabin-Karp。如果散列匹配,则需要对文本中的当前窗口执行模式的完整测试,因为散列也可能与其他值冲突。


通过将字母表中的每个符号与素数相关联,然后将这些素数的乘积计算为哈希码,您将获得更少的冲突。

但是,如果您想要计算这样的正在运行的产品,那么会有一些数学技巧可以帮助您:每次执行窗口时,运行的哈希码乘以 multiplicative inverse离开窗口的符号代码,然后乘以进入窗口的符号代码。

例如,假设您正在计算字母的散列' a' - ' z'作为无符号的64位值。使用这样的表:

symbol | code | code-1
-------+------+---------------------
   a   |    3 | 12297829382473034411
   b   |    5 | 14757395258967641293
   c   |    7 |  7905747460161236407
   d   |   11 |  3353953467947191203
   e   |   13 |  5675921253449092805
  ...
   z   |  103 | 15760325033848937303

n 的乘法逆是在乘以 n 模数某个数时产生1的数字。这里的模数是2 64 ,因为您使用的是64位数字。因此,5 * 14757395258967641293应该是1,例如。这很有效,因为你只是multiplying in GF(264).

计算第一个素数的列表很简单,你的平台应该有一个库efficiently来计算这些数的乘法逆。

使用数字3开始编码,因为2与整数的大小是共同的(在你正在处理的任何处理器上的幂为2),并且不能被反转。

答案 2 :(得分:3)

  1. 创建一个26个整数的数组letter_counts(设置为零)和一个变量missing_count来保存缺少的字母数。

  2. 对于子字符串中的每个字母,将letter_counts的关联int减1,并将missing_count递增1(因此missing_count将最终等于子字符串的大小)。

  3. 假设子字符串的大小为k。查看字符串的前k个字母。将letter_counts的关联int增加1.如果递增后,该值为< = 0,则missing_count减1。

  4. 现在,我们像这样沿着字符串向前滚动。 一个。删除最接近窗口开头的字母,减少letter_counts的关联成员。如果在递减之后,我们有一个int< 0,然后将missing_count递增1。 湾在窗口外添加字符串的第一个字母。增加letter_counts的关联成员。如果在递增后我们有一个int< = 0,那么missing_count减1。

  5. 如果在任何时候missing_count == 0,我们的窗口中会有一个搜索字符串的字谜。

    我们维护的不变量是missing_count保存子字符串中不在窗口中的字母数。如果为零,则窗口中的字母与子字符串中的字母完全匹配。

    这是Theta(n) - 线性时间,因为我们只查看每个字母一次。

    ---编辑---

    letter_counts只需要存储子字符串的不同字母,并且只需要保存与子字符串(signed)大小一样大的整数。因此,内存使用量在子字符串的大小上是线性的,但在字符串的大小上是恒定的。

答案 3 :(得分:0)

我建议这样做可能很愚蠢,但另一种选择可能是将两个字符串分解为数组,然后逐个字符地递归搜索它们。

为避免重复的字符匹配,如果在text数组中找到一个字符,则会删除其各自的数组索引,从而有效缩小每次匹配的完成时间阵列扫描,同时确保包含2x'B'的textpattern与3x'B'不匹配。

为了增加性能,您可以在逐个字符计数之前扫描两个字符串,并列出每个字符串中存在哪些字母,然后比较这些列表以查看是否存在任何差异(例如尝试在“apple”中找到字母“z”,如果有字符串标记为“Anagram not possible”。