可以使用python字典理解来创建子字符串及其位置的字典吗?

问题描述 投票:6回答:5

给定一串字符,我想创建一个包含在字符串中的所有n字符子串的字典,其中字典键是子字符串,值是列表。列表的第一个元素是子字符串的出现次数,列表的第二个元素是这些出现的起始位置列表。

例如,使用n=3,字符串'abcdabcxdabc'导致此字典:

{'abc': [3, [0, 4, 9]], 
 'cda': [1, [2]], 
 'dab': [2, [3, 8]], 
 'bcd': [1, [1]], 
 'cxd': [1, [6]], 
 'bcx': [1, [5]], 
 'xda': [1, [7]]}

下面的代码工作并且是有效的,因为它只遍历字符串一次,但我想知道是否有更优雅和/或更pythonic方式来执行此操作,可能使用字典理解。我对python很新,并且仍在试图弄清楚何时使用理解等有意义(甚至可能)。

text = 'abcdabcxdabc'
n = 3
d = {}
for i in range(len(text) - n + 1):
    sub = text[i:i + n]
    if sub in d:
        d[sub][0] += 1
        d[sub][1].append(i)
    else:
        d[sub] = [1, [i]]
print(d)    

更新:感谢所有回复。他们通常证实我怀疑这太复杂而无法在一次理解中有效实施(但感谢火山表明如果效率不是一个问题就有可能实现)。感谢RemcoGerlich和Ignacio Vazquez-Abrams将我指向defaultdict。我将不得不深入研究这一点。我的计时结果表明,与dict相比,初始化defaultdict会有更多的开销,但是运行时间可能稍微快一点,至少对于这个例子。 (时间结果发布在下面单独的评论中。)现在我想知道是否有任何情况下dict优先于defaultdict。还有,感谢Narcolei指点我的timeit功能。

python dictionary dictionary-comprehension
5个回答
4
投票

问题是v[0]取决于长度或v[1],这意味着生成v[1]的操作必须运行两次,或者字典必须迭代以填充v[0]以替换包含的虚拟值第一次。

另一个问题是dict理解期望整个键和值立即可用,这意味着您必须运行列表推导才能获得该字符的所有索引,这意味着整个操作变为O(n2)。

我将做的唯一优化是替换d的创建,这样您就不需要检查密钥包含。

d = collections.defaultdict(lambda: [0, []])

2
投票

这是可怕的,但(我添加了偏移量,你可以从偏移列表中获得的出现次数)。是的,可以这样做

In [83]: my_str = 'abcdabcxdabc'

In [84]: n=3

In [85]: {substr: [my_str.replace(substr, ' '*n, c).index(substr) 
                   for c in xrange(my_str.count(substr))]
   ....: for substr in set(my_str[idx:idx+n] for idx in xrange(len(my_str)-n))}
Out[85]: 
{'abc': [0, 4, 9],
 'bcd': [1],
 'bcx': [5],
 'cda': [2],
 'cxd': [6],
 'dab': [3, 8],
 'xda': [7]}

1
投票

正如@Ignacio所说,任何试图解决这个问题的理解都将具有二次运行时性能,如@ volcano的回答所示。解决问题的最简洁方法是这样的:

def substrings(text, n):
    d = collections.defaultdict(list)
    for i in xrange(len(text)-n+1):
        d[text[i:i+n]].append(i)
    return d

请注意,您不需要存储子串的数量,因为您可以只使用len(d['abc'])来获取abc的出现次数。

以下是这种方法与理解的一些时间:

>>> import collections
>>> 
>>> def substrings(text, n):
>>>     d = collections.defaultdict(list)
>>>     for i in xrange(len(text)-n+1):
>>>         d[text[i:i+n]].append(i)
>>>     return d
>>> 
>>> def substrings2(text, n):
>>>     return {substr: [my_str.replace(substr, ' '*n, c).index(substr) for c in xrange(my_str.count(substr))] for substr in set(my_str[idx:idx+n] for idx in xrange(len(my_str)-n))}
>>> 
>>> text = 'abcdabcxdabc'
>>> 
>>> %timeit substrings(text, 3)
100000 loops, best of 3: 9.51 µs per loop
>>> %timeit substrings2(text, 3)
10000 loops, best of 3: 26.3 µs per loop
>>> text = text * 100
>>> %timeit substrings(text, 3)
1000 loops, best of 3: 440 µs per loop
>>> %timeit substrings2(text, 3)
100 loops, best of 3: 8.68 ms per loop

请注意,当输入大小增加100倍时,理解时间增加1000倍。


