最常见的长度为X的子串

时间:2009-10-20 20:14:39

标签: algorithm substring

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

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

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

8 个答案:

答案 0 :(得分:7)

您可以在O(n)时间内使用rolling hash执行此操作(假设散列分布良好)。一个简单的滚动哈希就是字符串中字符的xor,你可以使用2 xors从前一个子字符串哈希中逐步计算它。 (有关比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长度子串与&lt; = n其他长度x子串的碰撞的概率s最多为1/2。因此,如果发生冲突,请选择一个新的随机哈希函数并重试,在成功之前只需要一定数量的尝试。

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

UPDATE2:

实际上,你需要2log(n)比特的哈希来避免所有(n选择2)冲突,因为任何冲突都可能隐藏正确的答案。仍然可行,看起来hashing by general polynomial division应该可以做到这一点。

答案 1 :(得分:4)

我没有看到在严格的O(n)时间内执行此操作的简单方法,除非X是固定的并且可以被视为常量。如果X是算法的参数,那么执行此操作的大多数简单方法实际上是O(n * X),因为您需要在长度为X的子字符串上执行比较操作,字符串副本,哈希等。每次迭代。

(我想象,一分钟,s是一个数千兆字节的字符串,X是一个超过一百万的数字,并没有看到任何简单的方法进行字符串比较,或哈希长度为X的子串,这是O(1),而不依赖于X的大小

通过将所有内容保留在适当的位置,可以避免字符串副本,并避免重新散列整个子字符串 - 可能通过使用增量散列算法,您可以一次添加一个字节,并删除最古老的字节 - 但我不知道任何这样的算法不会导致大量的冲突需要通过昂贵的后处理步骤过滤掉。

<强>更新

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

答案 2 :(得分:1)

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

  • 为每个字符串长度构建计数哈希表
  • 迭代你的字符串集合,相应地更新哈希表,将当前最优先的数字存储为与哈希表分开的整数变量
  • 进行。

答案 3 :(得分:0)

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的子字符串 - &gt;它在s字符串中出现的次数

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

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

答案 4 :(得分:0)

LZW算法执行此操作

这正是Lempel-Ziv-Welch(LZW在GIF图像格式中使用)压缩算法所做的。它找到了流行的重复字节,并将其更改为短暂的。

LZW on Wikipedia

答案 5 :(得分:0)

这是我在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;
}

答案 6 :(得分:0)

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

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

接下来,在'a'节点下,'o'的子节点将出现,对应于子串'ao'。这出现了两次。在'o'节点'a'也会出现两次。

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

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

一:图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)好。

答案 7 :(得分:-1)

在O(n)中无法做到这一点。

如果你能在这个问题上证明我错了,请随意向我发誓,但我一无所获。