题目描述
输入一个整数,输出该数二进制表示中111的个数。其中负数用补码表示。
考查知识点
1、计算机中的二进制与十进制
计算机中的int类型占用444个字节,每个字节占888位,则int类型占用了323232位。
计算机中的正整数就是其本身转化为二进制后的表示;负数的表示是其绝对值的补码。
原码:
一个正数的原码,是按照绝对值大小转换成的二进制数;
一个负数的原码,是按照绝对值大小转换成的二进制数,然后最高位补111。
比如:
555 和 −5-5−5 的原码分别是:
00000000 00000000 00000000 00000101; //5的原码
10000000 00000000 00000000 00000101; //-5的原码
反码:
正数的反码与原码相同,负数的反码为对该数的原码除符号位外各位取反。
比如:
555 和 −5-5−5 的反码分别是:
00000000 00000000 00000000 00000101; //5的反码
11111111 11111111 11111111 11111010; //-5的反码
补码:
正数的补码与原码相同;
负数的补码为对该数的原码除符号位外各位取反,然后在最后一位加111。
比如:
−5-5−5的补码为:
1111111 11111111 11111111 11111011; // -5的补码
总结:
原码表示法规定:用符号位和数值表示带符号数,正数的符号位用“000”表示,负数的符号位用“111”表示,数值部分用二进制形式表示。
反码表示法规定:正数的反码与原码相同,负数的反码为对该数的原码除符号位外各位取反。
补码表示法规定:正数的补码与原码相同,负数的补码为对该数的原码除符号位外各位取反,然后在最后一位加111。
正零和负零的补码相同,[+0]补=[-0]补=0000 0000B
八进制和十六进制:
无论是多少进制,C、C++都是int类型,八进制前面加000,十六进制前面加0x。但如果直接打印数值,结果还是十进制。
int i = 0x75; // 无论8进制还是16进制 直接写 打印的时候直接恢复到十进制
cout << i << endl; // i = 117为其结果
2、C,C++中的运算符
<<表示左移操作符:
m << n :表示m的2n2^n2n,也表示m为二进制时,左移n位,即后面补n个000。
int j = 5 << 2; // 表示5 * 4 = 20
cout << "j = " << j << endl; // 20
>>表示右移操作符:
m >> n :表示m 除以 2n2 ^ n2n 取整,也表示m为二进制表示时,m右移n位,即删掉后面n位数。
int j = 7 >> 2; // 7 / 2 = 3
cout << "j = " << j << endl; // 3
&表示按位与:
两个二进制数每一位均为111,该位置返回111,其余情况返回000。
int i = 5, j = 4;// 5 = 101, 4 = 100
int k = i & j; // 按位与 k = 100
cout << k << endl; // 4
|表示按位或:
两个二进制数每一位均为000,该位置返回000,其余情况返回111。
int i = 6, j = 5;// 6 = 110, 5 = 101
int k = i | j; // 按位与 k = 111
cout << k << endl; // 7
&&表示逻辑与:
&&两边式子为真返回真,否则返回假。
||表示逻辑或:
||两边的表达式均为假返回假,否则返回真。
解题思路
本题目可以通过 & 按位与运算来求二进制中111的个数 和通过 对一个从111开始的整数循环的 << 左移 来匹配二进制中1的个数。
代码实现1
如果一个整数不为000,那么这个整数至少有一位是111。如果我们把这个整数减111,那么原来处在整数最右边的111就会变为000,原来在111后面的所有的000都会变成111(如果最右边的111后面还有000的话)。其余所有位将不会受到影响。
例如: 如果 n=10011n = 10011n=10011,
1、n−1=10010n - 1 = 10010n−1=10010 ,n & flag 是 10011&10010=1001010011 \& 10010 = 1001010011&10010=10010,此时$ n = 10010$,即二进制中少了一个111。
2、n−1=10010−1=10001n - 1 = 10010 - 1 = 10001n−1=10010−1=10001,n & flag 是 10010&10001=1000010010 \& 10001= 1000010010&10001=10000,此时 n=10000n = 10000n=10000,即二进制中又少了一个111。
以此类推,每次进行按位与,均少一个111,最后 n = 0 统计少了多少个111即可。
class Solution {
public:
int NumberOf1(int n) {
int num = 0;
while(n)
{
num++;
n &= (n - 1); //n = n & (n - 1); // 两者一致
}
return num;
}
};
代码实现2
flag每次 的 flag << 1 就是每次 从111 变成 101010 变成 100100100 变成 100010001000… 每次 flag 与 n进行按位与 只要不为000 就说明 nnn 中的某一位为111 否则该位为000。
例如: 如果 n=10010n = 10010n=10010,
1、flag = 1,n & flag 是 10010&00001=010010 \& 00001 = 010010&00001=0;
2、flag = 10,n & flag 是 10010&00010=0001010010 \& 00010 = 0001010010&00010=00010,即该位置存在111。
…
以此类推即可。
这种思想还是有一定的隐患的,就是while循环必须32次,直到flag = 0 为止。
class Solution {
public:
int NumberOf1(int n) {
int num = 0;
int flag = 1;
while(flag)
{
if (n & flag)
num++;
flag = flag << 1; // flag <<= 1; //两者一致
}
return num ;
}
};
下面是在代码中的while循环中添加这段代码后的效果:
cout << "flag = " << flag << endl;
flag的变化:
flag = 2
flag = 4
flag = 8
flag = 16
flag = 32
flag = 64
flag = 128
flag = 256
flag = 512
flag = 1024
flag = 2048
flag = 4096
flag = 8192
flag = 16384
flag = 32768
flag = 65536
flag = 131072
flag = 262144
flag = 524288
flag = 1048576
flag = 2097152
flag = 4194304
flag = 8388608
flag = 16777216
flag = 33554432
flag = 67108864
flag = 134217728
flag = 268435456
flag = 536870912
flag = 1073741824
flag = -2147483648
flag = 0
可见这种算法的效率还是比较低的。
某公司面试真题
用一个表达式,判断一个数xxx是否是2N2^N2N,不能用任何循环语句。
解析
2、4、8、162、4、8、162、4、8、16…转化为二进制是10、100、1000、1000010、100、1000、1000010、100、1000、10000…,也就是二进制中存在至多111个111,如果x−1x - 1x−1后再与xxx进行按位与运算,返回000则说明是xxx是2N2^N2N,否则不是。
答案
!(x & (x - 1));