【问题标题】:Fastest method to generate big random string with lower Latin letters生成具有较低拉丁字母的大随机字符串的最快方法
【发布时间】:2013-04-24 21:50:52
【问题描述】:

我正在尝试解决 Timus Online Judge 的 this 问题。要解决这个问题,您需要生成一个由 1 000 000 个小写拉丁字母组成的序列,并在 1 秒内将其写入标准输入。

用 C++ 或 Java 很容易解决这个问题。我这里有python解决方案:

import os
from random import randint

s = ''.join(chr(97 + randint(0, 25)) for i in range(1000000))
os.write(1, bytes(s, 'utf8'))

需要 1.7 秒:

$ time python3.3 1219.py > /dev/null

real    0m1.756s
user    0m1.744s
sys     0m0.008s

我得到了“超出时间限制”的结果。所以问题是“如何更快地做到这一点?”

UPD1: 使用randint(97, 122) 将时间缩短到 16 毫秒。现在是 1.740s

UPD2: @Martijn Pieters 的解决方案需要 0.979 秒,但它也没有通过测试。

UPD3 Martijn Pieters 提出了一个很好的解决方案,但是还是很慢:

from sys import stdin
from random import choice
from string import ascii_lowercase

s = ''.join([choice(ascii_lowercase) for _ in range(1000000)])
stdout.write(s) 

耗时 0.924 秒

from sys import stdout
from random import choice
from string import ascii_lowercase

for _ in range(1000000):
    stdout.write(choice(ascii_lowercase))

耗时 1.173 秒

from sys import stdout
from random import choice
from string import ascii_lowercase
bal = [c.encode('ascii') for c in ascii_lowercase]
out = stdout.buffer

for _ in range(1000000):
    out.write(choice(bal))

耗时 1.155 秒

from sys import stdout
from random import choice
from string import ascii_lowercase

bal = [c.encode('ascii') for c in ascii_lowercase]
stdout.buffer.write(b''.join([choice(bal) for _ in range(1000000)]))

耗时 0.901 秒

UPD4

一些人 just solved Timus 的问题。我希望他能分享他的解决方案:)

UPD5 感谢 Ashwini Chaudhary 与我们分享他的 Python 2.x 解决方案:

from random import choice
from string import ascii_lowercase
lis=list(ascii_lowercase)
print ''.join(choice(lis) for _ in xrange(1000000)) 

在我的电脑上需要 0.527s,它通过了 Timus 的测试。但是 Python3.x 的问题仍然存在。

UPD6 感谢Markku K.这段代码:

import os
from random import random
from string import ascii_lowercase

bal = [c.encode('ascii') for c in ascii_lowercase]
os.write(1, b''.join([bal[int(random() * 26)] for _ in range(1000000)]))

耗时 0.445s,但仍未通过测试

