题目:给定一个非负整数 n ,请计算 0 到 n 之间的每个数字的二进制表示中 1 的个数,并输出一个数组。

示例 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

说明:

  • 0 <= n <= 105

进阶:

  • 给出时间复杂度为 O(n * sizeof(integer)) 的解答非常容易。但你可以在线性时间 O(n) 内用一趟扫描做到吗?
  • 要求算法的空间复杂度为 O(n) 。
  • 你能进一步完善解法吗?要求在 C++或任何其他语言中不使用任何内置函数(如 C++ 中的 __builtin_popcount )来执行此操作。

思路分析

本题的解法基于位运算。对于常用的十进制数转换成二进制数,我们可以查看下表。

x 二进制数 1 的个数
0 0 0
1 1 1
2 10 1
3 11 2
4 100 1
5 101 2
6 110 2
7 111 3
8 1000 1
9 1001 2
10 1010 2

对于任意的正整数x,按位与操作符有这样的一条性质,那就是如果x & (x - 1) = 0,那么x一定是2的整数次幂。我们以1 & 2为例:

//让对应位数一样,往前面补0
//   01  => 1的二进制数表示
// & 10 => 2的二进制数表示
// ——————————————————————
//    0   => 按位与操作符对应2个操作数,只有当2个位都为1时才是1,否则就是0

根据这条性质,我们可以知道,对于任意的十进制数,始终将x与x - 1执行按位与,其结果本质上就是将最右侧的位数值为1变为0,因此当该结果为0时,执行的操作次数即统计出了1的次数。这种算法也被叫做Brian Kernighan 算法。可能以上的示例看不出这条规律,那么我们再举一个示例,比如求10 & 9。

//位数一样,不需要往前面补0
//   1001  => 9的二进制数表示
// & 1010 => 10的二进制数表示
// ——————————————————————
//   1000   => 按位与操作符对应2个操作数,只有当2个位都为1时才是1,否则就是0

因此,我们可以创建一个长度为n + 1的数组,循环n,然后对每一个循环的迭代值i统计执行x & x - 1的操作次数,即可解答本题。代码如下:

/**
 * @param {number} n
 * @return {number[]}
 */
var countBits = function (n) {
    const res = [];
    for(let i = 0;i < n + 1;i++){
        res[i] = countOnes(i);
    }
    // 返回结果
    return res;
};
var countOnes = function(x){
    let count = 0;
    while(x > 0){
        x &= x - 1;
        // 统计次数
        count++;
    }
    return count;
}

以上算法的时间复杂度和空间复杂度分析如下:

  • 时间复杂度:O(n * logn)。需要对从 0 到 n 的每个整数使用计算「1的个数」,对于每个整数计算「1的个数」的时间都不会超过 O(n * logn)。
  • 空间复杂度:O(1)。除了返回的数组以外,空间复杂度为常数。

基于Brian Kernighan 算法,我们可以得出另一种解决思路,那就是动态规划。本题基于动态规划也有三种解法,这里只讲解一种,理解了其中一种,对于其它2种则也能很好的理解,可以点击如下的参考更多思路。

我们定义正整数x的二进制表示中最低/最右侧的1的所在位为[最低设置位],例如10的二进制表示可以为1010(2),亦可以简写成10(2)。因此其最低设置位为2。令y = x & (x - 1),显然0 <= y < x,对于最后的结果res,则有res[x] = res[y] + 1。因此对于任意的正整数x,则都会有res[x] = res[x & (x - 1)] + 1。基于这条性质,我们可以很快写出如下代码:

/**
 * @param {number} n
 * @return {number[]}
 */
var countBits = function (n) {
    // 正整数0中含有1的个数为0,因此动态规划dp[0] = 0;
    const res = [0];
    // 迭代起始值需要从1开始
    for(let i = 1;i < n + 1;i++){
        res[i] = res[i & i - 1] + 1;
    }
    // 返回结果
    return res;
};

以上算法的时间复杂度和空间复杂度分析如下:

  • 时间复杂度:O(n)。对于每个整数,只需要O(1)的时间复杂度来计算1的个数。
  • 空间复杂度:O(1)。除了返回的数组以外,空间复杂度为常数。
Apple

Apple

Graduated in Computer Science and Engineering, but currently working with GNU/Linux infrastructure and in the spare time I'm an Open Source programmer (Python and C), a drawer and author in the YINGJUE Blog.


Comments