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

时间:2017-03-24 19:21:57

标签: 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的计数.I'我确定这两种方法都存在,但也无法解决。

3 个答案:

答案 0 :(得分: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

有一个着名的小伎俩技巧

 k & (k - 1)

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

result[0] = 0
for k = 1 to n:
    result[k] = result[k & (k - 1)] + 1

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;
    }
}

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

答案 1 :(得分:3)

我们首先手动评估一些小数字的位数。我们注意到n的位数与先前结果之间的递归关系:

 n: | count(n): | recurrence:
==============================
  0 |        0 |
  1 |        1 |
------------------------------
 10 |        1 | =  count(0) + 1
 11 |        2 | =  count(1) + 1
------------------------------
100 |        1 | =  count(0) + 1
101 |        2 | =  count(1) + 1
110 |        2 | = count(10) + 1
111 |        3 | = count(11) + 1
...

鉴于所有位数最多为2 = 2 1,我们可以通过加1来计算最多4位= 2位的位数。鉴于位数最多为4 = 2 2,我们可以计算位数最多8 = 2 3添加1.我们generalize this to the k-th power of two并且可以提出以下示例性实现:

// Counts and returns number of enabled bits for integers 0 - n:
function count_bits(n) {
  let count = new Array(n);
  count[0] = 0;
  for (let i = 1, j = 0, k = 1; i <= n; ++i, ++j) {
    if (i == 2**k) { // k-th bit of i has been enabled
      k++;
      j = 0;
    }
    count[i] = count[j] + 1;
  }
  return count;
}

// Example:
console.log(count_bits(17).join());

我们注意到所涉及的所有操作都是递增1,随机数组访问,复制数组元素并通过i检查循环增量i == 2**k的第k位,这可以重写为{{1}或者 - 对于i & 1 << k的任意精度 - 作为随机数组访问。

假设上面列出的所有基本操作都在我们机器上的i中,那么总的运行时复杂度在O(1)

这同样适用于任意精度整数 - 其中递增的平均运行时复杂度为O(n) - 除非复制O(1)花费的时间超过常量。不幸的是,对于任意大整数的情况,我们的运行时复杂度在count[i] = count[j] + 1,因为我们需要O(n log(log(n)))空间来存储n的位数。

答案 2 :(得分:3)

更简单的回答:

设x为长度为k + 1的数组,x [i]具有i中的设定位数。

x[0] = 0
for i=1:k
    x[i] = x[i>>1] + i&1