繁体   English   中英

在 Python 中计算字符串中的重复字符

[英]Counting repeated characters in a string in Python

我想计算每个字符在字符串中重复的次数。 除了比较来自 AZ 的字符串的每个字符并增加计数器之外,还有什么特别的方法可以做到吗?

更新(参考Anthony 的回答):到目前为止,无论您提出什么建议,我都必须写 26 次。 有更容易的方法吗?

import collections

d = collections.defaultdict(int)
for c in thestring:
    d[c] += 1

collections.defaultdict就像一个dict (实际上是它的子类),但是当一个条目被寻找但没有找到时,它不会报告它没有它,而是通过调用提供的 0 参数可调用来创建并插入它。 最流行的是defaultdict(int) ,用于计数(或等效地,制作多集 AKA 包数据结构)和defaultdict(list) ,它永远不需要使用.setdefault(akey, []).append(avalue)和类似的尴尬习语。

所以一旦你完成了这个d是一个类似 dict 的容器,将每个字符映射到它出现的次数,当然,你可以以任何你喜欢的方式发出它。 例如,最流行的字符优先:

for c in sorted(d, key=d.get, reverse=True):
  print '%s %6d' % (c, d[c])

我的第一个想法是这样做:

chars = "abcdefghijklmnopqrstuvwxyz"
check_string = "i am checking this string to see how many times each character appears"

for char in chars:
  count = check_string.count(char)
  if count > 1:
    print char, count

然而,这不是一个好主意! 这将扫描字符串 26 次,因此您可能会比其他一些答案多做 26 倍的工作。 你真的应该这样做:

count = {}
for s in check_string:
  if s in count:
    count[s] += 1
  else:
    count[s] = 1

for key in count:
  if count[key] > 1:
    print key, count[key]

这确保您只遍历字符串一次,而不是 26 次。

另外,Alex 的回答很好 - 我不熟悉集合模块。 我将来会使用它。 他的回答比我的更简洁,技术上也更胜一筹。 我建议使用他的代码而不是我的代码。

Python 2.7+ 包括collections.Counter类:

import collections
results = collections.Counter(the_string)
print(results)

盛大的性能比较

滚动到TL;DR 图表的末尾

由于我“无事可做”(理解:我只是有很多工作),我决定做一个小小的表演比赛。 我收集了最明智或最有趣的答案,并在CPython 3.5.1 中对它们进行了一些简单的timeit 我只用一个字符串测试了它们,在我的例子中这是一个典型的输入:

>>> s = 'ZDXMZKMXFDKXZFKZ'
>>> len(s)
16

请注意,不同输入的结果可能会有所不同,无论是字符串的长度不同或不同字符的数量不同,还是每个字符的平均出现次数不同。


不要重新发明轮子

Python 使我们变得简单。 collections.Counter类完全符合我们的要求,而且还有更多功能。 到目前为止,它的用法是这里提到的所有方法中最简单的。

取自@oefe ,不错的发现

>>> timeit('Counter(s)', globals=locals())
8.208566107001388

Counter走得更远,这就是为什么它需要这么长时间。

¿字典,理解?

让我们尝试使用一个简单的dict来代替。 首先,让我们使用 dict comprehension 以声明方式进行操作。

我自己想出了这个......

>>> timeit('{c: s.count(c) for c in s}', globals=locals())
4.551155784000002

这将从头到尾遍历s ,并且对于每个字符,它将计算其在s出现的次数。 由于s包含重复字符,上述方法多次搜索s以查找相同的字符。 结果自然总是一样的。 因此,让我们计算每个字符出现的次数。

我自己想出了这个, @IrshadBhat也是如此

>>> timeit('{c: s.count(c) for c in set(s)}', globals=locals())
3.1484066140001232

更好的。 但是我们仍然需要搜索字符串来计算出现次数。 每个不同的字符进行一次搜索。 这意味着我们将不止一次读取字符串。 我们可以做得更好! 但为此,我们必须摆脱我们的声明主义高马,进入一种势在必行的心态。

非凡的代码

AKA 必须抓住他们!

灵感来自@anthony

>>> timeit('''
... d = {}
... for c in s:
...   try:
...     d[c] += 1
...   except KeyError:
...     d[c] = 1
... ''', globals=locals())
3.7060273620008957

