题目:338. 比特位计数
这个题目大致的意思:就是比如给一个数 n,假设 n = 5, 那么就返回数组【长度为 n + 1】,然后返回 数组下标这个数对应的二进制的 1 有多少个
做法一【遍历 + 位运算】
遍历数组,然后通过下标循环进行位运算,得到最右边的一,然后通过右移,去掉 1,然后数组的值累加
去掉二进制数中最右边的 1
// ~eor + 1: 取反后加 1
// 可以这么想:
// 1. 括号优先级最高 所以先处理括号里面的
// 2. ~eor + 1: 将eor进行取反,1变为0,0变为1,然后 + 1
// 2.1 这一步比较巧妙,从最右边第一位开始看,
// 如果该位的数是 0,取反后为 1,那么在加 1,那就为0,倒数第二位 加 1,后面同理,例如:eor = 00110000(48)
// 如果该位的数是 1,取反后为 0,那么在加 1,那就为1,左边的数全部不变,例如:eor = 011010001(47)
// 也就是说变化的只有 eor 中位数最右边的 1,以及他的右边(由 1 变为 0)
// 3. eor & (~eor + 1): 可以这么想 (~eor + 1) 之后 变化的位数为 最右边的 1 本身 和 他的左边
// 又因为已经取反过了,所以 最右边 1 的 右边保持取反后的,进行 & 运算,因为和之前的数相反所以此时他的左边全部都是 0,
// 最右边的 1,取反 + 1 后还是 1,和他原来的位数也是 1,所以为 1
// 最右边的 1 左边的数,从原本的 0,变为 0, 0 & 0 还是 0, 所以他的左边全都是 0
// 例如:47: 00101111, (~47 + 1): 11010001, 47 & (~47 + 1): 00000001
// 获取最有边的 1
int rightOne = eor & (~eor + 1);
// 去掉最右边的 1
// 从上面的 rightOne 获取到最右边的 1
// 假设 eor 为 12,那么二进制就为 1100,rightOne 就为 0100
// 那么这时候就可以将他取反,~rightOne = 1011,
// 然后在与 eor 逻辑与,也就是两个都为 1 的时候结果才为1,这样就能把1去掉,然后还不影响
eor &= (~rightOne);
// 例如 eor = 12
// &: 位运算中,只有两个数都为1,才为1
// 12(十进制)=》1100(二进制)
// 11(十进制)=》1011(二进制)
// 12 & 11 = 1100 & 1011 = 1000
eor &= (eor - 1)
解决
时间复杂度:O(nlogn),空间复杂度:O(n)
/**
* 执行用时分布 2 ms 击败 45.27% 复杂度分析 消耗内存分布 45.38 MB 击败 90.86%
*/
public int[] countBits(int n) {
int[] arr = new int[n + 1];
for (int i = 1; i < arr.length; i++) {
int temp = i;
while (temp != 0){
// 获取到最右边的 1 的位置【二进制】
int rightOne = temp & (~temp + 1);
// 去掉最右边的 1
temp &= (~rightOne);
arr[i]++;
}
}
return arr;
}
做法二【动态规划 + 位运算】
通过前面的答案去减少后面不必要的计算,达到时间上的效率
想法
假设计算 12 的有多少个 1,而这个时候前面已经得到了 0到 11 的答案,那么这个时候是否可以利用前面的答案获取到 12 的答案呢?
这时候就想到了,既然前面是利用的位运算,那么这里也可以通过位运算将最右边的 1 去掉,因为前面的答案已经有了,那么结果不就是 1 + arr[num] 吗(num 为 去掉最右边 1 之后的 数)
为什么一定要去掉最右边的 1,而不是直接通过某种规律获取呢?我的想法是可以通过这个获取的结果的方式,将数减少到能碰撞到前面的答案
例子
12 (十进制)=》 1100(二进制)
去掉最右边的 1 之后:100 (二进制)=》4(十进制)
那么结果就等于 1 + arr[4] = 2
解决
时间复杂度:O(n),空间复杂度:O(n)
/**
* 位运算 + 动态规划
* 执行用时分布 1 ms 击败 99.79% 复杂度分析 消耗内存分布 45.44 MB 击败 78.31%
*/
public int[] countBits2(int n) {
int[] arr = new int[n + 1];
for (int i = 1; i < arr.length; i++) {
// 获取到最右边的 1 的位置【二进制】
int rightOne = i & (~i + 1);
// 去掉最右边的 1
int num = i & (~rightOne);
// 去掉最右边的一个 1,之后就可以重复利用之前的结果
arr[i] = 1 + arr[num];
}
return arr;
}
CSDN 链接:blog.csdn.net/a2497_28226…