题目描述(简单难度)
给一个数字,求出其二进制形式中 1
的个数。
解法一
简单粗暴些,依次判断最低位是否是 1
,然后把它加入到结果中。判断最低位是否是 1
,我们只需要把原数字和 000000..001
相与,也就是和 1
相与即可。
public int hammingWeight(int n) {
int count = 0;
while (n != 0) {
count += n & 1;
n >>>= 1;
}
return count;
}
解法二
比较 trick
的方法,官方 题解提供的,分享一下。
有一个方法,可以把最右边的 1
置为 0
,举个具体的例子。
比如十进制的 10
,二进制形式是 1010
,然后我们只需要把它和 9
进行按位与操作,也就是 10 & 9 = (1010) & (1001) = 1000
,也就是把 1010
最右边的 1
置为 0
。
规律就是对于任意一个数 n
,然后 n & (n-1)
的结果就是把 n
的最右边的 1
置为 0
。
也比较好理解,当我们对一个数减 1
的话,比如原来的数是 ...1010000
,然后减一就会向前借位,直到遇到最右边的第一个 1
,变成 ...1001111
,然后我们把它和原数按位与,就会把从原数最右边 1
开始的位置全部置零了 ...10000000
。
有了这个技巧,我们只需要把原数依次将最右边的 1
置为 0
,直到原数变成 0
,记录总共操作了几次即可。
public int hammingWeight(int n) {
int count = 0;
while (n != 0) {
n &= (n - 1);
count += 1;
}
return count;
}
解法三
有点类似于 190 题 的解法二,通过整体的位操作解决问题,参考 这里-by-time-m-is-the-count-of-1's-and-another-several-method-of-O(1)-time) ,也是比较 trick
的,不容易想到,但还是很有意思的。
本质思想就是用本身的比特位去记录对应位数的比特位 1
的个数,举个具体的例子吧。为了简洁,求一下 8
比特的数字中 1
的个数。
统计数代表对应括号内 1 的个数
1 1 0 1 0 0 1 1
首先把它看做 8 组,统计每组 1 的个数
原数字:(1) (1) (0) (1) (0) (0) (1) (1)
统计数:(1) (1) (0) (1) (0) (0) (1) (1)
每个数字本身,就天然的代表了当前组 1 的个数。
接下来看做 4 组,相邻两组进行合并,统计数其实就是上边相邻组统计数相加即可。
原数字:(1 1) (0 1) (0 0) (1 1)
统计数:(1 0) (0 1) (0 0) (1 0)
十进制: 2 1 0 2
接下来看做 2 组,相邻两组进行合并,统计数变成上边相邻组统计数的和。
原数字:(1 1 0 1) (0 0 1 1)
统计数:(0 0 1 1) (0 0 1 0)
十进制: 3 2
接下来看做 1 组,相邻两组进行合并,统计数变成上边相邻组统计数的和。
原数字:(1 1 0 1 0 0 1 1)
统计数:(0 0 0 0 0 1 0 1)
十进制: 5
看一下 「统计数」的变化,也就是统计的 1
的个数。
看下二进制形式的变化,两两相加。
看下十进制形式的变化,两两相加。
最后我们就的得到了 1
的个数是 5
。
所以问题的关键就是怎么实现每次合并相邻统计数,我们可以通过位操作实现,举个例子。
比如上边 4
组到 2
组中的前两组合成一组的变化。要把 (1 0) (0 1)
两组相加,变成 (0 0 1 1)
。其实我们只需要把 1001
和 0011
相与得到低两位,然后把 1001
右移两位再和 0011
相与得到高两位,最后将两数相加即可。也就是(1001) & (0011) + (1001) >>> 2 & (0011)= 0011
。
扩展到任意情况,两组合并成一组,如果合并前每组的个数是 n
,合并前的数字是 x
,那么合并后的数字就是 x & (000...111...) + x >>> n & (000...111...)
,其中 0
和 1
的个数是 n
。
public int hammingWeight(int n) {
n = (n & 0x55555555) + ((n >>> 1) & 0x55555555); // 32 组向 16 组合并,合并前每组 1 个数
n = (n & 0x33333333) + ((n >>> 2) & 0x33333333); // 16 组向 8 组合并,合并前每组 2 个数
n = (n & 0x0f0f0f0f) + ((n >>> 4) & 0x0f0f0f0f); // 8 组向 4 组合并,合并前每组 4 个数
n = (n & 0x00ff00ff)+ ((n >>> 8) & 0x00ff00ff); // 4 组向 2 组合并,合并前每组 8 个数
n = (n & 0x0000ffff) + ((n >>> 16) & 0x0000ffff); // 2 组向 1 组合并,合并前每组 16 个数
return n;
}
写成 16
进制可能不好理解,我们拿16 组向 8 组合并举例,合并前每组 2 个数。也就是上边我们推导的,我们要把 (1 0) (0 1)
两组合并,需要和 0011
按位与,写成 16
进制就是 3
,因为合并完是 8
组,所以就是 8
个 3
,即 0x33333333
。
总
解法一比较常规,解法二很技巧,解法三就更加技巧了,但想法很强。这几天都是操作二进制数,只要对一些位操作了解,常规解法只要按照题目意思还原过程即可。