【问题标题】:Most common substring of length X最常见的长度为 X 的子串
【发布时间】:2010-12-08 11:53:55
【问题描述】:

我有一个字符串 s,我想搜索在 s 中最常出现的长度为 X 的子字符串。允许重叠的子字符串。

例如,如果 s="aoaoa" 且 X=3,则算法应找到“aoa”(在 s 中出现 2 次)。

是否存在在 O(n) 时间内完成此操作的算法?

【问题讨论】:

  • 您拼错了“lenght”和“example”。正确的是“长度”和“示例”

标签: algorithm substring


【解决方案1】:

它应该是 O(n*m),其中 m 是列表中字符串的平均长度。对于非常小的 m 值,算法将接近 O(n)

  • 为每个字符串长度构建一个计数哈希表
  • 遍历您的字符串集合,相应地更新哈希表,将当前最流行的数字存储为与哈希表分开的整数变量
  • 完成。

【讨论】:

  • 鉴于有 /(n*(n-1)/ 长度为 /n/ 的字符串的子串,我看不出你粗略描述的算法如何是 O(n) - - 您的迭代步骤至少应为 O(N^2)
  • @Ian Clelland:X 是固定的,不是吗?
  • 完全正确——我在问题描述中错过了这一点。
  • 'X' 是固定的,一次迭代 = O(n)
  • 仍然不是真正的 O(N) -- 在 N 个位置的每一个位置,您都需要查看接下来的 M 个字符(例如产生 M 个字符的散列),所以复杂度是 O(纳米)。将其视为 O(N) 仅意味着您假设 M 是如此之小以至于它的贡献可以忽略不计。
【解决方案2】:

Python 中的简单解决方案

from collections import defaultdict
from operator    import itemgetter

def naive(s, X):
    freq = defaultdict(int)
    for i in range(len(s) - X + 1):
        freq[s[i:i+X]] += 1
    return max(freq.iteritems(), key=itemgetter(1))

print naive("aoaoa", 3)
# -> ('aoa', 2)

简单的英语

  1. 创建映射:长度为X的子字符串->在s字符串中出现了多少次

    for i in range(len(s) - X + 1):
        freq[s[i:i+X]] += 1
    
  2. 在映射中找到第二项(频率)最大的一对

    max(freq.iteritems(), key=itemgetter(1))
    

【讨论】:

  • freq[s[i:i+X]] 的复杂度是 O(n)... 这是一个 O(n^2) 算法
  • 看起来像是一道作业题,所以我没有提供代码示例。
  • @qjkx: `freq[s[i:i+X]] 的复杂度是 O(X)... 这是一个 O(n*X) 算法。
【解决方案3】:

我看不到在严格的 O(n) 时间内执行此操作的简单方法,除非 X 是固定的并且可以被视为常数。如果 X 是算法的参数,那么执行此操作的最简单方法实际上是 O(n*X),因为您需要对长度为 X 的子字符串进行比较操作、字符串复制、散列等每次迭代。

(我想象一下,s 是一个多 GB 的字符串,而 X 是超过一百万的某个数字,并且没有看到任何简单的进行字符串比较或散列长度为 X 的子字符串的方法,是 O(1),并且不依赖于 X 的大小)

在扫描过程中,可以通过保留所有内容并避免重新散列整个子字符串来避免字符串复制——也许通过使用增量散列算法,您可以一次添加一个字节,然后删除最旧的字节——但我不知道有任何此类算法不会导致需要通过昂贵的后处理步骤过滤掉的大量冲突。

更新

Keith Randall 指出这种散列称为滚动散列。但是,仍然需要将每个匹配项的起始字符串位置存储在哈希表中,然后在扫描字符串后验证所有匹配项是否为真。您需要根据为每个哈希键找到的匹配数对可能包含 n-X 个条目的哈希表进行排序,并验证每个结果——在 O(n) 中可能不可行。

【讨论】:

  • Clellan 为什么我们需要重新发明散列函数。你可以在散列中使用 bulit 吗?
  • 你为什么认为会有很多冲突?在一般情况下,适当的滚动哈希不会有这个问题。
  • 为什么需要存储起始位置?你不相信你的哈希表可以保持计数吗?正确设计的哈希表,无论是开放寻址还是单独链接,都将具有恒定的时间插入。这包括重新散列。
【解决方案4】:

在 O(n) 中没有办法做到这一点。

如果你能证明我在这个问题上错了,请随意给我投反对票,但我什么都没有。

【讨论】:

    【解决方案5】:

    您可以在 O(n) 时间内使用 rolling hash 执行此操作(假设散列分布良好)。一个简单的滚动散列将是字符串中字符的异或,您可以仅使用 2 个异或从前一个子串散列增量计算它。 (有关比 xor 更好的滚动哈希,请参阅 Wikipedia 条目。)在 O(n) 时间内使用滚动哈希计算 n-x+1 个子字符串的哈希。如果没有碰撞,答案很明确——如果发生碰撞,你需要做更多的工作。想弄清楚这是否可以在 O(n) 时间内解决,我的大脑很痛苦。

    更新:

    这是一个随机 O(n) 算法。您可以通过扫描哈希表在 O(n) 时间内找到顶部哈希(保持简单,假设没有关系)。使用该散列查找一个 X 长度的字符串(在散列表中保留记录,或者只是重做滚动散列)。然后使用O(n) string searching algorithm 在 s 中查找该字符串的所有出现。如果您发现出现的次数与您在哈希表中记录的次数相同,那么您就完成了。

    如果没有,这意味着你有一个哈希冲突。选择一个新的随机散列函数,然后重试。如果您的哈希函数具有 log(n)+1 位并且是成对独立的 [Prob(h(s) == h(t)) < 1/2^{n+1} if s != t],那么 s 中最频繁的 x 长度子字符串与 s 的

    现在我们只需要一个随机成对独立滚动哈希算法。

    更新2:

    实际上,您需要 2log(n) 位哈希来避免所有(n 选择 2)冲突,因为任何冲突都可能隐藏正确答案。仍然可行,看起来hashing by general polynomial division 应该可以解决问题。

    【讨论】:

    • 这就是我想的那种哈希——谢谢你的链接。更伤脑筋:您必须假设发生了碰撞,并手动检查每个碰撞,所以现在您处于 O(n + X*(可能的碰撞次数)) 可能大于也可能不大于 O(n )
    • 我很困惑为什么不在 java 库中使用 hashmap、hashtable 或 hash 函数,因为它们已经构建好了。
    • @user2372074:因为这个标准算法(散列所有〜n个长度为X的子串)需要O(nX)时间。这比 O(n) 的算法差。
    • @Keith Randall 只需要清楚,对于标准算法,O(nX) 应该是空间复杂度吗?
    • @user2372074:不,你可以用 O(n) 个字或 O(n lg n) 位来完成。
    【解决方案6】:

    LZW 算法做到这一点

    这正是 Lempel-Ziv-Welch(用于 GIF 图像格式的 LZW)压缩算法所做的。它找到普遍的重复字节并将它们更改为简短的内容。

    LZW on Wikipedia

    【讨论】:

    • 我相信 LZW 是 O(n) 解码,但比编码慢,这是 OP 正在寻找的。​​span>
    • LZW 本身是肯定的,因为它会查找所有长度的重复子字符串。我只是说他可以使用类似的原理但寻找固定长度的字符串。
    【解决方案7】:

    这是我用 C 编写的一个版本。希望对您有所帮助。

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main(void)
    {
        char *string = NULL, *maxstring = NULL, *tmpstr = NULL, *tmpstr2 = NULL;
        unsigned int n = 0, i = 0, j = 0, matchcount = 0, maxcount = 0;
    
        string = "aoaoa";
        n = 3;
    
        for (i = 0; i <= (strlen(string) - n); i++) {
            tmpstr = (char *)malloc(n + 1);
            strncpy(tmpstr, string + i, n);
            *(tmpstr + (n + 1)) = '\0';
            for (j = 0; j <= (strlen(string) - n); j++) {
                tmpstr2 = (char *)malloc(n + 1);
                strncpy(tmpstr2, string + j, n);
                *(tmpstr2 + (n + 1)) = '\0';
                if (!strcmp(tmpstr, tmpstr2))
                    matchcount++;
            }
            if (matchcount > maxcount) {
                maxstring = tmpstr;
                maxcount = matchcount;
            }
            matchcount = 0;
        }
    
        printf("max string: \"%s\", count: %d\n", maxstring, maxcount);
    
        free(tmpstr);
        free(tmpstr2);
    
        return 0;
    }
    

    【讨论】:

      【解决方案8】:

      您可以构建子字符串树。这个想法是像电话簿一样组织您的子字符串。然后查找子字符串并将其计数加一。

      在上面的示例中,树将具有以字母开头的部分(节点):“a”和“o”。 'a' 出现 3 次, 'o' 出现两次。所以这些节点的计数分别为 3 和 2。

      接下来,在“a”节点下,将出现一个“o”的子节点,对应于子字符串“ao”。这出现了两次。在'o'节点下'a'也出现了两次。

      我们以这种方式继续,直到到达字符串的末尾。

      'abac' 的树表示可能是(同一级别的节点用逗号分隔,子节点在括号中,计数出现在冒号之后)。

      a:2(b:1(a:1(c:1())),c:1()),b:1(a:1(c:1())),c:1( )

      如果把树画出来会更明显!例如,这一切都说明字符串“aba”出现一次,或字符串“a”出现两次等。但是,存储量大大减少,更重要的是检索速度大大加快(与保留子列表相比)字符串)。

      要找出最重复的子字符串,对树进行深度优先搜索,每次到达叶节点时,记下计数,并跟踪最高的那个。

      运行时间可能有点像 O(log(n)) 不确定,但肯定比 O(n^2) 好。

      【讨论】:

      • 但是如何将所有子字符串添加到数据结构中(这似乎是一个 trie)?
      • 通过遍历特里树一次从有问题的子字符串中获取一个字母。如果在到达子字符串末尾之前到达叶节点,则开始向树中添加节点,每个字母一个节点。
      • 有问题的子字符串是什么?除非你使用某种滑动窗口
      • 您必须提取所有子字符串。从第一个字符开始,一次填写一个字母,直到到达字符串的末尾。然后移动到第二个字符,冲洗并重复。 trie 只是一种记录保存机制。
      猜你喜欢
      • 1970-01-01
      • 2011-05-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-28
      • 1970-01-01
      • 2016-06-14
      相关资源
      最近更新 更多