Python - 字典查找每个字符的频率是不是很慢?

Posted

技术标签:

【中文标题】Python - 字典查找每个字符的频率是不是很慢?【英文标题】:Python - Is a dictionary slow to find frequency of each character?Python - 字典查找每个字符的频率是否很慢? 【发布时间】:2011-02-01 02:29:43 【问题描述】:

我正在尝试使用 O(n) 复杂度的算法来查找任何给定文本中每个符号的频率。我的算法看起来像:

s = len(text) 
P = 1.0/s 
freqs =  
for char in text: 
    try: 
       freqs[char]+=P 
    except: 
       freqs[char]=P 

但我怀疑这个字典方法是否足够快,因为它取决于字典方法的底层实现。这是最快的方法吗?

更新:如果使用集合和整数,速度不会提高。这是因为该算法已经是 O(n) 复杂度,所以不可能有本质的加速。

例如,1MB 文本的结果:

without collections:
real    0m0.695s

with collections:
real    0m0.625s

【问题讨论】:

字典操作使用散列并且是 O(1)。它怎么可能“不够快”? “足够快”是什么意思?你测量了什么?你的目标是什么? 你到底为什么要使用浮点数呢? @psihodelia Philosoraptor 曾经说过,在整数数学中使用整数。 【参考方案1】:

不,这不是最快的,因为您知道字符的范围有限,您可以使用列表和直接索引,使用字符的数字表示来存储频率。

【讨论】:

你是对的。它应该提供一些微小的进步(没有尝试/除了检查)。 是的,范围限制为 2 ** 21 种可能性。【参考方案2】:

我不熟悉python,但是对于查找频率,除非您知道频率范围(在这种情况下您可以使用数组),否则字典是要走的路。 如果您知道 unicode、ASCII 等范围内的字符,则可以定义具有正确数量值的数组。 但是,这会将其空间复杂度从 O(n) 更改为 O(可能的 n),但您将获得从 O(n*(字典提取/插入时间)) 到 O(n) 的时间复杂度改进。

【讨论】:

虽然我提出了相同的答案,但大 O 是一样的 :-)【参考方案3】:

击败dict 非常非常困难。由于 Python 中的几乎所有内容都是基于 dict 的,因此它经过了高度调整。

【讨论】:

“调整”?字典是哈希,查找是 O(1)。当您拥有一个从根本上如此快速的算法时,调整就没有多大意义了。 哈希算法本身已针对大多数内置类型进行了调整,因为密钥可以是任何可哈希类型。 @S.Lott:理论上,只有渐近性能很重要。在实践中,大 O 符号隐藏的常数因子也很重要。 @Daniel Stutsbach Philosoraptor 同意。【参考方案4】:

如何避免在循环内进行浮点操作,并在一切完成后进行?

这样,你每次都可以+1,应该会更快。

按照 S.Lott 的建议,更好地使用 collections.defaultdict。

freqs=collections.defaultdict(int)

for char in text: 
   freqs[char]+=1

或者你可能想试试,collections.Counter in python 2.7+

>>> collections.Counter("xyzabcxyz")
Counter('y': 2, 'x': 2, 'z': 2, 'a': 1, 'c': 1, 'b': 1)

或者

您可以尝试psyco,它为python 进行即时编译。 你有循环,所以我认为你会通过psyco获得一些性能提升


编辑 1:

我基于big.txt (~6.5 MB) 做了一些基准测试,peter norvig 使用 spelling corrector

Text Length: 6488666

dict.get : 11.9060001373 s
93 chars u' ': 1036511, u'$': 110, u'(': 1748, u',': 77675, u'0': 3064, u'4': 2417, u'8': 2527, u'<': 2, u'@': 8, ....

if char in dict : 9.71799993515 s
93 chars u' ': 1036511, u'$': 110, u'(': 1748, u',': 77675, u'0': 3064, u'4': 2417, u'8': 2527, u'<': 2, u'@': 8, ....

