传送门:https://leetcode-cn.com/problems/counting-bits/

题目

给定一个非负整数 num
对于 0 ≤ i ≤ num范围中的每个数字 i ,计算其二进制数中的 1的数目并将它们作为数组返回。

示例 1:

输入: 2
输出: [0,1,1]

示例 2:

输入: 5
输出: [0,1,1,2,1,2]

一.解题思路:模拟计算->二进制优化

最直观的方法是对从0num 的每个数直接计算比特数

按位与运算**(&)**的一个性质是:
对于任意整数 x,令 x=x & (x−1),该运算将 x 的二进制表示的最后一个 1 变成 0。因此,对 x 重复该操作,直到 x 变成 0,则操作次数即为 x比特数

复杂度分析

时间复杂度:[LeetCode]Dp338 比特位计数 [二进制操作] - 图1

其中 是 型的二进制位数,k=32 。需要对从 0num的每个数使用O(k) 的时间计算,因此时间复杂度是 O(k×num)

空间复杂度:[LeetCode]Dp338 比特位计数 [二进制操作] - 图2
除了返回的数组意外,空间复杂度为常数

代码

官方代码

  1. class Solution {
  2. public int[] countBits(int num) {
  3. int[] bits = new int[num + 1];
  4. for (int i = 0; i <= num; i++) {
  5. bits[i] = countOnes(i);
  6. }
  7. return bits;
  8. }
  9. public int countOnes(int x) {
  10. int ones = 0;
  11. while (x > 0) {
  12. x &= (x - 1);
  13. ones++;
  14. }
  15. return ones;
  16. }
  17. }

二.解题思路:动态规划->最高有效位

image.png
我们在观察中发现,除了 0 以外,所有数的最高位一定为 1 ,并且去除最高位后发现剩余部分其实已经统计过。
以十进制 7 为例,二进制为 111,去除最高位固定的 1 。剩余部分为 011 ,其实就是十进制的 3
7-4=3 ,且 4 就是擦除的最高位 100 。整个的重点就是如何确定最高位的 100 的范围 。即最高有效位
确定只要依靠二进制的按位与性质,若 y&(y-1)=0 则是最高有效位 。

解释最高有效位: 对于正整数 x,如果可以知道最大的正整数 y,使得 y<=xy2 的整数次幂,此时称 yx「最高有效位」。则 y 的二进制表示中只有最高位是 1,其余都是 0

状态转移方程:

使用 表示当前的最高有效位,遍历从1_num_ 的每个正整数 i,进行如下操作。

  1. - 如果 `i&(i-1)=0`,则令 `highBit=i`,更新当前的最高有效位
  2. - `i` `**i-highBit**` 的二进制多一个 `1` ,因此使用状态转移方程

复杂度分析

时间复杂度:[LeetCode]Dp338 比特位计数 [二进制操作] - 图4

对于每个数只需要 O(1) 的时间计算 。

空间复杂度:[LeetCode]Dp338 比特位计数 [二进制操作] - 图5
除了返回的数组意外,空间复杂度为常数

代码

官方代码

class Solution {
    public int[] countBits(int num) {
        int[] bits = new int[num + 1];
        int highBit = 0;
        for (int i = 1; i <= num; i++) {
            if ((i & (i - 1)) == 0) {
                highBit = i;
            }
            bits[i] = bits[i - highBit] + 1;
        }
        return bits;
    }
}

三.解题思路:动态规划->最低有效位

[LeetCode]Dp338 比特位计数 [二进制操作] - 图6
我们在观察中不难发现 [🦄尼玛难的丫匹],任何一个数字都分奇偶性。
奇数的末尾一定是1,偶数的末尾一定是0
因此当去除末尾的二进制后,剩余部分的值显然先前已经计算过。因此可以实现状态转移。

对于正整数 x ,将其二进制表示右移一位,等价与将其二进制表示的最低位去掉,得到的数是 [ x/2 ]。

  - 若 `x` 是偶数,则 `bits[x]=bits[x/2]`
  - 若 `x` 是奇数,则 `bits[x]=bits[x/2]+1`

上述两种情况可以合并成:bits[x]=bits[x/2]+(x&1)

解释:当 x 为奇数 x&1 =1,奇数的末尾一定是1 当 x 为偶数 x&1 =0,偶数的末尾一定是0

以十进制偶数 6 举例子,去除末尾的 0,剩余部分是 11 也就是十进制的 36/2=3
以十进制奇数 5 举例子,去除末尾的 1,剩余部分是 10 也就是十进制的 25/2=2 [ 整型向下取整 ]

状态转移方程:


复杂度分析

时间复杂度:[LeetCode]Dp338 比特位计数 [二进制操作] - 图7

对于每个数只需要 O(1) 的时间计算 。

空间复杂度:[LeetCode]Dp338 比特位计数 [二进制操作] - 图8
除了返回的数组意外,空间复杂度为常数

代码

官方代码

class Solution {
    public int[] countBits(int num) {
        int[] bits = new int[num + 1];
        for (int i = 1; i <= num; i++) {
            bits[i] = bits[i >> 1] + (i & 1);
        }
        return bits;
    }
}

四.解题思路:动态规划->设置最低位

回到最初的思路上,我们解题主要依赖于二进制的按位 & 性质。

按位与运算**(&)**的一个性质是:
对于任意整数 x,令 x=x & (x−1),该运算将 x 的二进制表示的最后一个 1 变成 0。因此,对 x 重复该操作,直到 x 变成 0,则操作次数即为 x比特数

除了 0 以外,任意的数字最高位一定是 1 ,所以当我们执行一次按位& 操作后,其实规模已经缩小了。

状态转移方程:

复杂度分析

时间复杂度:[LeetCode]Dp338 比特位计数 [二进制操作] - 图9

对于每个数只需要 O(1) 的时间计算 。

空间复杂度:[LeetCode]Dp338 比特位计数 [二进制操作] - 图10
除了返回的数组意外,空间复杂度为常数

代码

官方代码

class Solution {
    public int[] countBits(int num) {
        int[] bits = new int[num + 1];
        for (int i = 1; i <= num; i++) {
            bits[i] = bits[i & (i - 1)] + 1;
        }
        return bits;
    }
}