如何在不转换为二进制的情况下检查汉明重量?

14

如何在不实际转换和计算的情况下获取一个数字的二进制表示中 “1”的数量

例如:

  def number_of_ones(n):
      # do something
      # I want to MAKE this FASTER (computationally less complex).
      c = 0
      while n:
        c += n%2
        n /= 2
      return c


>>> number_of_ones(5)
    2
>>> number_of_ones(4)
    1

这是 http://stackoverflow.com/questions/843737/count-bits-in-the-number-closed 的重复吗? - ChrisW
@ChrisW - Python和C是两种不同的编程语言。 - TStamper
1
这不是完全重复的。在Python中,位运算要比C语言更加耗费资源。 - Nadia Alramli
Python的整数已经是二进制的,这就是为什么谈论它们的汉明重量/ popcount是有意义的。在32位整数中计算设置位的数量指出,自Python 3.10以来,可以使用int.bit_count()函数。 - Peter Cordes
10个回答

13

我不是Python程序员,但希望这对你有所帮助。

c = 0
while n:
    c += 1
    n &= n - 1

return c

虽然有点晦涩,但它的主要优势是速度和简洁性。while循环仅在n中设置为1的位数上迭代一次。


8
您无法降低此计算的复杂度。它将是O(n)位数,或者,如使用“&”技巧所示的答案一样,O(n)位被设置为1;但是,除非您使用的所有数字都是特殊情况,否则后者平均应该是n/2,因此这两个O(n)数字是相同的。
当然,查找表技巧实际上对计算复杂度没有任何作用;它只是用时间换空间,但不改变基本经济学原理,即必须以某种方式检查每个位,并且没有办法绕过它。您不能逻辑上回答有关数字位的问题而不检查它们中的每一个。
现在,我想我有点草率,因为许多这些示例实际上是O(n^2),因为在Python中,您必须同时检查整个数字,因此对于一个Python长整数,例如100字节,+、&或/操作将至少查看每个字节一次,并且这将一遍又一遍地发生,直到数字被减少到零(在上述方案中),因此这些操作实际上是O(n^2)操作。我不确定Python是否允许在这里进行真正的O(n)解决方案。
无论如何:如果您真的询问的是计算复杂度,这特别是指大O分析,那么这就是您的答案。 :-)

6

在我看来,一个好的方法是使用查找表-创建一个将字节转换为1的数量的字典(您可以使用您发布的代码生成它,只需要运行一次),然后使用类似以下内容的东西:

def number_of_ones(n):
    sum = 0
    while n != 0:
        sum += lookup_table[n & 0xff]
        n >>= 8
    return sum

我认为这是空间和运行时间之间相当不错的权衡。

6

如果你想在一行代码中完成,你可以使用以下方法:

sum( [x&(1<<i)>0 for i in range(32)] )

4

Hamming weight是用于计算二进制数中1的个数的算法。

以下是一个时间复杂度为O(log(n))(n为位数)的解决方案:

m1  = 0x5555555555555555 #binary: 0101...
m2  = 0x3333333333333333 #binary: 00110011..
m4  = 0x0f0f0f0f0f0f0f0f #binary:  4 zeros,  4 ones ...
m8  = 0x00ff00ff00ff00ff #binary:  8 zeros,  8 ones ...
m16 = 0x0000ffff0000ffff #binary: 16 zeros, 16 ones ...
m32 = 0x00000000ffffffff #binary: 32 zeros, 32 ones
h01 = 0x0101010101010101 #the sum of 256 to the power of 0,1,2,3...

def hammingWeight(x: int) -> int:
    x -= (x>>1)&m1
    x = (x&m2) + ((x>>2)&m2)
    x = (x+(x>>4))&m4
    return ((x*h01)>>56)&0x7f

在Python 3.10中,int类型将拥有一个bit_count()方法。

1
Python的整数是任意精度的;这将计算低64位的popcount。这对于某些用例可能很好。显然,bit_count()在清晰度和性能方面都要好得多 - Peter Cordes

1
这里:
def bitCount(int_no):

    c = 0
    while(int_no):
        int_no &= (int_no - 1)
        c += 1
    return c

这可能是一种旧的、高效的方法来实现这个...最初是用C实现的(算法有一个我记不起来的名字)。它对我很有效,希望对其他人也有效。

1

如果你真的关心速度,用C语言编写代码(参见this question),并使用类似于ctypes的东西与C实现进行接口交互。


我关注的是计算复杂度,而不是实际速度或语言。 - Pratik Deoghare

0
p = lambda n:n and 1+p(n&(n-1))

这个程序使用了短路和递归,当n大于0时,它会切换到计算1+p(n&(n-1)),其中调用了p(n&(n-1))等等,当n为0时返回0。复杂度为O(n),因为它运行的次数是二进制中存在的1的数量。

例如:p(9) = 1+p(8) = 1+1+p(0) = 1+1+0=2


如果这是一个有效的答案,加上一些解释会更有价值。 - Jared Goguen

0

我今天来这里是为了检查现有技术的状态。

