文章目录
1.二进制中1的个数
请实现一个函数,输入一个整数,输出该数二进制表示中 1 的个数。例如,把 9 表示成二进制是 1001,有 2 位是 1。因此,如果输入 9,则该函数输出 2。
输入:00000000000000000000000000001011
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 ‘1’。
算法:
在分析这种算法之前,我们先来分析把-一个数减去1的情况。如果一个整数不等于0,那么该整数的二进制表示中至少有一位是1。先假设这个数的最右边一位是1,那么减去1时,最后一位变成0而其他所有位都保持不变。也就是最后一位相当于做了取反操作,由1变成了0。
接下来假设最后一位不是1而是0的情况。如果该整数的二进制表示中最右边的1位于第m位,那么减去1时,第m位由1变成0,而第m位之后的所有0都变成1,整数中第m位之前的所有位都保持不变。举个例子:一个二进制数1100, 它的第二位是从最右边数起的-一个1。减去1后,第二位变成0,它后面的两位0变成1,而前面的1保持不变,因此得到的结果是1011。
在前面两种情况中,我们发现把一个整数减去1,都是把最右边的1变成0。如果它的右边还有0,则所有的0都变成1,而它左边的所有位都保持不变。接下来我们把-一个整数和它减去1的结果做位与运算,相当于把它最右边的1变成0。还是以前面的1100为例,它减去1的结果是1011。我们再把1100和1011做位与运算,得到的结果是1000。 我们把1100最右边的1变成了0,结果刚好就是1000。
我们把上面的分析总结起来就是:把一个整数减去1,再和原整数做与运算,会把该整数最右边的1变成0。那么一个整数的二二进制表示中有多少个1,就可以进行多少次这样的操作。基于这种思路,我们可以写出新的代码:(参考剑指offer)
/**
* 方法2:由于n和n-1作与&比较的时候,最右边的第一个1会变成0,而这个1后边的0会变成1,1会变成0
* 把一个整数减去1,再和原整数做与运算,会把该整数最右边的1变成0。那么一个整数的二二进制表示中有多少个1,
* 就可以进行多少次这样的操作。基于这种思路,我们可以写出新的代码:
*/
public int hammingWeight2(int n) {
int count = 0;
while (n != 0) {
count++;
n = n & (n - 1);
}
return count;
}
对于上边问题一般会有这么个存在缺陷的做法(不推荐的做法):
int count = 0;
while (n != 0) {
if ((n & 1) == 1) {
count++;
}
n = n >> 1;
}
return count;
2.求一个数是不是2的幂次方
/**
* 给定一个整数,编写一个函数来判断它是否是 2 的幂次方。
* 输入: 16
* 输出: true
* 解释: 2^4 = 16
* 算法:一个整数只要是2的幂次方,那么他的二进制表示中肯定就只有1个1,
* 因此,对这个数的二进制表示减1的时候,原来1的位置会变成0,原来是0的地方会变成1
* 将减1的数字与源数字进行与运算,如果源数字只有1个1的时候,那么与运算后,整个数会变成0
*/
public boolean isPowerOfTwo(int n) {
if (n == 0) {
return false;
}
long x = (long)n;
if ((x & (x - 1)) == 0) {
return true;
}
return false;
}
3.来个小结
把一个整数减去1之后再和原来的整数做位与运算,得到的结果相当于把整数的二进制表示中最右边的1变成0。很多二进制的问题都可以用这种思路解决。
3.1 与、或、异或、按位取反的运算规律总结
按位取反:~ :二进制位中的0变1,1变0
3.2 左移运算符"<<" 与右移运算符">>"
左移运算符m<<n表示把m左移n位。在左移n位的时候,最左边的n位将被丢弃,同时在最右边补上n个0。比如:
00001010<< 2 = 00101000
10001010<< 3 = 01010000
右移运算符m>>n表示把m右移n位。在右移n位的时候,最右边的n位将被丢弃。但右移时处理最左边位的情形要稍微复杂-一点。如果数字是一个无符号数值, 则用0填补最左边的n位:如果数字是一个有符号数值,则用数字的符号位填补最左边的n位。也就是说,如果数字原先是一个正数,则右移之后在最左边补n个0;如果数字原先是负数,则右移之后在最左边补n个1。下面是对两个8位有符号数进行右移的例子:
00001010>> 2 = 00000010
10001010>> 3 = 11110001
4.继续刷题:不用额外的变量交换两个整数的值(异或应用)
如何不用任何额外变量交换两个整数的值?
说明:
异或^,其实表示的是存储两个元素的不同属性,0 ^ 0 = 0;1 ^ 0 = 1; 0 ^ 1 = 1;1 ^ 1 = 0
明显地,可以看出,0表示的是相同的属性,1表示的是不同的属性
使用异或运算交换两个数字,第一次使用a记录两者的不同信息,第二次使用b记录与b相反的信息,第三次使用a记录与新b相反的信息,代码:
/**
* 编写一个函数,不用临时变量,直接交换numbers = [a, b]中a与b的值。
* 算法:
* 如果给定整数a和b,用以下三行代码即可交换a和b的值。
* a=a^b;
* b=a^ b;
* a=a^ b;
* 如何理解这三行代码的具体功能呢?首先要理解关于异或运算的特点:
* 假设a异或 b的结果记为c,c就是a整数位信息和b整数位信息的所有不同信息。
* 比如,a=4=100, b=3=011,a^b=c=111。
* a异或c的结果就是b。比如a=4=100, c= =111, a^c=011=3=b。
* b异或c的结果就是a。比如b=3=011, c= =111, b^c=100=4=a。
*/
public int[] swapNumbers(int[] numbers) {
numbers[0] = numbers[0] ^ numbers[1];
numbers[1] = numbers[0] ^ numbers[1];
numbers[0] = numbers[0] ^ numbers[1];
return numbers;
}
5. 不用任何比较判断找出两个数中较大的数
public int flip(int n) {
return n ^ 1;
}
public int sign(int n) {
return flip((n >> 31) & 1);
}
public int getMax1(int a, int b) {
int c = a - b;
int scA = sign(c);
int scB = flip(scA);
return a * scA + b * scB;
}
sign函数的功能是返回整数n的符号,正数和0返回1,负数则返回0。flip 函数的功能是如果n为1,返回0,如果n为0,返回1。所以,如果a-b的结果为0或正数,那么scA为1,scB为0;如果a-b的值为负数,那么scA为0,scB为1。scA和scB必有一个
为1,另一个必为0。所以returna* scA+b* scB;,就是根据a-b的值的状况,选择要么返回a,,要么返回b。
但方法一是有局限性的,那就是如果a-b的值出现溢出,返回结果就不正确。
第二种方法可以彻底解决溢出的问题,也就是如下代码中的getMax2方法。
public int flip(int n) {
return n ^ 1;
}
public int sign(int n) {
return flip((n >> 31) & 1);
}
public int getMax2(int a, int b) {
int c = a - b;
int sa = sign(a);
int sb = sign(b);
int sc = sign(c);
int difSab = sa ^ sb;
int sameSab = flip(difSab);
int returnA = difSab * sa + sameSab * sc;
int returnB = flip(returnA);
return a * returnA + b * returnB;
}
借鉴左神的程序员面试指南:
6. 只用位运算不用算术运算实现整数的加减乘除运算
public int add(int a, int b) {
int sum,carry;
while (b != 0) {
sum = a ^ b;
carry = (a & b) << 1;
a = sum;
b = carry;
}
return a;
}
解释(剑指offer):