嗯,值得一试。 如果您深入研究 Python 源代码(我不能肯定地说,因为我从未真正这样做过),您可能会发现,当您执行except ExceptionType ,Python 必须检查引发的ExceptionType实际上是ExceptionType还是其他一些ExceptionType类型。 顺便说一句,让我们看看如果我们省略该检查并捕获所有异常需要多长时间。

@anthony 制作

>>> timeit('''
... d = {}
... for c in s:
...   try:
...     d[c] += 1
...   except:
...     d[c] = 1
... ''', globals=locals())
3.3506563019982423

它确实节省了一些时间,因此人们可能会想将其用作某种优化。
不要那样做! 或者实际上这样做。 现在做:

插曲 1

import time
while True:
  try:
    time.sleep(1)
  except:
    print("You're trapped in your own trap!")

你看? 除了其他事情之外,它还捕获KeyboardInterrupt 事实上,它捕获了所有的异常。 包括你可能甚至没有听说过的,比如SystemExit

插曲2

import sys
try:
  print("Goodbye. I'm going to die soon.")
  sys.exit()
except:
  print('BACK FROM THE DEAD!!!')

现在回到计数字母和数字以及其他字符。

玩追赶

例外不是要走的路。 你必须努力追上他们,当你终于赶上时,他们只是吐了你,然后扬起眉毛,好像这是你的错。 幸运的是,勇敢的人为我们铺平了道路,因此我们可以消除例外情况,至少在这个小练习中。

dict类有一个很好的方法- get -这使我们能够从字典检索项目,就像d[k] 除非键k不在字典中,否则它可以返回一个默认值。 让我们使用该方法而不是摆弄异常。

归功于@Usman

>>> timeit('''
... d = {}
... for c in s:
...   d[c] = d.get(c, 0) + 1
... ''', globals=locals())
3.2133633289995487

几乎和基于集合的字典理解一样快。 在更大的输入上,这个可能会更快。

为工作使用正确的工具

对于至少知识渊博的 Python 程序员来说,首先想到的可能是defaultdict 它和上面的版本做的事情几乎一样,除了你给它一个值工厂而不是一个值。 这可能会导致一些开销,因为必须为每个缺失的键单独“构造”该值。 让我们看看它的表现如何。

希望@AlexMartelli不会因为from collections import defaultdict而把我钉在十字架上

>>> timeit('''
... dd = defaultdict(int)
... for c in s:
...   dd[c] += 1
... ''', globals=locals())
3.3430528169992613

没有那么糟糕。 我想说的是,执行时间的增加是为提高可读性而付出的小代价。 但是,我们也看重性能,我们不会就此止步。 让我们更进一步,用零预填充字典。 这样我们就不必每次都检查该项目是否已经存在。

@sqram 致敬

>>> timeit('''
... d = dict.fromkeys(s, 0)
... for c in s:
...   d[c] += 1
... ''', globals=locals())
2.6081761489986093

那挺好的。 Counter快三倍多,但仍然足够简单。 就个人而言,这是我最喜欢的,以防您以后不想添加新角色。 即使你这样做了,你仍然可以做到。 它不如其他版本方便:

d.update({ c: 0 for c in set(other_string) - d.keys() })


实用性胜过纯度(除非它不是很实用)

现在有点不同的计数器。 @IdanK想出了一些有趣的东西。 代替使用哈希表(又名字典又名dict ),我们可以避免哈希冲突的风险和随之而来的解析开销。 我们还可以避免散列键的开销,以及额外的未占用表空间。 我们可以使用list 字符的 ASCII 值将是索引,它们的计数将是值。 正如@IdanK 所指出的那样,这个列表让我们可以恒定时间访问一个字符的数量。 我们所要做的就是使用内置函数ord将每个字符从str转换为int 这将为我们提供列表中的索引,然后我们将使用它来增加字符的计数。 所以我们要做的是:我们用零初始化列表,完成工作,然后将列表转换为dict 这个dict将只包含那些具有非零计数的字符,以使其与其他版本兼容。

作为旁注,此技术用于称为计数排序计数排序的线性时间排序算法。 它非常有效,但排序的值的范围是有限的,因为每个值都必须有自己的计数器。 要对 32 位整数序列进行排序,将需要 43 亿个计数器。

>>> timeit('''
... counts = [0 for _ in range(256)]
... for c in s:
...   counts[ord(c)] += 1
... d = {chr(i): count for i,count in enumerate(counts) if count != 0}
... ''', globals=locals())
25.438595562001865

哎哟! 不酷! 让我们试着看看当我们省略构建字典时需要多长时间。