这是一个时常出现的话题,现代CPU有人口统计算法。这对于在某些通信应用程序中计算 Bit Error Rate 很有用。这是汉明重量,与 Hamming Distance 相关,Scipy 有一个 实现,但它使用数组而不是数字的二进制表示进行计算。对于固定大小的计算,例如 numpy 数组,我知道的最快方法是 此回答中的方法,但是所述方法,我将其称为分而治之的方法。在一般情况下,分而治之的复杂度为 O(log(n)) 而不是 O(n)。对于大多数情况,接受的答案非常好,甚至在诸如 C 这样的语言中更好,您可以简单地取((*char)bits)[i],而无需移动数字。

这里我提供了一个通用的分治算法实现,其中掩码是根据输入数字的大小动态计算的。
def dq_number_of_ones(n):
  nb = 1
  while n >> nb:
    nb *= 2
  t = (1 << nb) - 1
  masks = []
  while nb > 1:
    nb //= 2
    t ^= t >> nb
    masks.append(t)
  
  t = n
  s = 1
  for tm in masks[::-1]:
    tm = masks.pop()
    t = ((tm & t) >> s) + ((tm >> s) & t)
    s *= 2
  return t

为了完整性,这里展示原帖的方法和被接受的查找表方法

def number_of_ones(n):
    # do something
    # I want to MAKE this FASTER (computationally less complex).
    c = 0
    while n:
      c += n%2
      n //= 2
    return c
lookup_table = [number_of_ones(i) for i in range(256)]
def lt_number_of_ones(n):
    sum = 0
    while n != 0:
        sum += lookup_table[n & 0xff]
        n >>= 8
    return sum

这是两者的实际比较

import time
import matplotlib.pyplot as plt
x = []
t1 = []
t2 = []
t3 = []
for i in range(3,8000,20):
  y = i**i
  if i < 1000:
    time.sleep(0.0001)
    t0 = time.time()
    w1 = number_of_ones(y)
    t1.append(time.time() - t0)
  else:
    t1.append(np.nan)
  time.sleep(0.0001)
  t0 = time.time()
  w2 = dq_number_of_ones(y)
  t2.append(time.time() - t0)
  time.sleep(0.0001)
  t0 = time.time()
  _ = lt_number_of_ones(y)
  t3.append(time.time() - t0)
  time.sleep(0.0001)
  
  x.append(i)

plt.figure(figsize=(12, 4))
plt.subplot(121)
plt.plot(x, t1)
plt.plot(x, t2)
plt.plot(x, t3)
plt.xlim([10,None])
plt.title('Linear scale')
plt.ylabel('time to count bits in $x^x$')
plt.subplot(122)
plt.loglog(x, t1)
plt.loglog(x, t2)
plt.loglog(x, t3)
plt.xlim([10,None])
plt.title('Log scale')
plt.legend(['direct counting', 'lookup table', 'divide and conquer'])

enter image description here


固定宽度掩码(例如0x5555555555555555)的策略最好被描述为 SWAR 位操作技巧。(如在在32位整数中计算设置位的数量中所述)。我猜分而治之也有点合适,因为你要将其分成2位累加器并扩展。然而,该版本仅适用于64位整数及更窄的版本。在程序中有时使用大整数的情况下,可以检查大小是否更大。我认为与内置的bit_count()相比,这些都非常慢,希望内置函数能够使这些过时。 - Peter Cordes
1
计算 n &= n - 1 的迭代次数(通常归因于Kernighan)是另一个值得关注的算法,适用于只有少量设置位的数字。 - Peter Cordes
这个Kerninghan是谁? - Bob
1
是的,计算32位整数中设置位的数量的答案引用了http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetKernighan,该网站将其归因于《C程序设计语言第2版》第2-9练习(由K&R编写)。但是它也指出:“这个方法最初由Peter Wegner在CACM 3(1960),322中发表。”尽管如此,正如我所说,它通常被称为Kernighan的方法,可能是因为Sean Anderson对它的归属。昨天我评论时,我还没有查看过历史记录。 - Peter Cordes
不错的信息 :) 你一定是一个非常讨人厌的人!!! 嘿嘿 - Bob

0
我对Siong的“位耗尽”版本的bitCount产生了兴趣。我开始尝试着玩弄它,看看能否得出一个从左到右的变体代码。
最后,我得到的代码显然比原来的更加计算密集。另一方面,它能够捕捉到位集的索引偏移,所以我将整数收集到一个列表中。
原始代码(防爆):

def bitCount(int_no):

    if not isinstance( int_no, int) or int_no < 0:
        return None

    count = 0
    while(int_no):
        int_no &= (int_no - 1)
        count += 1
        
    return count

左右版本:
from math import floor, log

def bitsSet(int_no):

    if not isinstance( int_no, int) or int_no < 0:
        return None
    
    bit_indexes = []
    while(int_no):
        index = floor(log(int_no, 2))
        int_no &= (pow(2, index) - 1)
        bit_indexes.insert(0, index)
    
    return bit_indexes

如果你计算len(bitsSet(n)),结果会是相同的答案。走回家的路要绕远一点...

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接