这是一个面试问题。最初的问题是:
给定正整数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'我确定这两种方法都存在,但也无法解决。
答案 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