【问题标题】: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 曾经说过,在整数数学中使用整数。

标签: python algorithm probability frequency


【解决方案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 倍。

      【讨论】:

      【解决方案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 字符计数器扩展,你也可以在你的基准测试中包含/测试吗? stackoverflow.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_counter 和char_list 移到全局级别是没有意义的。
      【解决方案12】:

      我已经为 Python 编写了 Char Counter C Extension,看起来比 collections.Counter 快 300x,比 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

      【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-02-04
      • 2016-06-23
      相关资源
      最近更新 更多