给定一个非负整数 ,请计算 到 之间的每个数字的二进制表示中 的个数,并输出一个数组。
示例 1:
输入: n = 2
输出: [0,1,1]
解释:
0 --> 0
1 --> 1
2 --> 10
示例 2:
输入: n = 5
输出: [0,1,1,2,1,2]
解释:
0 --> 0
1 --> 1
2 --> 10
3 --> 11
4 --> 100
5 --> 101
说明 :
进阶:
__builtin_popcount
)来执行此操作。这道题需要计算从 到 的每个整数的二进制表示中的 的数目。
部分编程语言有相应的内置函数用于计算给定的整数的二进制表示中的 的数目,例如 Java 的 Integer.bitCount
,C++ 的 __builtin_popcount
,Go 的 bits.OnesCount
等,读者可以自行尝试。下列各种方法均为不使用内置函数的解法。
为了表述简洁,下文用「一比特数」表示二进制表示中的 的数目。
最直观的做法是对从 到 的每个整数直接计算「一比特数」。每个 int
型的数都可以用 32 位二进制数表示,只要遍历其二进制表示的每一位即可得到 的数目。
利用 Brian Kernighan 算法,可以在一定程度上进一步提升计算速度。Brian Kernighan 算法的原理是:对于任意整数 ,令 ,该运算将 的二进制表示的最后一个 变成 。因此,对 重复该操作,直到 变成 ,则操作次数即为 的「一比特数」。
对于给定的 ,计算从 到 的每个整数的「一比特数」的时间都不会超过 ,因此总时间复杂度为 。
function countBits(n) {
const bits = new Array(n + 1).fill(0)
for (let i = 0; i <= n; i++) {
bits[i] = countOnes(i)
}
return bits
}
function countOnes(x) {
let ones = 0
while (x > 0) {
x &= x - 1
ones++
}
return ones
}
复杂度分析:
方法一需要对每个整数使用 的时间计算「一比特数」。可以换一个思路,当计算 的「一比特数」时,如果存在 , 的「一比特数」已知,且 和 相比, 的二进制表示只多了一个 ,则可以快速得到 的「一比特数」。
令 表示 的「一比特数」,则上述关系可以表示成:。
对于正整数 ,如果可以知道最大的正整数 ,使得 且 是 的整数次幂,则 的二进制表示中只有最高位是 ,其余都是 ,此时称 为 的「最高有效位」。令 ,显然 ,则 。
为了判断一个正整数是不是 的整数次幂,可以利用方法一中提到的按位与运算的性质。如果正整数 是 的整数次幂,则 的二进制表示中只有最高位是 ,其余都是 ,因此 。由此可见,正整数 是 的整数次幂,当且仅当 。
显然, 的「一比特数」为 。使用 表示当前的最高有效位,遍历从 到 的每个正整数 ,进行如下操作。
如果 ,则令 ,更新当前的最高有效位。
比 的「一比特数」多 ,由于是从小到大遍历每个整数,因此遍历到 时, 的「一比特数」已知,令 。
最终得到的数组 即为答案。
function countBits(n) {
const bits = new Array(n + 1).fill(0)
let highBit = 0
for (let i = 1; i <= n; i++) {
if ((i & (i - 1)) == 0) {
highBit = i
}
bits[i] = bits[i - highBit] + 1
}
return bits
}
复杂度分析:
方法二需要实时维护最高有效位,当遍历到的数是 的整数次幂时,需要更新最高有效位。如果再换一个思路,可以使用「最低有效位」计算「一比特数」。
对于正整数 ,将其二进制表示右移一位,等价于将其二进制表示的最低位去掉,得到的数是 。如果 的值已知,则可以得到 的值:
如果 是偶数,则 ;
如果 是奇数,则 。
上述两种情况可以合并成: 的值等于 的值加上 除以 的余数。
由于 可以通过 得到, 除以 的余数可以通过 得到,因此有:。
遍历从 到 的每个正整数 ,计算 的值。最终得到的数组 即为答案。
function countBits(n) {
const bits = new Array(n + 1).fill(0)
for (let i = 1; i <= n; i++) {
bits[i] = bits[i >> 1] + (i & 1)
}
return bits
}
复杂度分析:
定义正整数 的「最低设置位」为 的二进制表示中的最低的 所在位。例如,10 的二进制表示是 ,其最低设置位为 ,对应的二进制表示是 。
令 ,则 为将 的最低设置位从 变成 之后的数,显然 ,。因此对任意正整数 ,都有 。
遍历从 到 的每个正整数 ,计算 的值。最终得到的数组 即为答案。
var countBits = function (n) {
const bits = new Array(n + 1).fill(0)
for (let i = 1; i <= n; i++) {
bits[i] = bits[i & (i - 1)] + 1
}
return bits
}
复杂度分析: