我正在尝试从字符串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
?
答案 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)
创建一个26个整数的数组letter_counts(设置为零)和一个变量missing_count来保存缺少的字母数。
对于子字符串中的每个字母,将letter_counts的关联int减1,并将missing_count递增1(因此missing_count将最终等于子字符串的大小)。
假设子字符串的大小为k。查看字符串的前k个字母。将letter_counts的关联int增加1.如果递增后,该值为< = 0,则missing_count减1。
现在,我们像这样沿着字符串向前滚动。 一个。删除最接近窗口开头的字母,减少letter_counts的关联成员。如果在递减之后,我们有一个int< 0,然后将missing_count递增1。 湾在窗口外添加字符串的第一个字母。增加letter_counts的关联成员。如果在递增后我们有一个int< = 0,那么missing_count减1。
如果在任何时候missing_count == 0,我们的窗口中会有一个搜索字符串的字谜。
我们维护的不变量是missing_count保存子字符串中不在窗口中的字母数。如果为零,则窗口中的字母与子字符串中的字母完全匹配。
这是Theta(n) - 线性时间,因为我们只查看每个字母一次。
---编辑---
letter_counts只需要存储子字符串的不同字母,并且只需要保存与子字符串(signed)大小一样大的整数。因此,内存使用量在子字符串的大小上是线性的,但在字符串的大小上是恒定的。
答案 3 :(得分:0)
我建议这样做可能很愚蠢,但另一种选择可能是将两个字符串分解为数组,然后逐个字符地递归搜索它们。
为避免重复的字符匹配,如果在text
数组中找到一个字符,则会删除其各自的数组索引,从而有效缩小每次匹配的完成时间阵列扫描,同时确保包含2x'B'的text
与pattern
与3x'B'不匹配。
为了增加性能,您可以在逐个字符计数之前扫描两个字符串,并列出每个字符串中存在哪些字母,然后比较这些列表以查看是否存在任何差异(例如尝试在“apple”中找到字母“z”,如果有字符串标记为“Anagram not possible”。