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

Posted

技术标签:

【中文标题】在一个字符串中搜索另一个字符串的字谜?【英文标题】:Search a string for an anagram of another string? 【发布时间】:2013-01-18 01:00:25 【问题描述】:

我正在尝试从字符串 text 中找到一个子字符串,它是字符串 pattern 的变位词。

我的问题: 可以将Rabin-Karp algorithm 调整为此目的吗?还是有更好的算法?

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

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

更新 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

【问题讨论】:

你的暴力破解算法是什么? 只需遍历每个文本子字符串,将每个子字符串与 O(n^2) 中的模式进行比较!所以总数是 O(n^3) , 你的“字母”是什么?真的是 A-Z,还是更大的一组符号? @erickson:它只是小写的 a-z 字母, 我添加了一个更详细的关于滚动字谜散列的描述,它会产生很少的错误冲突。 【参考方案1】:

计算不依赖于模式中字母顺序的模式哈希(例如,使用每个字母的字符代码之和)。然后以“滚动”方式对文本应用相同的散列函数,就像在 Rabin-Karp 中一样。如果哈希匹配,您需要针对文本中的当前窗口执行模式的完整测试,因为哈希也可能与其他值发生冲突。


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

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

例如,假设您将字母“a”-“z”的哈希值计算为无符号的 64 位值。使用这样的表格:

符号 |代码 |代码-1 --------+------+--------- 一个 | 3 | 12297829382473034411 乙 | 5 | 14757395258967641293 c | 7 | 7905747460161236407 d | 11 | 3353953467947191203 电子| 13 | 5675921253449092805 ... z | 103 | 15760325033848937303

n 的乘法逆元是乘以 n, 对某个数取模得到 1 的数。这里的模数是 264,因为您使用的是 64 位数字。例如,5 * 14757395258967641293 应该是 1。这行得通,因为你只是multiplying in GF(264).

计算第一个素数的列表很容易,您的平台应该有一个库来efficiently 计算这些数字的乘法倒数。

从数字 3 开始编码,因为 2 是整数大小的互质数(在您正在处理的任何处理器上都是 2 的幂),并且不能反转。

【讨论】:

你确定平均情况是 O(n) 吗?似乎很多不同的子字符串都可能是误报。 我已经实现了这个哈希(对字符 ASCII 码求和)并使用 rabin-karp 效果很好,谢谢 :) 我已经测试过了,每个字符串都会碰撞 10% 的整个测试子字符串,是否可以使用更好的滚动哈希? 您只是简单地将int 中的代码相加吗?您可以尝试计算long 中代码的乘积。如果您的哈希码分布在更大的空间中,冲突的可能性就会降低。 @RamiJarrar 看看你是否可以“不接受”这个答案。我认为Dave 和templatetypedef 的答案(它们本质上是相同的解决方案)更容易理解并且应该有效。使用我的解决方案,您需要像直方图这样的东西来验证窗口中的内容实际上是一个字谜,而不仅仅是一个错误的碰撞。一旦你有了它,用它来实施他们的解决方案只是一小步。【参考方案2】:

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

更具体地说,创建一个关联数组 A 从字符到它们的频率的映射。如果要搜索字符串 P 的变位词,请阅读第一个 |P|将文本字符串 T 中的字符转换为 A 并适当地构建直方图。您可以将窗口向前滑动一步,并通过减少与窗口中第一个字符相关联的频率,然后增加与滑入窗口中的新字符相关联的频率,在 O(1) 关联数组操作中更新 A。

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

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

希望这会有所帮助!

【讨论】:

啊哈,我会测试这个算法,但是,我认为(有人告诉我)有一个更快的算法,而且只有恒定的内存! 您可以做的优化是,每次比较直方图时,记录您“丢失”了多少个字符。然后您可以在再次检查之前向前移动那么多字符(仍在更新直方图)。 这实际上是一个比选择作为解决方案的那个更好的算法核心。让它真正快速的技巧是保留一组当前与模式直方图不匹配的直方图元素。您可以为窗口的每次移动以恒定的时间增量调整此设置。当集合为空时,你有一个匹配!当卓越的解决方案没有获胜时,我讨厌它。 对于那些只阅读票数最高的答案的人; Dave's answer 对此进行了改进,通过将直方图初始化为搜索字符串的负图像并检测滑动窗口何时将其归零来消除比较。【参考方案3】:

    创建一个包含 26 个整数(设置为零)的数组 letter_counts 和一个变量 missing_count 来保存丢失字母的计数。

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

    假设子字符串的大小为 k。查看字符串的前 k 个字母。将 letter_counts 的关联 int 加 1。如果增加后的值为

    现在,我们像这样沿着字符串“前滚”。 一种。删除最接近窗口开头的字母,减少 letter_counts 的关联成员。如果在递减之后,我们有一个 int

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

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

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

--- 编辑---

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

【讨论】:

【参考方案4】:

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

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

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

【讨论】:

以上是关于在一个字符串中搜索另一个字符串的字谜?的主要内容,如果未能解决你的问题,请参考以下文章

如何优化我的字谜搜索功能?

给定一个字符串数组,返回所有的字谜字符串组

搜索字谜时,是否可以保持单词的大写和小写?

查找给定单词的字谜

字符串数组只包含字谜?

PHP中的字谜算法