【问题标题】:Find substrings in a set of strings在一组字符串中查找子字符串
【发布时间】:2017-01-19 07:48:24
【问题描述】:

我有一大组 (50k-100k) 字符串 mystringsmystrings 中的某些字符串可能是其他字符串的确切子字符串,我想折叠这些字符串(丢弃子字符串并只保留最长的字符串)。现在我正在使用一种简单的方法,它具有O(N^2) 的复杂性。

unique_strings = set()
for s in sorted(mystrings, key=len, reverse=True):
    keep = True
    for us in unique_strings:
        if s in us:
            keep = False
            break
    if keep:
        unique_strings.add(s)

哪些数据结构或算法可以使这项任务更容易,并且不需要O(N^2) 操作。库还可以,但我需要保持纯 Python。

【问题讨论】:

  • 更多 Pythonic,丢弃 keep 布尔值并在 for 循环上使用 else 子句(当然不会改变时间复杂度)python-notes.curiousefficiency.org/en/latest/python_concepts/…
  • @Chris_Rands 你能演示一下吗?一旦找到匹配项,就没有理由继续迭代内部 for 循环,因此中断。但是,一旦我们退出内部循环,我们不知道我们是否因为找到匹配项而中断,或者我们是否刚刚完成迭代。也许我遗漏了一些东西,但我认为这是实现这种(诚然天真的)方法的最简洁和高效的方式。
  • 您保留break,只需将if keep: 替换为else:(相同的缩进)并删除所有带有keep 的行。 else 子句仅在break 不发生时执行。如果您不熟悉 for-else 构造,请阅读我上面链接的文章
  • 酷!关键字太常见了,我忽略了循环的语义含义。
  • 其实再看一遍,你可以用any()或者all()代替,比如if not any(s in us for us in unique_strings): unique_strings.add(s)会像break一样短路

标签: python string set substring


【解决方案1】:

在 set() 中查找子字符串:

name = set()
name.add('Victoria Stuart')                         ## add single element
name.update(('Carmine Wilson', 'Jazz', 'Georgio'))  ## add multiple elements
name
{'Jazz', 'Georgio', 'Carmine Wilson', 'Victoria Stuart'}

me = 'Victoria'
if str(name).find(me):
    print('{} in {}'.format(me, name))
# Victoria in {'Jazz', 'Georgio', 'Carmine Wilson', 'Victoria Stuart'}

这很容易——但如果你想返回匹配的字符串,那就有点问题了:

for item in name:
    if item.find(me):
            print(item)
'''
Jazz
Georgio
Carmine Wilson
'''

print(str(name).find(me))
# 39    ## character offset for match (i.e., not a string)

如你所见,上面的循环只执行直到条件为True,在打印我们想要的项目(匹配字符串)之前终止。

使用regex(正则表达式)可能更好,更容易:

import re

for item in name:
    if re.match(me, item):
            full_name = item
            print(item)
# Victoria Stuart
print(full_name)
# Victoria Stuart

for item in name:
    if re.search(me, item):
            print(item)
# Victoria Stuart

来自Python docs

search() 与 match()

Python 提供了两种不同的基于常规的原始操作 表达式:re.match() 仅在开头检查匹配 字符串,而 re.search() 检查 字符串...

【讨论】:

    【解决方案2】:

    一种天真的方法:

    1. sort strings by length, longest first  # `O(N*log_N)`
    2. foreach string:  # O(N)
        3. insert each suffix into tree structure: first letter -> root, and so on.  
           # O(L) or O(L^2) depending on string slice implementation, L: string length
        4. if inserting the entire string (the longest suffix) creates a new 
           leaf node, keep it!
    
    O[N*(log_N + L)]  or  O[N*(log_N + L^2)]
    

    这可能远非最佳,但对于较大的N(字符串数量)和较小的L(平均字符串长度)应该明显优于O(N^2)

    您还可以按长度降序遍历字符串,并将每个字符串的所有子字符串添加到一个集合中,并且只保留那些不在集合中的字符串。算法大 O 应该与上述最坏情况 (O[N*(log_N + L^2)]) 相同,但实现要简单得多:

    seen_strings, keep_strings = set(), set()
    for s in sorted(mystrings, key=len, reverse=True):
        if s not in seen_strings:
            keep_strings.add(s)
            l = len(s)
            for start in range(0, l-1):
                for end in range(start+1, l):
                    seen_strings.add(s[start:end])
    

    【讨论】:

      【解决方案3】:

      与此同时,我想出了这种方法。

      from Bio.trie import trie
      unique_strings = set()
      suffix_tree = trie()
      for s in sorted(mystrings, key=len, reverse=True):
          if suffix_tree.with_prefix(contig) == []:
              unique_strings.add(s)
              for i in range(len(s)):
                  suffix_tree[s[i:]] = 1
      

      优点:对于我正在使用的数据集而言,≈15 分钟 --> ≈20 秒。 坏处:引入biopython 作为依赖项,它既不是轻量级也不是纯python(正如我最初问的那样)。

      【讨论】:

        【解决方案4】:

        您可以对字符串进行预排序并创建一个字典,将字符串映射到排序列表中的位置。然后您可以遍历字符串列表 (O(N)) 和后缀 (O(L)) 并将这些条目设置为位置字典中存在的None (O(1) 字典查找和 O(1)列表更新)。所以总的来说,这具有 O(N*L) 复杂度,其中 L 是平均字符串长度。

        strings = sorted(mystrings, key=len, reverse=True)
        index_map = {s: i for i, s in enumerate(strings)}
        unique = set()
        for i, s in enumerate(strings):
            if s is None:
                continue
            unique.add(s)
            for k in range(1, len(s)):
                try:
                    index = index_map[s[k:]]
                except KeyError:
                    pass
                else:
                    if strings[index] is None:
                        break
                    strings[index] = None
        

        对以下示例数据进行测试得出的加速因子约为 21:

        import random
        from string import ascii_lowercase
        
        mystrings = [''.join(random.choices(ascii_lowercase, k=random.randint(1, 10)))
                     for __ in range(1000)]
        mystrings = set(mystrings)
        

        【讨论】:

          猜你喜欢
          • 2020-05-05
          • 2011-07-13
          • 2022-06-17
          • 2015-12-31
          • 2021-08-13
          • 2015-10-28
          • 2017-12-04
          相关资源
          最近更新 更多