【问题讨论】:

  • 使用列表理解而不是生成器表达式。使用join 有时可以节省一点。 (并且join 无论如何都会将您的生成器转换为列表或元组)。
  • randint(97, 122) 可能比97 + randint(0, 25) 节省一点时间。在 Python 中,即使是加法也不便宜,因为它涉及类型检查。
  • 随机选择一个字母,写一百万次。 :) 它是 1,000,000 个随机字母。
  • 我已经分享过了。 :)
  • 一定是random() * 26,这是迄今为止最快的,在0.64秒内执行。(我已经更新了我的解决方案)。 py3x 仍然没有运气。 :(

标签: python performance python-3.x random stdin


【解决方案1】:

生成和写入大小为 2 的较大幂的块。

也许使用由 26 个小写字母组成的字符串或数组,然后随机选择 then 而不是生成字符。

【讨论】:

    【解决方案2】:

    尝试将其中的一部分转换为 C++ 或其他编译语言。这几乎可以保证让它更快。不幸的是,Python 并不太快,尤其是在涉及到这样的事情时。试试 C++、C 或 Pascal

    编辑:另见Python Performance Tips

    【讨论】:

    • 我可以用 C++ 做到这一点。我想知道:有没有办法用 Python 做到这一点?
    • @ilalex:见Python Performance Tips
    • fastest solution 正是这样做的:将尽可能多的功能从显式的解释代码转移到 Python 的内置函数和标准库中 - 使用 bytearray,它的功能是在内部调用任意构造函数(再次传递内置函数)和maketrans。 J.F. 的诀窍是不要在 Python 中编写单个循环,而是在 Python 内部使用 C 编写。
    【解决方案3】:

    使用string.ascii_lowercase 代替chr 生成小写字符:

    from sys import stdin
    from random import choice
    from string import ascii_lowercase
    
    s = ''.join([choice(ascii_lowercase) for _ in range(1000000)])
    stdout.write(s)
    

    另外直接写stdout似乎更快,用python编码自己并不比在C代码中处理更快。

    我也使用列表推导; str.join() 需要扫描输入序列两次,一次确定输出的长度,一次实际将输入元素复制到输出字符串。列表推导然后击败较慢的生成器到列表代码。

    仅使用choice(ascii_lowercase) 而不是从整数生成每个字符的方法快两倍多:

    >>> timeit.timeit('f()', 'from __main__ import yours as f', number=3)
    11.299837955011753
    >>> timeit.timeit('f()', 'from __main__ import mine as f', number=3)
    5.330044150992762
    

    您可以尝试通过将单个字符直接写入stdout 来避免''.join() 开销:

    from sys import stdout
    from random import choice
    from string import ascii_lowercase
    
    for _ in range(1000000):
        stdout.write(choice(ascii_lowercase))
    

    接下来要尝试的是写入原始字节:

    from sys import stdout
    from random import choice
    from string import ascii_lowercase
    bal = [c.encode('ascii') for c in ascii_lowercase]
    out = stdout.buffer
    
    for _ in range(1000000):
        out.write(choice(bal))
    

    但在我的测试中,这些都比 ''.join() 没有任何改进。

    接下来我们将 ASCII 字符编码为字节一次,然后使用bytes.join()

    from sys import stdout
    from random import choice
    from string import ascii_lowercase
    
    bal = [c.encode('ascii') for c in ascii_lowercase]
    stdout.buffer.write(b''.join([choice(bal) for _ in range(1000000)]))
    

    bal 是编码为字节的小写 ASCII 字符列表,我们从中随机挑选 100 万个项目,将它们连接成一个大字节字符串,然后将其一次性写入二进制标准输出缓冲区。

    字节连接和字符串版本一样“慢”:

    >>> timeit.timeit('f()', 'from __main__ import bytes as f', number=3)
    5.41390264898655
    

    但我们编码 26 个字符,而不是 100 万个,因此写入阶段更快。

    【讨论】:

    • 如果我需要在标准输出中写入字符串,如何使用列表理解?
    • @ilalex: stdout 你的意思是?它根据输出编码将 unicode 字符串编码为字节。在这种情况下,您只生成 ASCII,所以没关系。
    • 我正在尝试以这种方式使用列表理解: for a in [choice(ascii_lowercase) for _ in range(1000000)]: stdout.write(a) 但它需要的时间比你的多上面的代码。
    • @ilalex:改用for _ in range(1000000): stdout.write(choice(ascii_lowercase))
    • 避免 ''.join() 比较慢。
    【解决方案4】:

    我刚刚被接受的解决方案(python 2.7,执行时间:0.984):

    from random import choice
    from string import ascii_lowercase
    
    lis = list(ascii_lowercase)
    print ''.join(choice(lis) for _ in xrange(1000000)) 
    

    访问列表元素比访问字符串更快。

    In [13]: from random import choice
    
    In [14]: from string import ascii_lowercase
    
    In [15]: lis = list(ascii_lowercase)
    
    In [16]: %timeit ''.join(choice(lis) for _ in xrange(10**5))
    1 loops, best of 3: 128 ms per loop
    
    In [17]: %timeit ''.join(choice(ascii_lowercase) for _ in xrange(10**5))
    1 loops, best of 3: 134 ms per loop
    

    而且你不需要stdoutstdin 在这里,因为大多数在线评判我们这样的东西来测试你的脚本:

    $python script.py <in.txt >out.txt
    

    因此您可以使用print 代替stdoutraw_input() 代替stdin,尽管对于大量输入stdin.readlineraw_input() 更快。

    更新 1

    使用@Markku 的tip 执行时间在py2.7 中减少到0.64:

    from random import random
    from string import ascii_lowercase
    
    lis = list(ascii_lowercase)
    print "".join( [lis[int(random() * 26)] for _ in xrange(1000000)] )
    

    【讨论】:

    • 我不明白为什么这在 Python 3 中不起作用,除了 print 语句。
    【解决方案5】:

    通过在原始解决方案中将 randint(0,25) 更改为 int(random()*25),我获得了巨大的速度提升。在我的机器上,时间从大约 2 秒变为大约 0.6 秒。如果您查看 random.py 代码,您会发现 randint 充满了您不想要或不需要的检查。

    更新:糟糕,减一。你需要 int(random()*26)。谢谢Ashwini

    【讨论】:

      【解决方案6】:

      这是 Python 3 代码,它在 0.28 秒内生成 1000000 个“随机”小写字母(另请参阅末尾的 0.11-seconds 解决方案;@Ashwini Chaudhary 的问题代码在我的机器上占用了 0.55 秒,@ Markku K. 的代码 -- 0.53):

      #!/usr/bin/env python3
      import os
      import sys
      
      def write_random_lowercase(n):
          min_lc = ord(b'a')
          len_lc = 26
          ba = bytearray(os.urandom(n))
          for i, b in enumerate(ba):
              ba[i] = min_lc + b % len_lc # convert 0..255 to 97..122
          sys.stdout.buffer.write(ba)
      
      write_random_lowercase(1000000)
      

      % len_lc 歪曲了分布(如何修复它见最后)虽然它仍然满足条件(ascii、小写、1、2、3 字母序列的频率):

      $ python3 generate-random.py | python3 check-seq.py
      

      check-seq.py:

      #!/usr/bin/env python3
      import sys
      from collections import Counter
      from string import ascii_lowercase
      
      def main():
          limits = [40000, 2000, 100]
      
          s = sys.stdin.buffer.readline() # a single line
          assert 1000000 <= len(s) <= 1000002 # check length +/- newline
          s.decode('ascii','strict') # check ascii
          assert set(s) == set(ascii_lowercase.encode('ascii')) # check lowercase
      
          for n, lim in enumerate(limits, start=1):
              freq = Counter(tuple(s[i:i+n]) for i in range(len(s)))
              assert max(freq.values()) <= lim, freq
      
      main()
      

      注意:在 acm.timus.ru generate-random.py 上给出“超出输出限制”。

      为了提高性能,您可以使用bytes.translate() method0.11 秒):

      #!/usr/bin/env python3
      import os
      import sys
      
      # make translation table from 0..255 to 97..122
      tbl = bytes.maketrans(bytearray(range(256)),
                            bytearray([ord(b'a') + b % 26 for b in range(256)]))
      # generate random bytes and translate them to lowercase ascii
      sys.stdout.buffer.write(os.urandom(1000000).translate(tbl))
      

      如何修复% len_lc 歪斜

      256(字节数)不能被 26(小拉丁字母的数量)整除,因此公式 min_lc + b % len_lc 使某些值出现的频率低于其他值,例如:

      #!/usr/bin/env python3
      """Find out skew: x = 97 + y % 26 where y is uniform from [0, 256) range."""
      from collections import Counter, defaultdict
      
      def find_skew(random_bytes):
          char2freq = Counter(chr(ord(b'a') + b % 26) for b in random_bytes)
          freq2char = defaultdict(set)
          for char, freq in char2freq.items():
              freq2char[freq].add(char)
          return {f: ''.join(sorted(c)) for f, c in freq2char.items()}
      
      print(find_skew(range(256)))
      # -> {9: 'wxyz', 10: 'abcdefghijklmnopqrstuv'}
      

      在这里,输入 range(256) 是均匀分布的(每个字节只出现一次),但输出中的 'wxyz' 字母比其他 910 出现的频率要低。要修复它,可以删除未对齐的字节:

      print(find_skew(range(256 - (256 % 26))))
      # -> {9: 'abcdefghijklmnopqrstuvwxyz'}
      

      这里,输入是均匀分布在[0, 234)范围内的字节,输出是均匀分布的ascii小写字母。

      bytes.translate() 接受第二个参数来指定要删除的字节:

      #!/usr/bin/env python3
      import os
      import sys
      
      nbytes = 256
      nletters = 26
      naligned = nbytes - (nbytes % nletters)
      tbl = bytes.maketrans(bytearray(range(naligned)),
                            bytearray([ord(b'a') + b % nletters
                                       for b in range(naligned)]))
      bytes2delete = bytearray(range(naligned, nbytes))
      R = lambda n: os.urandom(n).translate(tbl, bytes2delete)
      
      def write_random_ascii_lowercase_letters(write, n):
          """*write* *n* random ascii lowercase letters."""    
          while n > 0:
              # R(n) expected to drop `(nbytes - nletters) / nbytes` bytes
              # to compensate, increase the initial size        
              n -= write(memoryview(R(n * nbytes // naligned + 1))[:n])
      
      write = sys.stdout.buffer.write
      write_random_ascii_lowercase_letters(write, 1000000)
      

      如果随机生成器(此处为os.urandom)生成超出对齐范围(&gt;=234)的长字节序列,则while 循环可能会执行多次。

      如果使用random.getrandbits(8*n).to_bytes(n, 'big') 而不是os.urandom(n),则时间性能可以提高另一个数量级。前者使用 Mersenne Twister 作为核心生成器,可能比使用操作系统提供的源的os.urandom() 更快。如果您使用随机字符串作为秘密,后者会更安全。

      【讨论】:

      • 我给他们写了一封关于这个问题的信,他们已经确认他们在 Python 中定义了 OLE 存在错误。
      • 不是批评,只是一般性评论:任何解释语言的快速技巧是将尽可能多的控制逻辑移动到内置函数中,而不是使用解释代码。请注意 JF 最快的解决方案如何在时序关键代码中没有在 Python 中实现单个循环:os.urandom 进行内存分配和随机数生成,str.translate 迭代数字,将它们转录为所需的输出格式(拉丁文小写字符)。最终结果类似于 kirbyfan64sos 提出的建议:用 C 编写代码。我想说:了解你的标准库! :-)
      • @cfi:确实,在 Python 代码中对许多字节的按位操作在 CPython 上比在 C 上要慢得多(x100-200 倍)。注意:Pypy、Jython、IronPython 中的快速实现可能看起来不同。
      【解决方案7】:

      使用random.choices?

      在 Python 3.6 上:

      随机导入 导入字符串 %timeit ''.join(random.choices(string.ascii_lowercase, k=10**6)) 1 个循环,最好的 3 个:每个循环 235 毫秒

      【讨论】:

        【解决方案8】:

        执行时间0.51s

        from sys import stdout
        from string import ascii_lowercase
        l = 1000000
        q = ['a']*l
        lc = list(ascii_lowercase)
        c = 0
        for i in range(0,l-2,3):
            j = i // 3
            j_26 = j // 26
            q[i]= lc[j_26 // 26 % 26]
            q[i+1] = lc[j_26 % 26]
            q[i+2] = lc[j % 26]
        
        stdout.write(''.join(q))
        

        【讨论】:

          【解决方案9】:

          也许:

          import _random
          
          x = _random.Random()
          for y in range( 1000000 ): 
            a = x.random()
          

          【讨论】:

          • 其他答案返回 am 对象或输出数据的文件,并显示运行代码所花费的时间
          猜你喜欢
          • 2012-12-29
          • 2016-09-23
          • 1970-01-01
          • 1970-01-01
          • 2018-07-03
          • 2011-09-23
          • 2015-07-17
          • 2018-11-09
          相关资源
          最近更新 更多