前 n 个数字二进制中 1 的个数
题目:
给定一个非负整数 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 <= 100000
进阶:
给出时间复杂度为 O(n*sizeof(integer)) 的解答非常容易。但你可以在线性时间 O(n) 内用一趟扫描做到吗?
要求算法的空间复杂度为 O(n) 。
你能进一步完善解法吗?要求在C++或任何其他语言中不使用任何内置函数(如 C++ 中的 __builtin_popcount )来执行此操作。
分析:
题目中说明部分给了一种思路,那么就是实现时间复杂度为 O(n*sizeof(integer)) 的解答,不妨来看下,虽然题目说这种方式简单,但是依旧需要思考,优化的过程是在基础进行,不能直接跳过最简单的解法去思考高难度的解法,这样是不现实的。
代码:
第一版:暴力法
第一版中三种方式都运行效率基本一样:因为基本都是题目说的时间复杂度最大的那几种。
实现题目中的解法,对于遍历到的每一个数字n,都单独进行count --1个数的操作。以下是几种实现的方式,使用firstX来分别区分。
class Solution {
public int[] countBits(int n) {
// 下面两个结果都是答案,是分别不同的方式求解得出
int[] res01 = first01(n);
int[] res02 = first02(n);
int[] res03 = first03(n);
}
public static int[] first01(int n){
int one_cnt[] = new int[n+1];
for(int i=0;i<=n;i++){
one_cnt[i] = calOneCntVer01(i);
}
return one_cnt;
}
public static int calOneCntVer01(int i){
// 使用int类型转二进制-->String 【本题i<= 100000,符合int范围,可以直接调用】
// 根据String的replace方法得到1的个数
String binStr = Integer.toBinaryString(i);
return binStr.length()-binStr.replace("1","").length();
}
//使用遍历32位进制,得到1的个数
public static int[] first02(int n){
int one_cnt[] = new int[n+1];
// 01) 因为本题n<=100000,所以先求最高进制位,比如3的表示是 0011,那么求每一个位上是否为1只需要从倒数第二位开始即可,因为第二位左面的位上肯定都是0,可以不管
int x = 100000;
int posCount = 0;
for(int i=30;i>=0;i--){
if(x>=(1<<i)){
posCount = i+1; // 这里就得到了n的最高有效位,再往左就都是0了【这里要注意得到结果+1,就类似货装卡车的问题,一卡车装1t,货物总共4.3t,也需要5辆卡车才能完全涵盖4.3t货物】
break;
}
}
// 02) 遍历每一个n的取值 调用calOneCntVer02方法
for(int i=0;i<=n;i++){
// 03) 直接从posCount开始判断是否为1即可
one_cnt[i] = calOneCntVer02(i,posCount);
}
return one_cnt;
}
public static int calOneCntVer02(int n,int posCount){
int oneCount = 0;
// 01)从第posCount 向右边的二进制位移动,表示在i上就是i的递减
for(int i=posCount;i>=0;i--){
if(n>=(1<<i)){
// 02) 如果n>=(1<<i) 证明第i位上肯定=1
// n-=(1<<i) 的意思就是 将i位的1 归0 ,然后继续递减判断
n-=(1<<i);
// 每次 归0,就是一个1的消失,这里需要记录一共存在几个1
oneCount++;
}
}
return oneCount;
}
// 直接使用java内置函数 Integer.bitCount 直接得出结果 【本题n符合int范围可以使用】
public static int[] first03(int n){
int one_cnt[] = new int[n+1];
for(int i=0;i<=n;i++){
one_cnt[i] = Integer.bitCount(i);
}
return one_cnt;
}
}
第二版:Brian Kernighan
算法
这个方法应该少数人可以了解,大部分可能都是不太熟悉【包括我😂】。
Brian Kernighan
算法的原理是:对于任意整数x
,令x=x&(x-1)
,该运算将x
的二进制表示的最后一个1
变成0
。因此,对x
重复该操作,直到x
变成0
,则操作次数即为x
的「一比特数」。
在代码实现前,我想解释下该算法的正确性,防止大家使用了该原理,但是不知道为什么该原理为什么正确,那就很尴尬了。
1.如果x是偶数,那么x的二进制表示低位一定是0. 【举例 12-> …1100,38->0010 0110】
x | 12(1100) | 38(0010 0110) | 126(0111 1110) |
---|---|---|---|
x-1 | 11(1011) | 37(0010 0101) | 125(0111 1101) |
x&(x-1) | 1000 | 0010 0100 | 0111 1100 |
从上应该不难看出,其实x & (x-1)
的 步骤就是把最低位的1
消掉的同时不影响高位的1
存在。所以如果一直重复该步骤,可以实现将一个一个的低位1
消除掉,直到没有1
存在为止。
// 原理在上已经解释的很清楚,实现过程也比较简单,这里就省略了
class Solution {
public int[] countBits(int n) {
int[] res = new int[n+1];
for(int i=0;i<=n;i++){
res[i] = calOneCnt(i);
}
return res;
}
public static int calOneCnt(int x){
int count = 0;
while(x>0){
x&=(x-1);
count++;
}
return count;
}
}
第三版:【动态规划解法】
n | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
二进制 | 0000 0001 | 0000 0010 | 0000 0011 | 0000 0100 | 0000 0101 | 0000 0110 | 0000 0111 |
个数(1) | 1 | 1 | 2 | 1 | 2 | 2 | 3 |
可以发现如下规律:设f(n)表示二进制表示下1的个数
①如果n
是2的i次方
,【注意,这里n
不是2
的倍数,而2
的i次方,i
属于正整数】那么n
的二进制中只含一个1
。
满足:``f(n) = 1`
②如果一个数n
,满足如下条件: 2^(i) < n < 2^(i+1)
【为了方便,我们只看左边的界限,先忽略右面的界限,并将2^(i)
假设为z
】,则上面的假设条件切换为:
假设数z是2的i次方,数n满足 n>z
且n小于2*z
。
实践便可得知 ==》f(n) = f(z) + f(n-z)
【
二进制的加法,高位和低位的二进制相加,高位1
的个数 + 低位1
的个数就是最后的总个数,因为f(z)
只有高位存在1
,其余位都是0
,f(n-z)
只有低位存在1
,其他位也是0
,所以二者加起来就是f(n)
举个例子:n = 10,z = 8,f(10) = 1010个数为2,f(8)=1000个数为1,f(10-8)=0010个数为1。这种相加并不会影响到1的增多和减少。
】
设y = n-z
【必定满足0<y<n
】 ,结论就是:f(n) = f(n-z) + f(z)
因为f(z) = 1
,所以可以表示为:
📢📢📢📢
f(n) = f(n-z) + 1
【z是小于n的最大2次方整数】
好像我们找到了状态转移方程 😁😁😁 ==》 【动态规划解法】
⚠️⚠️⚠️⚠️
发现规律了就可以写代码来解决问题了。
class Solution {
public int[] countBits(int n) {
int[] res = new int[n+1];
//01)n=0的时候,二进制中1的个数为0,也是为了下面循环避免越界问题
//02)lastZ就是当前循环中小于i的最大2次方数
res[0] = 0;
int lastZ = 0;
//03)循环逻辑
for(int i=1;i<=n;i++){
//04)执行一次 Brian Kernighan 算法,如果是2的i次方,那么去掉1个0,x返回就变成0了
// 如果返回0,那么n=i的时候,二进制中1的个数就是1
// 否则的话,f(n) = f(n-z) + 1
if(processOneTimeRemove(i)==0){
res[i] = 1;
lastZ = i;
}else{
res[i] += (res[i-lastZ]+1);
}
}
return res;
}
//执行一次 Brian Kernighan 算法,如果是2的i次方,那么去掉1个0,x返回就变成0了
public static int processOneTimeRemove(int x){
return x&(x-1);
}
}
第四版:【奇偶特性–动态规划】
1、设n
满足n%2=0
【大白话:n
是偶数】,那么可得,n
的二进制最低位=0【这个是真的,不信的可以自行测试,不可能出现偶数的最低位=1😂,因为偶数一定是2的倍数,那么表现在二进制中,就是2 + 2 +。。。。2==> 2^i + 2^(i-1) + 2^(i-2)......+ 2^1
】
前提:设f(n)
表示二进制表示下1
的个数
所以可以得出:
f(n) = f(n>>1)
【>>
表示n除以2】
2、当n是奇数的时候,就反过来可得n的二进制最低位=1.
所以也可以同样得出:
f(n) = f(n>>1) + 1
【>>
表示n除以2】
3、将规律整合一下,可以得出最后的规律:
f(n) = f(n>>1) + n%2==0?0:1
class Solution {
public int[] countBits(int n) {
int[] res = new int[n+1];
for(int i=0;i<=n;i++){
res[i] = (res[i>>1] + (i%2==0?0:1));
}
return res;
}
}
总结:
这道题实现方法实在是太多了,【暴力法、不为人知的算法、记录高位法、动态规划法、奇偶二进制特性法等等】在没有写题解之前,确实思路有限,但是经过这么一次整理,我相信,不管是写作的我还是阅读的你,对于二进制必定已经加深很多了。
大家好,我是二十一画,感谢您的品读,如有帮助,不胜荣幸~😊