>>> timeit('''
... counts = [0 for _ in range(256)]
... for c in s:
...   counts[ord(c)] += 1
... ''', globals=locals())
10.564866792999965

还是不好。 但是等等,什么是[0 for _ in range(256)] 我们不能写得更简单吗? [0] * 256怎么样? 那更干净。 但它会表现得更好吗?

>>> timeit('''
... counts = [0] * 256
... for c in s:
...   counts[ord(c)] += 1
... ''', globals=locals())
3.290163638001104

相当。 现在让我们把字典放回去。

>>> timeit('''
... counts = [0] * 256
... for c in s:
...   counts[ord(c)] += 1
... d = {chr(i): count for i,count in enumerate(counts) if count != 0}
... ''', globals=locals())
18.000623562998953

几乎慢了六倍。 为什么需要这么长时间? 因为当我们enumerate(counts) ,我们必须检查 256 个计数中的每一个,看看它是否为零。 但是我们已经知道哪些计数为零,哪些不是。

>>> timeit('''
... counts = [0] * 256
... for c in s:
...   counts[ord(c)] += 1
... d = {c: counts[ord(c)] for c in set(s)}
... ''', globals=locals())
5.826531438000529

它可能不会比那更好,至少对于这么小的输入不会。 此外,它仅可用于 8 位 EASCII 字符。 Облять!

最终获胜者是...

>>> timeit('''
... d = {}
... for c in s:
...   if c in d:
...     d[c] += 1
...   else:
...     d[c] = 1
... ''', globals=locals())
1.8509794599995075

是的。 即使您每次都必须检查c是否在d ,对于此输入,它也是最快的方法。 没有预先填充d会使它更快(同样,对于这个 input )。 它比Counterdefaultdict详细得多,但也更有效。


这就是所有人

这个小练习给我们上了一课:在优化时,始终衡量性能,最好是根据您的预期输入。 针对常见情况进行优化。 不要仅仅因为它的渐近复杂度较低就假设某事实际上更有效。 最后但并非最不重要的一点是,请记住可读性。 尝试在“计算机友好”和“人性化”之间找到折衷。



更新

@MartijnPieters告诉我 Python 3 中可用的函数collections._count_elements

Help on built-in function _count_elements in module _collections:

_count_elements(...)
    _count_elements(mapping, iterable) -> None

    Count elements in the iterable, updating the mappping

这个函数是用 C 实现的,所以它应该更快,但这种额外的性能是有代价的。 价格与 Python 2 甚至未来版本不兼容,因为我们使用的是私有函数。

文档

[...] 带有下划线前缀的名称(例如_spam )应被视为 API 的非公开部分(无论是函数、方法还是数据成员)。 它应被视为实施细节,如有更改,恕不另行通知。

也就是说,如果您仍然想在每次迭代中节省 620 纳秒:

>>> timeit('''
... d = {}
... _count_elements(d, s)
... ''', globals=locals())
1.229239897998923



更新 2:大字符串

我认为在一些更大的输入上重新运​​行测试可能是一个好主意,因为 16 个字符的字符串是一个很小的输入,所有可能的解决方案都相当快(在 30 毫秒内迭代 1,000 次)

我决定使用莎士比亚全集作为测试语料库,结果证明这是一个很大的挑战(因为它的大小超过 5MiB 😅)。 我只使用了它的前 100,000 个字符,我不得不将迭代次数限制在 1,000,000 到 1,000 之间。

import urllib.request
url = 'https://ocw.mit.edu/ans7870/6/6.006/s08/lecturenotes/files/t8.shakespeare.txt'
s = urllib.request.urlopen(url).read(100_000)

collections.Counter在一个小的输入上真的很慢,但桌子已经转了

Counter(s)

=> 7.63926783799991

Naïve Θ(n 2 )时间字典理解根本行不通

{c: s.count(c) for c in s}

=> 15347.603935000052s (tested on 10 iterations; adjusted for 1000)

智能Θ(n)时间字典理解工作正常

{c: s.count(c) for c in set(s)}

=> 8.882608592999986

异常笨拙而缓慢

d = {}
for c in s:
  try:
    d[c] += 1
  except KeyError:
    d[c] = 1

=> 21.26615508399982

省略异常类型检查不会节省时间(因为异常只抛出几次)

d = {}
for c in s:
  try:
    d[c] += 1
  except:
    d[c] = 1

=> 21.943328911999743

dict.get看起来不错,但运行缓慢