0
投票

我使用defaultdict和火山的部分理解实现了几个变体并运行了一些时序测试。

原始版本(test1):

d1 = {}
for i in range(len(text) - n + 1):
    sub = text[i:i + n]
    if sub in d1:
        d1[sub][0] += 1
        d1[sub][1].append(i)
    else:
        d1[sub] = [1, [i]]

第一个变种(test2):

d = defaultdict(lambda: [0, []])
for i in range(len(text) - n + 1):
    sub = text[i:i + n]
    d[sub][0] += 1
    d[sub][1].append(i)       

第二个变化(test3):

d = defaultdict(lambda: [0, []])
for i, sub in [(i, text[i:i + n]) for i in range (len(text) - n + 1)]:
    d[sub][0] += 1
    d[sub][1].append(i)       

第三种变化(test4):

d = {sub: [text.replace(sub, ' '*n, c).index(sub) for c in range(text.count(sub))]
     for sub in set(text[i:i + n] for i in range(len(text) - n + 1))}

以下是时序结果(显示每个循环的执行时间):

text = "abcdabcxdabc":
10000 loops, best of 3, function test1: 7.37486786334e-06
10000 loops, best of 3, function test2: 1.02725863892e-05
10000 loops, best of 3, function test3: 1.16522984082e-05
10000 loops, best of 3, function test4: 1.98546753287e-05

text = "abcdabcxdabc"*10:
10000 loops, best of 3, function test1: 7.16965834334e-05
10000 loops, best of 3, function test2: 6.8394193171e-05
10000 loops, best of 3, function test3: 7.63521044367e-05
10000 loops, best of 3, function test4: 0.00016625460554

text = "abcdabcxdabc"*100:
1000 loops, best of 3, function test1: 0.000708709217238
1000 loops, best of 3, function test2: 0.000623426932274
1000 loops, best of 3, function test3: 0.000695915822531
1000 loops, best of 3, function test4: 0.00419154787196

text = "abcdabcxdabc"*1000:
1000 loops, best of 3, function test1: 0.00700270379835
1000 loops, best of 3, function test2: 0.00615744327104
1000 loops, best of 3, function test3: 0.00712429980398
1000 loops, best of 3, function test4: 0.296075626815

原始和前两个变体似乎是O(n),而第三个更接近O(n * n)。我想我会选择第二种变体,因为它是O(n)版本中最紧凑的。


0
投票

为了记录,另一个单线:

>>> n, s = 3, 'abcdabcxdabc'
>>> L=[(s[i:i+n], i) for i in range(len(s)-n+1)]
>>> L
[('abc', 0), ('bcd', 1), ('cda', 2), ('dab', 3), ('abc', 4), ('bcx', 5), ('cxd', 6), ('xda', 7), ('dab', 8), ('abc', 9)]
>>> d={t:[i for u, i in L if u == t] for t, _ in L}
>>> d
{'abc': [0, 4, 9], 'bcd': [1], 'cda': [2], 'dab': [3, 8], 'bcx': [5], 'cxd': [6], 'xda': [7]}
>>> {k:(len(v), v) for k, v in d.items()}
{'abc': (3, [0, 4, 9]), 'bcd': (1, [1]), 'cda': (1, [2]), 'dab': (2, [3, 8]), 'bcx': (1, [5]), 'cxd': (1, [6]), 'xda': (1, [7])}

在一行中:

>>> {k:(len(v), v) for L in ([(s[i:i+n], i) for i in range(len(s)-n+1)],) for k, v in ((t, [i for u, i in L if u == t]) for t, _ in L)}
{'abc': (3, [0, 4, 9]), 'bcd': (1, [1]), 'cda': (1, [2]), 'dab': (2, [3, 8]), 'bcx': (1, [5]), 'cxd': (1, [6]), 'xda': (1, [7])}

我将如何在“现实世界”中做:

>>> def substrings(s, n):
...     d = {}
...     tis = ((s[i:i+n], i) for i in range(len(s)-n+1))
...     for t, i in tis:
...         d.setdefault(t, []).append(i)
...     return {k:(len(v), v) for k, v in d.items()}
... 
>>> substrings(s, n)
{'abc': (3, [0, 4, 9]), 'bcd': (1, [1]), 'cda': (1, [2]), 'dab': (2, [3, 8]), 'bcx': (1, [5]), 'cxd': (1, [6]), 'xda': (1, [7])}

“真实世界”版本与单线程的不同之处在于:dict是以O(n)对O(n ^ 2)构建的

© www.soinside.com 2019 - 2024. All rights reserved.