【问题标题】:Finding a substring's position in a larger string在较大的字符串中查找子字符串的位置
【发布时间】:2015-09-13 21:36:00
【问题描述】:

我有一个大字符串和大量较小的子字符串,我正在尝试检查每个子字符串是否存在于较大的字符串中并获取每个子字符串的位置。

string="some large text here"
sub_strings=["some", "text"]

for each_sub_string in sub_strings:
    if each_sub_string in string:
        print each_sub_string, string.index(each_sub_string)

问题是,由于我有大量子字符串(大约一百万),因此需要大约一个小时的处理时间。有什么方法可以减少这个时间,也许是通过使用正则表达式或其他方式?

【问题讨论】:

  • 使用多个线程怎么样?
  • 虽然你做了很多额外的工作,因为在搜索一个子字符串时,你可能会找到另一个。
  • @Marged 实际上,我也有大量字符串,我正在使用 python 的多处理模块为每个字符串生成一个单独的进程。我还没有考虑为子字符串运行多个线程。
  • @RishavKundu 是的。这就是为什么我考虑使用正则表达式并将所有子字符串组合在一起的原因。有什么方法可以将它们组合在一起使用普通字符串处理进行搜索?
  • @Amith 您可能对此感兴趣en.wikipedia.org/wiki/Rabin–Karp_algorithm#Multiple_pattern_search

标签: python string algorithm


【解决方案1】:

根据子字符串长度的分布,您可以使用预处理来节省大量时间。

从集合 {23, 33, 45} 中说出子字符串的长度集合(这意味着您可能有数百万个子字符串,但每个子字符串都采用这三种长度之一)。

然后,对于每个长度,在您的大字符串中找到Rabin Window,并将结果放入该长度的字典中。也就是说,我们取 23。遍历大字符串,找到 23 个窗口的哈希值。假设位置 0 的哈希值是 13。因此,您将 13 映射到 [0] 的字典中插入 rabin23。然后你会看到位置 1 的哈希值也是 13。然后在rabin23 中,更新13 映射到[0, 1]。那么在位置 2,hash 为 4。所以在rabin23,4 映射到 [2]。

现在,给定一个子字符串,您可以计算它的 Rabin 哈希并立即检查相关字典以查找其出现的索引(然后您需要进行比较)。


顺便说一句,在许多情况下,子字符串的长度会表现出帕累托行为,即 90% 的字符串在 10% 的长度内。如果是这样,您只能对这些长度执行此操作。

【讨论】:

  • 谢谢。听起来很有希望。幸运的是,我正在使用中文字符(人名),它们通常约为 3 或 4 个字符。我会找到更多关于这个的。
【解决方案2】:

解决此问题的最佳方法是使用树实现。正如 Rishav 提到的,您在这里重复了很多工作。理想情况下,这应该实现为基于树的 FSM。想象一下下面的例子:

Large String: 'The cat sat on the mat, it was great'
Small Strings: ['cat', 'sat', 'ca']

然后想象一棵树,其中每一层都是一个额外的字母。

small_lookup = {
    'c': 
        ['a', {
            'a': ['t']
        }], {
    's':
        ['at']
    }
}

对粗略的格式表示歉意,但我认为直接映射回 python 数据结构会很有帮助。您可以构建一棵树,其中顶级条目是起始字母,它们映射到可以完成的潜在最终子字符串列表。如果你击中了一个列表元素并且没有任何其他嵌套在你击中一个叶子并且你知道你已经击中了该子字符串的第一个实例。

在内存中保存这棵树有点大,但如果你只有一百万个字符串,这应该是最有效的实现。您还应该确保在找到第一个单词实例时修剪树。

对于那些有 CS 能力的人,或者如果你想了解更多关于这种方法的信息,它是 Aho-Corasick string matching algorithm 的简化版本。

如果您有兴趣了解有关这些方法的更多信息,可以在实践中使用三种主要算法:

在某些领域,所有这些算法都将优于其他算法,但基于这样一个事实,即您要搜索的子字符串数量非常多,而且它们之间可能有很多重叠之处。我敢打赌,Aho-Corasick 会给你带来比其他两种方法更好的性能,因为它避免了O(mn) 最坏的情况

还有一个很棒的 python 库实现了Aho-Corasick 发现的here 算法,它应该可以让您避免自己编写粗略的实现细节。

【讨论】:

  • 谢谢。虽然看起来有点难以实现。但看起来有一个库可以做到这一点。
  • @Amith 我的想法,最后的库链接应该使实现过程非常简单。
【解决方案3】:

与其他答案相比,这种方法是次优的,但无论如何都可能足够好,并且易于实施。这个想法是改变算法,而不是依次针对较大的字符串测试每个子字符串,而是迭代大字符串并在每个位置测试可能匹配的子字符串,使用字典来缩小数量您需要测试的子字符串。

输出将与原始代码不同,因为它将按索引的升序排序,而不是按子字符串排序,但您可以根据需要对输出进行后处理以按子字符串排序。

创建一个字典,其中包含以每个可能的 1-3 个字符开头的子字符串列表。然后遍历字符串,并在每个字符处读取其后面的 1-3 个字符,并检查字典中以这 1-3 个字符开头的每个子字符串在该位置是否匹配:

string="some large text here"
sub_strings=["some", "text"]

# add each of the substrings to a dictionary based the first 1-3 characters
dict = {}
for s in sub_strings:
    if s[0:3] in dict:
        dict[s[0:3]].append(s)
    else:
        dict[s[0:3]] = [s];

 # iterate over the chars in string, testing words that match on first 1-3 chars
for i in range(0, len(string)):
    for j in range(1,4):
        char = string[i:i+j]
        if char in dict:        
            for word in dict[char]:
                if string[i:i+len(word)] == word:
                    print word, i

如果您不需要匹配任何长度为 1 或 2 个字符的子字符串,那么您可以摆脱 for j 循环,只需将 char 分配给 char = string[i:3]

使用第二种方法,我通过阅读 Tolstoy 的 War and Peace 并将其拆分为独特的单词来对算法进行计时,如下所示:

with open ("warandpeace.txt", "r") as textfile:
    string=textfile.read().replace('\n', '')    
sub_strings=list(set(string.split()))

对文本中的每个唯一单词进行完整搜索并输出每个单词的每个实例需要 124 秒。

【讨论】:

    猜你喜欢
    • 2017-03-27
    • 2017-03-26
    • 1970-01-01
    • 1970-01-01
    • 2012-05-21
    • 2014-10-22
    • 2012-08-03
    • 1970-01-01
    相关资源
    最近更新 更多