dict try/catch : 7.35899996758 s
93 chars u' ': 1036511, u'$': 110, u'(': 1748, u',': 77675, u'0': 3064, u'4': 2417, u'8': 2527, u'<': 2, u'@': 8, ....

collections.default : 7.29699993134 s
93 chars defaultdict(<type 'int'>, u' ': 1036511, u'$': 110, u'(': 1748, u',': 77675, u'0': 3064, u'4': 2417, u'8': 2527, u'<': 2, u'@': 8, ....

CPU 规格:1.6GHz Intel Mobile Atom CPU

据此,dict.get最慢collections.defaultdict最快,try/except也是最快的。


编辑 2:

添加了collections.Counter 基准,它比dict.get 慢,在我的笔记本电脑上花了 15 秒

collections.Counter : 15.3439998627 s
93 chars Counter(u' ': 1036511, u'e': 628234, u't': 444459, u'a': 395872, u'o': 382683, u'n': 362397, u'i': 348464,

【讨论】:

非常好的建议!它还应该提高数值精度。 请使用collections.defaultdict 而不是这个。 你可以使用 collections.defaultdict(int) 来代替 lambda 表达式来初始化频率 也许,如果您可以完全删除带有bare except 的示例,那就太好了。在几乎所有情况下(例如,几乎 99.99%),这是一种极其糟糕的处理方式。 @shylent:try: d[key]+=1 \n except KeyError: \n d[key] = 1 没什么问题比率约为 100/1e6。考虑到for-loop 是使用StopIteration 异常实现的,因此使用try/except 并没有那么糟糕。【参考方案5】:

为了避免除开销之外的尝试,您可以使用默认字典。

【讨论】:

【参考方案6】:

使用dict.setdefault 方法可以小幅加速,这样你就不会为每个新遇到的角色付出相当大的代价:

for char in text:
    freq[char] = freq.setdefault(char, 0.0) + P

附带说明:裸露的except: 被认为是非常糟糕的做法。

【讨论】:

请使用collections.defaultdict 而不是这个。它仍然更简单,更快。此外,问题中的浮点真的很糟糕。 setdefault 几乎是在 big.txt 文件上测试的 try/except 变体的 3 倍【参考方案7】:

如果你真的很在意速度,你可以考虑先用整数计算字符,然后通过(浮点)除法得到频率。

这里是数字:

python -mtimeit -s'x=0' 'x+=1'      
10000000 loops, best of 3: 0.0661 usec per loop

python -mtimeit -s'x=0.' 'x+=1.'
10000000 loops, best of 3: 0.0965 usec per loop

【讨论】:

【参考方案8】:

好吧,你可以用老式的风格来做......因为我们知道没有太多不同的字符并且它们是连续的,我们可以使用普通数组(或此处列出)并使用字符序数编号用于索引:

s = 1.0*len(text)
counts = [0]*256 # change this if working with unicode
for char in text: 
    freqs[ord(char)]+=1

freqs = dict((chr(i), v/s) for i,v in enumerate(counts) if v)

这可能会更快,但只是一个常数因素,两种方法应该具有相同的复杂性。

【讨论】:

基于 list 的变体比try/except 变体 2 倍(在 big.txt 上测试)。【参考方案9】:

在《爱丽丝梦游仙境》(163793 个字符)和古腾堡计划的“The Bible, Douay-Rheims Version”(5649295 个字符)上使用此代码:

from collections import defaultdict
import timeit

def countchars():
    f = open('8300-8.txt', 'rb')
    #f = open('11.txt')
    s = f.read()
    f.close()
    charDict = defaultdict(int)
    for aChar in s:
        charDict[aChar] += 1


if __name__ == '__main__':
    tm = timeit.Timer('countchars()', 'from countchars import countchars')  
    print tm.timeit(10)

我明白了:

2.27324003315 #Alice in Wonderland
74.8686217403 #Bible

两本书的字符数之比为0.029,时间之比为0.030,因此,算法为O(n),常数因子非常小。我应该认为,对于大多数(所有?)目的来说足够快。

【讨论】:

defaultdict 可能更简单的是 defaultdict(int)。不需要defaultFactory() 已修复。我不知道int() 返回0【参考方案10】:

如果数据是单字节编码,你可以使用 numpy 来加速计数过程:

import numpy as np

def char_freq(data):
    counts = np.bincount(np.frombuffer(data, dtype=np.byte))
    freqs = counts.astype(np.double) / len(data)
    return dict((chr(idx), freq) for idx, freq in enumerate(freqs) if freq > 0)

一些快速基准测试表明,这比聚合到 defaultdict(int) 快大约 10 倍。

【讨论】:

我已经发布了支持 unicode 的基于 numpy 的解决方案 ***.com/questions/2522152/… 这个答案在top answer中排名很高,票数不应该更高吗?【参考方案11】:

性能比较 h2>

注意:表格中的时间不包括加载文件所需的时间。

| approach       | american-english, |      big.txt, | time w.r.t. defaultdict |
|                |     time, seconds | time, seconds |                         |
|----------------+-------------------+---------------+-------------------------|
| Counter        |             0.451 |         3.367 |                     3.6 |
| setdefault     |             0.348 |         2.320 |                     2.5 |
| list           |             0.277 |         1.822 |                       2 |
| try/except     |             0.158 |         1.068 |                     1.2 |
| defaultdict    |             0.141 |         0.925 |                       1 |
| numpy          |             0.012 |         0.076 |                   0.082 |
| S.Mark's ext.  |             0.003 |         0.019 |                   0.021 |
| ext. in Cython |             0.001 |         0.008 |                  0.0086 |
#+TBLFM: $4=$3/@7$3;%.2g

使用的文件:'/usr/share/dict/american-english''big.txt'

比较“Counter”、“setdefault”、“list”、“try/except”、“defaultdict”、“numpy”、“cython”和@S.Mark 解决方案的脚本位于http://gist.github.com/347000

最快的解决方案是用 Cython 编写的 Python 扩展:

import cython

@cython.locals(
    chars=unicode,
    i=cython.Py_ssize_t,
    L=cython.Py_ssize_t[0x10000])
def countchars_cython(chars):
    for i in range(0x10000): # unicode code points > 0xffff are not supported
        L[i] = 0

    for c in chars:
        L[c] += 1

    return unichr(i): L[i] for i in range(0x10000) if L[i]

上一个比较: h3>
* python (dict) : 0.5  seconds
* python (list) : 0.5  (ascii) (0.2 if read whole file in memory)
* perl          : 0.5
* python (numpy): 0.07 
* c++           : 0.05
* c             : 0.008 (ascii)

输入数据:

$ tail /usr/share/dict/american-english
éclat's
élan
élan's
émigré
émigrés
épée
épées
étude
étude's
études

$ du -h /usr/share/dict/american-english
912K    /usr/share/dict/american-english

python(计数器):0.5 秒

#!/usr/bin/env python3.1
import collections, fileinput, textwrap

chars = (ch for word in fileinput.input() for ch in word.rstrip())
# faster (0.4s) but less flexible: chars = open(filename).read()
print(textwrap.fill(str(collections.Counter(chars)), width=79))

运行它:

$ time -p python3.1 count_char.py /usr/share/dict/american-english
Counter('e': 87823, 's': 86620, 'i': 66548, 'a': 62778, 'n': 56696, 'r':
56286,'t':51588,'o':48425,'l':39914,'c':30020,'d':28068,'u':25810,
“'”:24511,'g':22262,'p':20917,'m':20747,'h':18453,'b':14137,'y':
12367,'f':10049,'k':7800,'v':7573,'w':6924,'z':3088,'x':2082,'M':
1686,'C':1549,'S':1515,'q':1447,'B':1387,'j':1376,'A':1345,'P':
974,“L”:912,“H”:860,“T”:858,“G”:811,“D”:809,“R”:749,“K”:656,“E”:
618,'J':539,'N':531,'W':507,'F':502,'O':354,'I':344,'V':330,'Z':
150,“Y”:140,“é”:128,“U”:117,“Q”:63,“X”:42,“è”:29,“ö”:12,“ü”:12,
'ó': 10, 'á': 10, 'ä': 7, 'ê': 6, 'â': 6, 'ñ': 6, 'ç': 4, 'å': 3, 'û ': 3, 'í':
2,':2,'Å':1)
实数 0.44
用户 0.43
系统 0.01

perl:0.5秒 h3>
time -p perl -MData::Dumper -F'' -lanwe'$c$_++ for (@F);
END $Data::Dumper::Terse = 1; $Data::Dumper::Indent = 0; print Dumper(\%c) 
' /usr/share/dict/american-english

输出:

'S' => 1515,'K' => 656,'' => 29,'d' => 28068,'Y' => 140,'E' => 618,'y' => 12367,'g' => 22262,'e' => 87823,'' => 2,'J' => 539,'' => 241,'' => 3,'' => 6,'' = > 4,'' => 128,'D' => 809,'q' => 1447,'b' => 14137,'z' => 3088,'w' => 6924,'Q' => 63 ,'' => 10,'M' => 1686,'C' => 1549,'' => 10,'L' => 912,'X' => 42,'P' => 974,'' => 12,'\'' => 24511,'' => 6,'a' => 62778,'T' => 858,'N' => 531,'j' => 1376,'Z' = > 150,'u' => 25810,'k' => 7800,'t' => 51588,'' => 6,'W' => 507,'v' => 7573,'s' => 86620 ,'B' => 1387,'H' => 860,'c' => 30020,'' => 12,'I' => 344,'' => 3,'G' => 811,'U ' => 117,'F' => 502,'' => 2,'r' => 56286,'x' => 2082,'V' => 330,'h' => 18453,'f' = > 10049,'' => 1,'i' => 66548,'A' => 1345,'O' => 354,'n' => 56696,'m' => 20747,'l' => 39914 ,'' => 7,'p' => 20917,'R' => 749,'o' => 48425
实数 0.51
用户 0.49
系统 0.02

python(numpy):0.07秒 h3>

基于Ants Aasma's answer(修改为支持unicode):

#!/usr/bin/env python
import codecs, itertools, operator, sys
import numpy

filename = sys.argv[1] if len(sys.argv)>1 else '/usr/share/dict/american-english'

# ucs2 or ucs4 python?
dtype = 2: numpy.uint16, 4: numpy.uint32[len(buffer(u"u"))]

# count ordinals
text = codecs.open(filename, encoding='utf-8').read()
a = numpy.frombuffer(text, dtype=dtype)
counts = numpy.bincount(a)

# pretty print
counts = [(unichr(i), v) for i, v in enumerate(counts) if v]
counts.sort(key=operator.itemgetter(1))
print ' '.join('("%s" %d)' % c for c in counts  if c[0] not in ' \t\n')

输出:

("Å" 1) ("í" 2) ("ô" 2) ("å" 3) ("û" 3) ("ç" 4) ("â" 6) ("ê" 6) ("ñ" 6) ("ä" 7) ("á" 10) ("ó" 10) ("ö" 12) ("ü" 12) ("è" 29) ("X" 42) ("Q" 63) ("U" 117) ("é" 128) ("Y" 140) ("Z" 150) ("V" 330) ("I" 344) ("O" 354) ("F" 502) ("W" 507) ("N" 531) ("J" 539) ("E" 618) ("K" 656) ("R" 749) ("D" 809) ("G" 811) ("T" 858) ("H" 860) ("L" 912) ("P" 974) ("A" 1345) ("j" 1376) ("B" 1387) ("q" 1447) ("S" 1515) ("C" 1549) ("M" 1686) ("x" 2082) ("z" 3088) ("w" 6924) ("v" 7573) ("k" 7800) ("f" 10049) ("y" 12367) ("b" 14137) ("h" 18453) ("m" 20747) ("p" 20917) ("g" 22262) ("'" 24511) ("u" 25810) ("d" 28068) ("c" 30020) ("l" 39914) ("o" 48425) ("t" 51588) ("r" 56286) ("n" 56696) ("a" 62778) ("i" 66548) ("s" 86620) ("e" 87823)
real 0.07
user 0.06
sys 0.01

C ++:0.05秒 h3>
// $ g++ *.cc -lboost_program_options 
// $ ./a.out /usr/share/dict/american-english    
#include <iostream>
#include <fstream>
#include <cstdlib> // exit

#include <boost/program_options/detail/utf8_codecvt_facet.hpp>
#include <boost/tr1/unordered_map.hpp>
#include <boost/foreach.hpp>

int main(int argc, char* argv[]) 
  using namespace std;

  // open input file
  if (argc != 2) 
    cerr << "Usage: " << argv[0] << " <filename>\n";
    exit(2);
  
  wifstream f(argv[argc-1]); 

  // assume the file has utf-8 encoding
  locale utf8_locale(locale(""), 
      new boost::program_options::detail::utf8_codecvt_facet);
  f.imbue(utf8_locale); 

  // count characters frequencies
  typedef std::tr1::unordered_map<wchar_t, size_t> hashtable_t;  
  hashtable_t counts;
  for (wchar_t ch; f >> ch; )
    counts[ch]++;
  
  // print result
  wofstream of("output.utf8");
  of.imbue(utf8_locale);
  BOOST_FOREACH(hashtable_t::value_type i, counts) 
    of << "(" << i.first << " " << i.second << ") ";
  of << endl;

结果:

$ cat output.utf8 
(í 2) (O 354) (P 974) (Q 63) (R 749) (S 1,515) (ñ 6) (T 858) (U 117) (ó 10) (ô 2) (V 330 ) (W 507) (X 42) (ö 12) (Y 140) (Z 150) (û 3) (ü 12) (a 62,778) (b 14,137) (c 30,020) (d 28,068) (e 87,823) ( f 10,049) (g 22,262) (h 18,453) (i 66,548) (j 1,376) (k 7,800) (l 39,914) (m 20,747) (n 56,696) (o 48,425) (p 20,917) (q 1,447) (r 56,286 ) (s 86,620) (t 51,588) (u 25,810) (Å 1) (' 24,511) (v 7,573) (w 6,924) (x 2,082) (y 12,367) (z 3,088) (A 1,345) (B 1,387) ( C 1,549) (á 10) (â 6) (D 809) (E 618) (F 502) (ä 7) (å 3) (G 811) (H 860) (ç 4) (I 344) (J 539 ) (è 29) (K 656) (é 128) (ê 6) (L 912) (M 1,686) (N 531)

C(ASCII):0.0079秒 h3>
// $ gcc -O3 cc_ascii.c -o cc_ascii && time -p ./cc_ascii < input.txt
#include <stdio.h>

enum  N = 256 ;
size_t counts[N];

int main(void) 
  // count characters
  int ch = -1;
  while((ch = getchar()) != EOF)
    ++counts[ch];
  
  // print result
  size_t i = 0;
  for (; i < N; ++i) 
    if (counts[i])
      printf("('%c' %zu) ", (int)i, counts[i]);
  return 0;

【讨论】:

+1:哇!这是一个相当彻底的比较,有有趣的方法! @J.F.我也发布了我的 C 字符计数器扩展,你也可以在你的基准测试中包含/测试吗? ***.com/questions/2522152/… @S.Mark:我已经比较了基于“numpy”的变体和你的(我称之为“smark”)完整的程序对于“american-english”有相同的时间(90ms) .但是 profiler 显示 'smark' 比 'numpy' 快 4 倍(只有计数部分没有加载文件并将其转换为 unicode 部分)。对于 big.txt:'numpy' - 170ms,'smark' - 130ms,cc_ascii(fgets) - 30ms(如果我们忽略读取、解码文件,'numpy' 比 'smark' 慢 3 倍)。 @J.F. ,感谢您提供我的和很棒的比较图表。我认为如果您将 char_counter 和 char_list 移动到模块级变量,并在 PyMODINIT_FUNC 进行 malloc-ating(导入模块时分配一次),并使用 gcc -O3 编译,我认为它可以获得 cc_ascii 级速度。 @S.Mark:C 中的静态变量只初始化一次。看看gist.github.com/347279 把char_counterchar_list 移到全局级别是没有意义的。【参考方案12】:

我已经为 Python 编写了 Char Counter C Extension,看起来比 collections.Counter300x,比 collections.default(int) 快​​ 150x

C Char Counter : 0.0469999313354 s
93 chars u' ': 1036511, u'$': 110, u'(': 1748, u',': 77675, u'0': 3064, u'4': 2417, u'8': 2527, u'<': 2, u'@': 8,

这里是字符计数器 C 扩展代码

static PyObject *
CharCounter(PyObject *self, PyObject *args, PyObject *keywds)

    wchar_t *t1;unsigned l1=0;

    if (!PyArg_ParseTuple(args,"u#",&t1,&l1)) return NULL;

    PyObject *resultList,*itemTuple;

    for(unsigned i=0;i<=0xffff;i++)char_counter[i]=0;

    unsigned chlen=0;

    for(unsigned i=0;i<l1;i++)
        if(char_counter[t1[i]]==0)char_list[chlen++]=t1[i];
        char_counter[t1[i]]++;
    

    resultList = PyList_New(0);

    for(unsigned i=0;i<chlen;i++)
        itemTuple = PyTuple_New(2);

        PyTuple_SetItem(itemTuple, 0,PyUnicode_FromWideChar(&char_list[i],1));
        PyTuple_SetItem(itemTuple, 1,PyInt_FromLong(char_counter[char_list[i]]));

        PyList_Append(resultList, itemTuple);
        Py_DECREF(itemTuple);

    ;

    return resultList;

其中 char_counter 和 char_list 是在模块级别进行 malloc 的,所以每次函数调用时都不需要 malloc。

char_counter=(unsigned*)malloc(sizeof(unsigned)*0x10000);
char_list=(wchar_t*)malloc(sizeof(wchar_t)*0x10000);

它返回一个带有元组的列表

[(u'T', 16282), (u'h', 287323), (u'e', 628234), (u' ', 1036511), (u'P', 8946), (u'r', 303977), (u'o', 382683), ...

要转换为dict格式,只需dict()即可。

dict(CharCounter(text))

PS:Benchmark 包括了转换为 dict 的时间

CharCounter只接受Python Unicode String u"",如果文本是utf8,需要提前做.decode("utf8")

输入支持 Unicode,直到基本多语言平面 (BMP) - 0x0000 到 0xFFFF

【讨论】:

我已经发布了更新的比较(包括你的扩展)***.com/questions/2522152/… 我已经添加到用 Cython 编写的比较 Python 扩展中。它比 C 中的手写扩展快 2 倍。***.com/questions/2522152/…

以上是关于Python - 字典查找每个字符的频率是不是很慢?的主要内容,如果未能解决你的问题,请参考以下文章

与字典查找相比,Julia Valc() 似乎很慢

遍历现有键并更新字典python

python 从字典中找到出现频率高的单词

Python数据类型:字典类型及常用方法(updatedelpopkeysvaluesitemssort)

保存Python字典时,json为每个条目添加一个字符[重复]

在python 3中查找表中名字第一个字符的频率分布