d = {}
for c in s:
  d[c] = d.get(c, 0) + 1

=> 28.530086210000007

collections.defaultdict也不是很快

dd = defaultdict(int)
for c in s:
  dd[c] += 1

=> 19.43012963199999

dict.fromkeys需要读取(很长的)字符串两次

d = dict.fromkeys(s, 0)
for c in s:
  d[c] += 1

=> 22.70960557699999

使用list而不是dict既不好也不快

counts = [0 for _ in range(256)]
for c in s:
  counts[ord(c)] += 1

d = {chr(i): count for i,count in enumerate(counts) if count != 0}

=> 26.535474792000002

省略对dict的最终转换无济于事

counts = [0 for _ in range(256)]
for c in s:
  counts[ord(c)] += 1

=> 26.27811567400005

您如何构造list并不重要,因为它不是瓶颈

counts = [0] * 256
for c in s:
  counts[ord(c)] += 1

=> 25.863524940000048


counts = [0] * 256
for c in s:
  counts[ord(c)] += 1

d = {chr(i): count for i,count in enumerate(counts) if count != 0}

=> 26.416733378000004

如果转换listdict “聪明”的方式,它更慢(因为你遍历字符串两次)

counts = [0] * 256
for c in s:
  counts[ord(c)] += 1

d = {c: counts[ord(c)] for c in set(s)}

=> 29.492915620000076

dict.__contains__变体对于小字符串可能很快,但对于大字符串则不然

d = {}
for c in s:
  if c in d:
    d[c] += 1
  else:
    d[c] = 1

=> 23.773295123000025

collections._count_elementscollections.Counter _count_elements内部使用_count_elements

d = {}
_count_elements(d, s)

=> 7.5814381919999505


最终判决:除非您不能或不想,否则请使用collections.Counter :)



附录: NumPy

numpy包提供了一个方法numpy.unique ,它(几乎)精确地完成了我们想要的。

这种方法的工作方式与上述所有方法都非常不同:

  • 它首先使用快速排序对输入的副本进行排序,这在最坏情况下是O(n 2 )时间操作,尽管平均为O(n log n) ,在最佳情况下为O(n)

  • 然后它在索引处创建一个包含True的“掩码”数组,其中开始运行相同的值,即。 在值与先前值不同的索引处。 重复值在掩码中产生False 示例: [5,5,5,8,9,9]生成掩码[True, False, False, True, True, False]

  • 然后,此掩码用于从以下代码中的已排序输入 ‒ unique_chars中提取唯一值。 在我们的示例中,它们将是[5, 8, 9]

  • 掩码中True值的位置被放入一个数组中,输入的长度附加在该数组的末尾。 对于上面的示例,此数组将是[0, 3, 4, 6]

  • 对于这个数组,计算其元素之间的差异,例如。 [3, 1, 2] 这些是以下代码中已排序数组 ‒ char_counts中元素的相应计数。

  • 最后,我们通过压缩unique_charschar_counts创建一个字典: {5: 3, 8: 1, 9: 2}


import numpy as np

def count_chars(s):
  # The following statement needs to be changed for different input types.
  # Our input `s` is actually of type `bytes`, so we use `np.frombuffer`.
  # For inputs of type `str`, change `np.frombuffer` to `np.fromstring`
  #  or transform the input into a `bytes` instance.
  arr = np.frombuffer(s, dtype=np.uint8)

  unique_chars, char_counts = np.unique(arr, return_counts=True)

  return dict(zip(unique_chars, char_counts))

对于测试输入( 莎士比亚全集的前 100,000 个字符),此方法的性能优于此处测试的任何其他方法。 但请注意,在不同的输入下,这种方法可能会产生比其他方法更差的性能。 输入的预排序和每个元素的重复次数是影响性能的重要因素。

count_chars(s)

=> 2.960809530000006


如果您正在考虑使用此方法,因为它的速度是collections.Counter两倍多,请考虑:

  • collections.Counter具有线性时间复杂度。 numpy.unique最好是线性,最坏是二次的。

  • 加速并不是那么显着——在长度为 100,000 的输入上每次迭代可以节省大约 3.5 毫秒。

  • 使用numpy.unique显然需要numpy

考虑到这一点,除非您需要非常快,否则使用Counter似乎是合理的。 在这种情况下,您最好知道自己在做什么,否则使用numpy最终会比没有它慢。



附录 2:有点有用的图

