338. 比特位计数
给定一个非负整数 num。对于 0 ≤ i ≤ num 范围中的每个数字 i ,计算其二进制数中的 1 的数目并将它们作为数组返回。
示例 1:
输入: 2 输出: [0,1,1]
示例 2:
输入: 5 输出: [0,1,1,2,1,2]
进阶:
给出时间复杂度为O(n*sizeof(integer))的解答非常容易。但你可以在线性时间O(n)内用一趟扫描做到吗?
要求算法的空间复杂度为O(n)。
你能进一步完善解法吗?要求在C++或任何其他语言中不使用任何内置函数(如 C++ 中的 __builtin_popcount)来执行此操作。
思路一:
1. 遍历1-num的所有二进制
2. 使用布赖恩·克尼根算法,xor & (xor - 1)即可移除xor最右边的那个1,省去了移除0的过程,减少了迭代次数,直接删除最右边的1
1 class Solution { 2 public int[] countBits(int num) { 3 // 遍历1-num的所有二进制 4 int[] res = new int [num + 1]; 5 Arrays.fill(res, 0); 6 for(int i = 0; i <= num; i++){ 7 res[i] = 0; 8 int temp = i; 9 while(temp != 0){ 10 res[i]++; 11 temp = temp & (temp - 1); // 使用布赖恩·克尼根算法,xor & (xor - 1)即可移除xor最右边的那个1,省去了移除0的过程,减少了迭代次数,直接删除最右边的1 12 } 13 } 14 return res; 15 16 } 17 }
力扣测试时间需要3ms, 超过了39.70%的用户,效率不高
算法复杂度为:
时间复杂度为O(n*k), 对于每个数x, k表示x中1的个数
空间复杂度:O(n)。 我们需要 O(n) 的空间来存储计数结果。如果排除这一点,就只需要常数空间。
思路二:动态规划 + 最高有效位
我们假设 b = 2^m, 那么b的第(m + 1)个比特位肯定为1, 小于(m+1)的比特位全为0, 如果一个小于b的数x, 那么这个数的第(m+1)为肯定为0, 那么(x + b)的结果就是在x的原来的二进制形式上把原来第(m+1)位上的0改成1, 所以(x + b) 的二进制形式的1的个数比x多一个,由此推出,f(x+b) = f(x) + 1, 当x< b时, 这其实是[0, b)到[b, 2b)的映射
1 class Solution { 2 public int[] countBits(int num) { 3 int[] ans = new int[num + 1]; 4 int i = 0, b = 1; 5 while(b <= num){ 6 while(i < b && i + b <= num){ 7 ans[i + b] = ans[i] + 1; // 将[0, b)映射到[b, 2b) 8 i++; 9 } 10 i = 0; // 重置i= 0; 11 b <<= 1; 12 } 13 return ans; 14 } 15 }
力扣测试时间只需2ms, 超过了80.89%的用户,效率高于法一
复杂度分析:
对ans[i]的每个元素进行了一次求值,所以时间复杂度为O(n)
空间复杂度为O(n), ans数组既作为动态规划的数组,又是存储结果的数组,所以这个数组是必须的。
思路三:动态规划 + 最低有效位 (效率最高)
f(x) = f(x/2) + (x&1), (x/2)就是将x右移一位,移掉的那位可能是0也可能是1, 用x和1做与运算即可判断那位是1还是0,动态规划就是用利用前面的计算结果得出后面的计算结果,f(x/2)肯定比f(x)先计算出来,保存在ans[x/2]中,所以f(x)的计算可以利用f(x/2)的取值来获得。
1 class Solution { 2 public int[] countBits(int num) { 3 int[] ans = new int[num + 1]; 4 5 for(int i = 0; i <= num; i++){ 6 ans[i] = ans[i >> 1] + (i & 1); 7 } 8 return ans; 9 } 10 }
力扣测试时间只需1ms, 超过了99.93%的用户,这个算法的效率最高
复杂度分析:
对ans[i]的每个元素进行了一次求值,所以时间复杂度为O(n)
空间复杂度为O(n), ans数组既作为动态规划的数组,又是存储结果的数组,所以这个数组是必须的。
思路四:动态规划 + 最后设置位 (效率最高)
动态规划就是利用前面计算出来的结果退出后面的结果,所以如果我们可以通过前面已经计算出的f(x)退出后面的f(y)的话,就可以以O(n)的时间复杂度完成统计。这里寻找的x肯定必须小于y, 所以寻找的是(y&(y-1)), 这个数看起来很长,其实它就是y移除一个最右边的1的结果,所以这个值肯定比y小,且f(y)和f(y&(y-1))刚好相差一,很容易就由前面的值推出了后面的值。
f(x) = f(x&(x-1)) + 1
1 class Solution { 2 public int[] countBits(int num) { 3 int[] ans = new int[num + 1]; 4 5 for(int i = 1; i <= num; i++){ 6 ans[i] = ans[i & (i - 1)] + 1; 7 } 8 return ans; 9 } 10 }
力扣测试时间只需1ms, 超过了99.93%的用户,算法效率等同于法三
复杂度分析:
对ans[i]的每个元素进行了一次求值,所以时间复杂度为O(n)
空间复杂度为O(n), ans数组既作为动态规划的数组,又是存储结果的数组,所以这个数组是必须的。
思路来源:
https://leetcode-cn.com/problems/counting-bits/solution/bi-te-wei-ji-shu-by-leetcode/