我需要一种在Python中快速计算整数中位数的方法。我目前的解决方案是:
bin(n).count("1")
但我想知道是否有更快的方法来完成这个任务?
Python 3.10 引入了 int.bit_count()
方法:
>>> n = 19
>>> bin(n)
'0b10011'
>>> n.bit_count()
3
>>> (-n).bit_count()
3
这与bin(n).count("1")
的功能等效,但速度应该会快大约6倍。请参见Issue29882。
对于任意长度的整数,bin(n).count("1")
是我在纯Python中找到的最快的方法。
我尝试了将Óscar和Adam的解决方案分别适应为64位和32位块以处理整数。两者都比bin(n).count("1")
慢至少十倍(32位版本所需的时间大约多一半)。
另一方面,gmpy的popcount()
需要的时间只有bin(n).count("1")
的1/20。因此,如果您可以安装gmpy,请使用它。
回答评论中的一个问题,对于字节,我会使用查找表。你可以在运行时生成它:
counts = bytes(bin(x).count("1") for x in range(256)) # py2: use bytearray
或者直接以文字方式定义:
counts = (b'\x00\x01\x01\x02\x01\x02\x02\x03\x01\x02\x02\x03\x02\x03\x03\x04'
b'\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05'
b'\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05'
b'\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06'
b'\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05'
b'\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06'
b'\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06'
b'\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07'
b'\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05'
b'\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06'
b'\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06'
b'\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07'
b'\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06'
b'\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07'
b'\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07'
b'\x04\x05\x05\x06\x05\x06\x06\x07\x05\x06\x06\x07\x06\x07\x07\x08')
然后就是 counts[x]
,它会得到数字x
中1位的数量,其中 0 ≤ x ≤ 255。
bin(n).count("0")
是不准确的,因为有前缀'0b'。对于那些计算"零"的情况,需要使用bin(n)[2:].count('0')
。 - the wolfnumpy
数组的许多元素上运行,则会更快。 - gerritbin(n).count("1")
。然而,只能击败60%的Python提交。@[leetcode](https://leetcode.com/submissions/detail/91593414/) - northtreehttps://docs.python.org/3.10/library/stdtypes.html#int.bit_count
计算设置位的数量。 - user5556825def CountBits(n):
n = (n & 0x5555555555555555) + ((n & 0xAAAAAAAAAAAAAAAA) >> 1)
n = (n & 0x3333333333333333) + ((n & 0xCCCCCCCCCCCCCCCC) >> 2)
n = (n & 0x0F0F0F0F0F0F0F0F) + ((n & 0xF0F0F0F0F0F0F0F0) >> 4)
n = (n & 0x00FF00FF00FF00FF) + ((n & 0xFF00FF00FF00FF00) >> 8)
n = (n & 0x0000FFFF0000FFFF) + ((n & 0xFFFF0000FFFF0000) >> 16)
n = (n & 0x00000000FFFFFFFF) + ((n & 0xFFFFFFFF00000000) >> 32) # This last & isn't strictly necessary.
return n
这适用于64位正数,但很容易扩展,并且操作数量随参数的对数增长(即与参数的位数成线性关系)。
为了理解这是如何工作的,请想象将整个64位字符串分成64个1位桶。每个桶的值等于桶中设置的位数(如果没有设置位,则为0,如果设置了一位,则为1)。第一个变换会产生类似的状态,但每个2位长的32个桶。通过适当地移动桶并添加它们的值(一个加法可以处理所有桶,因为不能跨桶进行进位- n位数字始终足够长以编码数字n),可以实现这一点。进一步的转换导致具有指数增长大小的指数减少的桶数的状态,直到我们到达一个64位长的桶。这给出了原始参数中设置的位数。
CountBits()
以处理10k位数字。但由于巨大的常量,它将变得笨重。 - Adam Zalcmannumpy
数组的情况进行向量化。 - gerrit以下是 Python 实现的种群计数算法,该算法在这篇帖子中有详细介绍:
def numberOfSetBits(i):
i = i - ((i >> 1) & 0x55555555)
i = (i & 0x33333333) + ((i >> 2) & 0x33333333)
return (((i + (i >> 4) & 0xF0F0F0F) * 0x1010101) & 0xffffffff) >> 24
对于 0 <= i < 0x100000000
,它将起作用。
bin(n).count("1")
要慢一些。 - David Weldon%timeit numberOfSetBits(23544235423)
: 1000000 次循环,3 次取最佳结果:每次循环 818 纳秒
; %timeit bitCountStr(23544235423)
: 1000000 次循环,3 次取最佳结果:每次循环 577 纳秒
. - gerritnumberOfSetBits
在 841 微秒内处理完我的 864x64 numpy.ndarray
。而使用 bitCountStr
,我必须显式循环,需要 40.7 毫秒,即几乎慢了 50 倍。 - gerrit我真的很喜欢这种方法。它简单而且非常快,但也没有位数限制,因为Python有无限的整数。
实际上,它比看起来更狡猾,因为它避免了扫描零所浪费的时间。例如,在1000000000000000000000010100000001和1111中计算设置位将花费相同的时间。
def get_bit_count(value):
n = 0
while value:
n += 1
value &= value-1
return n
bin(n).count("1")
只需0.15秒,但使用您的函数需要3.8秒。如果数字中设置的位非常少,则它将快速工作,但是如果您选择任何随机数字,则上述函数的速度将慢几个数量级。 - zidarsk8#http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetTable
POPCOUNT_TABLE16 = [0] * 2**16
for index in range(len(POPCOUNT_TABLE16)):
POPCOUNT_TABLE16[index] = (index & 1) + POPCOUNT_TABLE16[index >> 1]
def popcount32_table16(v):
return (POPCOUNT_TABLE16[ v & 0xffff] +
POPCOUNT_TABLE16[(v >> 16) & 0xffff])
range
替换为xrange
。GMP
库。它包含了许多不同架构的手写汇编实现。
gmpy
是一个用C编写的Python扩展模块,它包装了GMP库。>>> import gmpy
>>> gmpy.popcount(2**1024-1)
1024
POPCOUNT_TABLE16
中使用 array.array
,那么内存使用将更加高效,因为它将被存储为整数数组,而不是 Python int
对象的动态大小列表。 - gsnedders你可以使用算法获取整数的二进制字符串 [1],但不需要将字符串连接起来,而是计算其中 1 的数量:
def count_ones(a):
s = 0
t = {'0':0, '1':1, '2':1, '3':2, '4':1, '5':2, '6':2, '7':3}
for c in oct(a)[1:]:
s += t[c]
return s
"%o" % a
而不是oct(a)
来避免删除前两个字符。(对于建议的十六进制改进,使用%x
% a) - kindall可以将查找表与int.to_bytes
(仅适用于Python 3)结合使用:
popcount8bit = bytes([popcount(x) for x in range(1<<8)]) # use any method to initialize this lookup table
popcount = lambda x: sum(map(popcount8bit.__getitem__,
x.to_bytes((x.bit_length()+7)//8, "little")))
关于这种解决方案,不幸的是,与 Python 3 中的 bin(x).count('1')
相比慢了约20%,但在 PyPy3 上快了两倍。
这是一个基准测试脚本,在不同位数的情况下比较了这里提供的几种不同的解决方案:
from __future__ import print_function #for Python 2
import sys
from timeit import timeit
import random
def popcount(x): return bin(x).count("1")
version3=sys.version.startswith("3")
for numBit in (2, 4, 8, 16, 31, 32, 63, 64, 1000, 10000):
maximum=int((1<<numBit)-1) #int cast just in case it overflows to long in Python 2
functions=[
(popcount, "bin count"),
(lambda x: "{:b}".format(x).count("1"), "format string count"),
]
try:
import gmpy
functions.append((gmpy.popcount, "gmpy"))
except ImportError:
pass
if sys.version.startswith("3"):
exec('''functions.append((lambda x: f"{x:b}".count("1"), "f-string count"))''')
if numBit<=16:
table1=[popcount(x) for x in range(maximum+1)]
functions.append((lambda x: table1[x], "lookup list"))
functions.append((table1.__getitem__, "lookup list without lambda"))
table2="".join(map(chr, table1))
functions.append((lambda x: ord(table2[x]), "lookup str"))
if version3:
table3=bytes(table1)
functions.append((lambda x: table3[x], "lookup bytes"))
if numBit==8:
functions.append((
b'\x00\x01\x01\x02\x01\x02\x02\x03\x01\x02\x02\x03\x02\x03\x03\x04'
b'\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05'
b'\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05'
b'\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06'
b'\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05'
b'\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06'
b'\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06'
b'\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07'
b'\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05'
b'\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06'
b'\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06'
b'\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07'
b'\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06'
b'\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07'
b'\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07'
b'\x04\x05\x05\x06\x05\x06\x06\x07\x05\x06\x06\x07\x06\x07\x07\x08'
.__getitem__, "lookup bytes hard coded 8 bit"))
table_hardcoded=(
b'\x00\x01\x01\x02\x01\x02\x02\x03\x01\x02\x02\x03\x02\x03\x03\x04'
b'\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05'
b'\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05'
b'\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06'
b'\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05'
b'\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06'
b'\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06'
b'\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07'
b'\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05'
b'\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06'
b'\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06'
b'\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07'
b'\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06'
b'\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07'
b'\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07'
b'\x04\x05\x05\x06\x05\x06\x06\x07\x05\x06\x06\x07\x06\x07\x07\x08')
functions.append((
table_hardcoded.__getitem__, "lookup bytes hard coded 8 bit local variable"))
functions.append((table3.__getitem__, "lookup bytes without lambda"))
if version3:
popcount8bit=bytes([popcount(x) for x in range(1<<8)]) #bytes because benchmark says that it's fastest
functions.append((
lambda x: sum(popcount8bit[x] for x in x.to_bytes((x.bit_length()+7)//8, "big")),
"to_bytes"
))
functions.append((
lambda x: sum(map(popcount8bit.__getitem__, x.to_bytes((x.bit_length()+7)//8, "big"))),
"to_bytes without list comprehension"
))
functions.append((
lambda x: sum(map(popcount8bit.__getitem__, x.to_bytes((x.bit_length()+7)//8, "little"))),
"to_bytes little endian, without list comprehension"
))
#for x in (2, 4, 8, 16, 32, 64):
# table1=[popcount(x) for x in range(1<<8)]
print("====== numBit=", numBit)
data=[]
numRepeat=10**7//(numBit+100)
for popcountFunction, description in functions:
random.seed(10) #make randint returns the same value
data.append((
timeit(lambda: popcountFunction(random.randint(0, maximum)), number=numRepeat),
description
))
time1, name1=data[0]
assert name1=="bin count"
data.sort()
maxLength=0
for time, description in data:
maxLength=max(maxLength, len(description))
for time, description in data:
print("{:{}} -> {:2f} = {} * {:2f}".format(description, maxLength+2, time, name1, time/time1))
它可以与Python 2和3一起使用;然而,如果Python 2中没有可用的解决方案,则不予考虑。
这里没有列出所有的解决方案。
结果:
"\\n"
来获取换行符)。 - PM 2Ringceil(n/32.)
个32位整数的数组进行存储。然后,您可以以与使用整数相同(或类似)的方式使用numpy数组,包括使用它们来索引另一个数组。setup = """
import numpy as np
#Using Paolo Moretti's answer https://dev59.com/rmkw5IYBdhLWcg3wiq-s#9829855
POPCOUNT_TABLE16 = np.zeros(2**16, dtype=int) #has to be an array
for index in range(len(POPCOUNT_TABLE16)):
POPCOUNT_TABLE16[index] = (index & 1) + POPCOUNT_TABLE16[index >> 1]
def popcount32_table16(v):
return (POPCOUNT_TABLE16[ v & 0xffff] +
POPCOUNT_TABLE16[(v >> 16) & 0xffff])
def count1s(v):
return popcount32_table16(v).sum()
v1 = np.arange(1000)*1234567 #numpy array
v2 = sum(int(x)<<(32*i) for i, x in enumerate(v1)) #single int
"""
from timeit import timeit
timeit("count1s(v1)", setup=setup) #49.55184188873349
timeit("bin(v2).count('1')", setup=setup) #225.1857464598633
class popcount_lk:
""" Creates an instance for calculating the population count of
bitstring, based on a lookup table of 8 bits. """
def __init__(self):
""" Creates a large lookup table of the Hamming weight of every 8 bit integer. """
self.lookup_table = bytes.maketrans(bytes(range(1<<8)),bytes((bin(i).count('1') for i in range(1<<8))))
self.byteorder = sys.byteorder
def __call__(self,x):
""" Breaks x, which is a python integer type, into chuncks of 8 bits.
Calls the lookup table to get the population count of each chunck and returns
the aggregated population count. """
return sum(x.to_bytes((x.bit_length()>>3)+1,self.byteorder).translate(self.lookup_table))
popcount = popcount_lk
print(popcount(56437865483765))
这应该比在CPython和PyPy3中使用bin(56437865483765).count('1')
要快3倍。
bytes.maketrans(bytes(range(1<<8)),bytes((bin(i).count('1') for i in range(1<<8))))
和 bytes(bin(i).count('1') for i in range(1 << 8))
是相同的。 - Mechanic Pigbin().count('1')
更快,所以加一分。我只是指出了可以修改的部分。另外,你能把代码注释中的文本描述移到正文中吗?这里的人似乎更喜欢在正文中而不是在代码注释中看到描述(我的浏览器翻译也无法处理代码注释)。 - Mechanic Pig
int
更长,那么你使用的是什么样的表示方法?这难道没有自己的计算方法吗? - Marcinf"{n:b}"
代替bin(n)
。这样做应该更快,并且不会出现烦人的“0b”前缀。此外,您还可以执行类似f"{n:032b}"
的操作,以获得宽度为32的零填充位串。 - PM 2Ringbin
是一个C函数,比Python函数调用的开销要小。当然,在这里,0b前缀和零填充是无关紧要的,我只是提到这些事情是为了那些可能需要在其他上下文中了解它们的读者。 - PM 2Ring