本篇博客会讲解一道经典的题目:求一个整数二进制中1的个数。阅读本篇博客前,需要你对C语言如何进行二进制位操作有一定的了解,如果还不太了解的话,可以阅读一下我的这篇博客。
我们假设有一个int类型的整数n,我们知道,n在内存中是以二进制的补码的形式存储的。这个补码的二进制,是由0和1组成的,究竟有多少个1呢?我们能不能写个程序来求一下?
“&1”的妙用
如果你认真读了我讲解位操作符的那篇博客,应该知道n&1
代表了啥。不知道也没关系,可以来观察一下:
1: 00000000000000000000000000000001
n: 11000111101011101010011110110111
左边的31位,由于“任何数 & 0”都会得到0,所以最终的结果只由最右边的1位决定。分类讨论一下:
- 如果n的最右边1位是0,则按位与的结果是0。
- 如果n的最右边1位是1,则按位与的结果是1。
再提炼一下,n & 1
的结果是1,说明n的最右边1位是1,否则n的最右边1位是0,换句话说,n & 1
的结果是n最右边的1位。
那么我们就有思路了:我们通过(n >> i) & 1
就能拿到第i位的值,统计其中有几个1就行了。
int GetNumOf1(int n)
{
int count = 0;
for (int i = 0; i < 32; ++i)
{
if (((n >> i) & 1) == 1)
++count;
}
return count;
}
从“%10 /10”到“%2 /2”
看到“%10 /10”,大家会联想到啥?如果你做题做多了,会第一时间反应到:%10可以拿到一个十进制数的最后一位,而/10会把最后一位去掉!比如:
123 % 10 = 3
123 / 10 = 12
12 % 10 = 2
12 / 10 = 1
1 % 10 = 1
1 / 10 = 0
在n变成0之前,就可以拿到十进制中的每一位。那如果是拿到二进制中的每一位呢?“%2 /2”不就行了!不过需要注意的是,“%2 /2”的操作必须对无符号整数进行,如果对有符号整数,遇到负数时结果是错的。比如,-1的补码有32个1,但是使用“%2/2”操作并不会操作32次,第一次/2就变成0了,结果是错的。
int GetNumOf1(unsigned int n)
{
int count = 0;
while (n)
{
if (n % 2 == 1)
++count;
n /= 2;
}
return count;
}
神奇的“n &= n - 1”
n &= n - 1
的效果是去掉n的二进制位中最右边的那个1。比如:
n: 00000000000000000010001100001001
n &= n - 1
n: 00000000000000000010001100001000
n &= n - 1
n: 00000000000000000010001100000000
n &= n - 1
n: 00000000000000000010001000000000
n &= n - 1
n: 00000000000000000010000000000000
n &= n - 1
n: 00000000000000000000000000000000
由于一开始的二进制中有5个1,所以执行5次n &= n - 1
后,n就变成0了。
这个思路实现成代码如下:
int GetNumOf1(unsigned int n)
{
int count = 0;
while (n)
{
++count;
n &= n - 1;
}
return count;
}
总结
n & 1
的操作可以得到n的补码的二进制最右边的那一位数。- 反复“%2 /2”可以得到二进制中的每一位。
n &= n - 1
的效果是去掉n的二进制位中最右边的那个1。
感谢大家的阅读!