我在莎士比亚全集的前缀上运行了上述13种不同的方法,并制作了一个交互式情节。 请注意,在图中,前缀和持续时间均以对数刻度显示(使用的前缀长度呈指数增长)。 单击图例中的项目以在图中显示/隐藏它们。

互动图(点击!)

点击打开!

这是我在不导入额外模块的情况下能想到的最短、最实用的方法。

text = "hello cruel world. This is a sample text"
d = dict.fromkeys(text, 0)
for c in text: d[c] += 1

打印 d['a'] 将输出 2

而且它也很快。

如果有人正在寻找没有collections模块的最简单方法。 我想这会有所帮助:

>>> s = "asldaksldkalskdla"
>>> {i:s.count(i) for i in set(s)}
{'a': 4, 'd': 3, 'k': 3, 's': 3, 'l': 4}

要么

>>> [(i,s.count(i)) for i in set(s)]
[('a', 4), ('k', 3), ('s', 3), ('l', 4), ('d', 3)]

您想使用dict

#!/usr/bin/env python

input = "this is a string"

d = {}

for c in input:
    try:
        d[c] += 1
    except:
        d[c] = 1

for k in d.keys():
    print "%s: %d" % (k, d[k])
dict = {}
for i in set(str):
    b = str.count(i, 0, len(str))
    dict[i] = b
print dict

如果我的字符串是:

str = "this is string!"

上面的代码将打印:

{'!': 1, ' ': 2, 'g': 1, 'i': 3, 'h': 1, 'n': 1, 's': 3, 'r': 1, 't': 2}

如果只是计算给定字符串中给定字符的重复次数的问题,请尝试这样的操作。

 word = "babulibobablingo" letter = 'b' if letter in word: print(word.count(letter))
inputString =  input("Enter a String:")
countedArray = {}

for char in inputString:
    if char in countedArray:  
        countedArray[char] += 1    
    else:
        countedArray[char] = 1
    
print(countedArray) 

您可以使用字典:

s = "asldaksldkalskdla"
dict = {}
for letter in s:
 if letter not in dict.keys():
  dict[letter] = 1
 else:
  dict[letter] += 1

print dict

我可以用两只手数出我知道 Python 的天数,所以如果我回答一些愚蠢的问题,请原谅我 :)

我想为什么不使用列表而不是使用字典? 我不确定列表和字典是如何在 Python 中实现的,因此必须对其进行测量才能知道什么更快。

如果这是 C++,我将只使用普通的 c-array/vector 进行恒定时间访问(这肯定会更快),但我不知道 Python 中相应的数据类型是什么(如果有的话......):

count = [0 for i in range(26)]

for c in ''.join(s.lower().split()): # get rid of whitespaces and capital letters
    count[ord(c) - 97] += 1          # ord('a') == 97

也可以使列表的大小为 ord('z') 然后在任何地方都去掉 97 减法,但是如果优化,为什么不一路:)

编辑:评论者建议加入/拆分不值得使用列表的可能收益,所以我想为什么不摆脱它:

count = [0 for i in range(26)]

for c in s:
    if c.isalpha(): count[ord(c.lower()) - 97] += 1

这将显示一个带有出现次数的字符字典

str = 'aabcdefghijklmnopqrstuvwxyz'
mydict = {}
for char in str:
    mydict[char]=mydict.get(char,0)+1
 print mydict

要计算字符串中的字符,您必须使用YOUR_VARİABLE.count('WHAT_YOU_WANT_TO_COUNT')

如果需要汇总,则必须使用 count() 函数。

variable = 'turkiye'
print(variable.count('u'))

输出:1

这是解决方案..

my_list=[]
history=""
history_count=0
my_str="happppyyyy"


for letter in my_str:
    if letter in history:
        my_list.remove((history,history_count))
        history=letter
        history_count+=1

    else:
        history_count=0
        history_count+=1
        history=letter


my_list.append((history,history_count))    


print my_list
s = 'today is sunday i would like to relax'
numberOfDuplicatedChar = len(s) - len(set(s))

# set重复的元素。

下面的代码对我有用,而无需寻找任何其他 Python 库。

def count_repeated_letter(string1):
    list1=[]

    for letter in string1:
        if string1.count(letter)>=2:
            if letter not in list1:
                list1.append(letter)


    for item in list1:
        if item!= " ":
            print(item,string1.count(item))


count_repeated_letter('letter has 1 e and 2 e and 1 t and two t')

输出:

e 4
t 5
a 4
1 2
n 3
d 3

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM