如何在O(n)时间内计算0,1,2,...,n中设置的1位数?

the*_*may 12 arrays algorithm math bit-manipulation time-complexity

这是一个面试问题.原始问题问:

给定正整数N,计算从0到N的每个整数中的1的数量,并以大小为N + 1的数组返回计数.在O(n)时间做.

一个例子是:

给定7,然后返回[0,1,1,2,1,2,2,3]

当然,最简单的方法是为每个整数创建一个计数1的循环,但这将是O(kn)时间,其中k是整数的大小(以位为单位).所以要么有办法在O(1)时间内计算一个整数的1,或者有一种方法可以直接生成从0到N的计数.我确定这两种方法都存在,但无法弄清楚无论是.

tem*_*def 14

这是一个很好的小观察,你可以用它在时间O(n)做到这一点.想象一下,你想知道在数字k中设置了多少1位,并且你已经知道在数字0,1,2,...,k-1中设置了多少1位.如果你能找到一种方法为了清除在数字k中设置的任何1位,你会得到一些较小的数字(让我们称之为m),然后在k中设置的位数将等于1加上设置的位数米 因此,假设我们可以找到一种方法来清除数字k中的任何 1位,我们可以使用此模式来解决问题:

result[0] = 0  // No bits set in 0
for k = 1 to n:
    let m = k, with some 1 bit cleared
    result[k] = result[m] + 1
Run Code Online (Sandbox Code Playgroud)

有一个着名的有点蠢事

 k & (k - 1)
Run Code Online (Sandbox Code Playgroud)

通过清除在数字k中设置的最低1位形成的数字,并且在时间O(1)中进行,假设机器可以在恒定时间内进行按位运算(这通常是合理的假设).这意味着这个伪代码应该这样做:

result[0] = 0
for k = 1 to n:
    result[k] = result[k & (k - 1)] + 1
Run Code Online (Sandbox Code Playgroud)

这样O(1)每个数字O(n)的总工作时间,所以完成的总工作量是O(n).

这是一种不同的方法.例如,想象一下,您知道数字0,1,2和3中的位数.您可以通过注意数字来生成数字4,5,6和7的位数.这些数字具有按位表示形式,这些表示形式是通过取0,1,2和3的按位表示形式然后前置1.然而,如果您知道位数为0,1,2,3,4,5,如图6和7所示,你可以通过在每个较低的数字前加1位来形成它们,从而产生8,9,10,11,12,13,14和15的位数.这就产生了这种伪代码,为简单起见,假设n的形式为2 k - 1,但可以很容易地适用于一般的n:

result[0] = 0;
for (int powerOfTwo = 1; powerOfTwo < n; powerOfTwo *= 2) {
    for (int i = 0; i < powerOfTwo; i++) {
        result[powerOfTwo + i] = result[i] + 1;
    }
}
Run Code Online (Sandbox Code Playgroud)

这也在时间O(n)中运行.要看到这一点,请注意,在此处所有循环的所有迭代中,数组中的每个条目都只写入一次,完成O(1)工作以确定应该将哪个值放入该插槽的数组中.

  • 虽然这绝对是一个很好的加速,但我不确定它是否真的改变了渐近的复杂性.如果我们将整数中的位数视为常数,那么OP的朴素算法已经是O(n).如果我们不这样做(即比特数是O(log n))那么这些算法将花费O(n log n)时间,再次与OP的朴素算法相同. (2认同)
  • @henry:即使我们使用时间复杂度的位计数模型(很少有用的imho),重复递增数字的摊销成本是O(1),因为有一半的时间,只检查一位; 四分之一的时间,它是两位; 八分之一的时间的八分之一等.类似的论证适用于为系列的大多数分布的一系列数字添加一个.您可能会认为有必要进行复制,但即使这样,位数的大小也是log log n,而不是log n. (2认同)