在一个字符串中计算字符出现次数的最佳方法

5

你好,我正在尝试将这些Python代码改为单行,但由于字典的修改而出现了一些错误。

for i in range(len(string)):
    if string[i] in dict:
        dict[string[i]] += 1

我相信通用的语法是:

abc = [i for i in len(x) if x[i] in array]

请问有人能告诉我,假设我正在对字典中的值加1,这可能是如何实现的吗?

谢谢


1
不清楚循环之前的 dict 是什么。如果它是空的,那么没有字符会被计算。 - reclosedev
6个回答

7
你想要做的可以使用 dict生成器表达式str.count()来完成:
abc = dict((c, string.count(c)) for c in string)

使用set(string)的替代方法(根据下面 soulcheck 的评论):

abc = dict((c, string.count(c)) for c in set(string))

时间关键点

看到下面的评论,我进行了一些测试,包括此答案和其他答案。 (使用Python 3.2)

测试函数:

@time_me
def test_dict(string, iterations):
    """dict((c, string.count(c)) for c in string)"""
    for i in range(iterations):
        dict((c, string.count(c)) for c in string)

@time_me
def test_set(string, iterations):
    """dict((c, string.count(c)) for c in set(string))"""
    for i in range(iterations):
        dict((c, string.count(c)) for c in set(string))

@time_me
def test_counter(string, iterations):
    """Counter(string)"""
    for i in range(iterations):
        Counter(string)

@time_me
def test_for(string, iterations, d):
    """for loop from cha0site"""
    for i in range(iterations):
        for c in string:
            if c in d:
                d[c] += 1

@time_me
def test_default_dict(string, iterations):
    """defaultdict from joaquin"""
    for i in range(iterations):
        mydict = defaultdict(int)
        for mychar in string:
            mydict[mychar] += 1

测试执行:

d_ini = dict((c, 0) for c in string.ascii_letters)
words = ['hand', 'marvelous', 'supercalifragilisticexpialidocious']

for word in words:
    print('-- {} --'.format(word))
    test_dict(word, 100000)
    test_set(word, 100000)
    test_counter(word, 100000)
    test_for(word, 100000, d_ini)
    test_default_dict(word, 100000)
    print()

print('-- {} --'.format('Pride and Prejudcie - Chapter 3 '))

test_dict(ch, 1000)
test_set(ch, 1000)
test_counter(ch, 1000)
test_for(ch, 1000, d_ini)
test_default_dict(ch, 1000)

测试结果:

-- hand --
389.091 ms -  dict((c, string.count(c)) for c in string)
438.000 ms -  dict((c, string.count(c)) for c in set(string))
867.069 ms -  Counter(string)
100.204 ms -  for loop from cha0site
241.070 ms -  defaultdict from joaquin

-- marvelous --
654.826 ms -  dict((c, string.count(c)) for c in string)
729.153 ms -  dict((c, string.count(c)) for c in set(string))
1253.767 ms -  Counter(string)
201.406 ms -  for loop from cha0site
460.014 ms -  defaultdict from joaquin

-- supercalifragilisticexpialidocious --
1900.594 ms -  dict((c, string.count(c)) for c in string)
1104.942 ms -  dict((c, string.count(c)) for c in set(string))
2513.745 ms -  Counter(string)
703.506 ms -  for loop from cha0site
935.503 ms -  defaultdict from joaquin

# !!!: Do not compare this last result with the others because is timed
#      with 1000 iterations instead of 100000
-- Pride and Prejudcie - Chapter 3  --
155315.108 ms -  dict((c, string.count(c)) for c in string)
982.582 ms -  dict((c, string.count(c)) for c in set(string))
4371.579 ms -  Counter(string)
1609.623 ms -  for loop from cha0site
1300.643 ms -  defaultdict from joaquin

准确地说,第一种解决方案是将生成器表达式传递给dict()构造函数,第二种解决方案是字典推导式。 - soulcheck
4
@cha0site 说得没错,不过你可以轻松修改它以达到相同的运行时间:abc = dict((c, string.count(c)) for c in set(string)) - soulcheck
1
@cha0site 啊,没注意到。是的。 - soulcheck
1
@RikPoggi,它本质上将一个O(n^2)的算法变成了一个O(n*d)的算法,其中n=字符串长度,d=字母表大小。这里d是常数,所以没问题。你仍然应该优先选择reclosedev或joaquin的解决方案,具体取决于你使用的Python版本,因为它是O(n)的,而且肯定更通用。 - soulcheck
@JohnMachin:谢谢你的建议,我已经这样做了。我还发现了之前计时中的一个错误,所以它们不可靠,现在应该没问题了。抱歉 - Rik Poggi
显示剩余15条评论

7

这里需要使用collections模块:


选项1.- collections.defaultdict:

>>> from collections import defaultdict
>>> mydict = defaultdict(int)

那么你的循环变成了:
>>> for mychar in mystring: mydict[mychar] += 1

选项2.-collections.Counter(来自Felix评论):

这是一个更适用于此特定情况的替代方案,它来自于相同的collections模块:

>>> from collections import Counter

那么您只需要 (!!!):
>>> mydict = Counter(mystring)

计数器只适用于Python 2.7及以上版本。因此,对于Python < 2.7,您应该使用defaultdict。


我对你的解决方案进行了计时,它是最好的一个,也许你想看一下。 (+1) - Rik Poggi

7

Python 2.7+的替代方案:

from collections import Counter

abc = Counter('asdfdffa')
print abc
print abc['a']

输出:

Counter({'f': 3, 'a': 2, 'd': 2, 's': 1})
2

1

这不是列表理解的好选择。通常情况下,你会使用列表理解来创建列表,而在其中具有副作用(改变全局状态)并不是一个好主意。

另一方面,你的代码可能更适合像这样:

for c in string:
    if c in dict:
        dict[c] += 1

或者如果你真的想要变得更加函数化(我将dict重命名为d,因为我需要Python内置的dict函数):

d.update(dict([ (c, d[c]+1, ) for c in string ]))

注意到我在列表推导中没有改变d,而是在外部更新了d

-1 看起来很糟糕,而且不起作用。d = {}; d.update(dict([ (c, d[c]+1, ) for c in 'fubar' ])) 会产生 KeyError: 'f'。将 d[c] 更改为 d.get(c, 0) 可以避免 KeyError,但它仍然无法正常工作,因为在循环中未更新 d,所以 d[c] 永远不能大于 0,因此 所有计数都是 1 - John Machin
@John:取决于你想做什么。原始问题假定dict中有某些内容,所以我也这样做了。 - cha0site
第一个建议要求将字典初始化为所有预期字符的0 - 这不够健壮。 - John Machin

-1
>>> def count(s):
    global k
    list =[]
    for i in s:
        k=0
        if i not in list:
            list.append(i)      
            for j in range(len(s)):
                if i == s[j]:
                    k +=1

            print 'count of char {0}:{1}'.format(i,k)


>>> count('masterofalgorithm')
count of char m:2
count of char a:2
count of char s:1
count of char t:2
count of char e:1
count of char r:2
count of char o:2
count of char f:1
count of char l:1
count of char g:1
count of char i:1
count of char h:1
>>> 

-1
你原来的循环不符合Python的风格。如果你只想遍历字符串中的每个字母,那就没必要通过range(len(string))进行迭代。请改为以下方式:
for c in my_string:
    if c in my_dict:
        my_dict[c] += 1

2
你忘记了在结尾处添加 else: my_dict[c] = 1 或者在开头设置 defaultdict。所有的计数都将为零。 - John Machin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接