LintCode上位运算&二进制的题目不多,加起来就10道题左右,把他们全部都AC一下,位运算这块就差不多了。面试的时候位运算也不是经常考察的点。因为这东西你知道就指导,不知道就不知道,没有啥深入探究讨论的余地。
对两个32位的整数求和,这道题要是直接回答return A + B那就只能等着收拒信了。这道题考查的是会不会用位运算做加法,如果用位运算的话,运行时间是比直接return A+B快得多的。
主要利用异或运算来完成,异或运算,相等为0,不同为1。如果转换为加法,异或运算有一个别名叫做:不进位加法。那么a ^ b就是a和b相加之后,该进位的地方不进位的结果。
然后下面考虑哪些地方要进位,自然是a和b里都是1的位置需要进位。a & b就是a和b里都是1的那些位置,a & b << 1 就是进位之后的结果。所以:a + b = (a ^ b) + (a & b << 1)
令x = a ^ b, y = (a & b) << 1 可以知道,这个过程是在模拟加法的运算过程,进位不可能一直持续,所以b最终会变为0。因此重复做上述操作就可以求得a + b的值。
public int aplusb(int a, int b) {
while (b != 0) {
int x = a ^ b;
int y = (a & b) << 1;
a = x;
b = y;
}
return a;
}
408. Add Binary
对两个用String类型表示的二进制串进行加法操作。思路很简单,就是模拟加法,首先要把2个string补成长度相等,比如A='1', B='11',那就要在A前面补一个0。当2个字符串长度相等后,再用循环来对每一位进行加法操作,用一个额外变量carry表示进位。
public String addBinary(String a, String b) {
// 补位,让2个string等长
int length = a.length() > b.length() ? a.length() : b.length();
if (a.length() < length) {
int toGo = length - a.length();
for (int i = 0; i < toGo; i++) {
a = "0" + a;
}
}
if (b.length() < length) {
int toGo = length - b.length();
for (int i = 0; i < toGo; i++) {
b = "0" + b;
}
}
// 模拟加法操作
String res = "";
int carry = 0;
for (int i = length - 1; i >= 0; i--) {
int val = (int)(a.charAt(i) - '0') + (int)(b.charAt(i) - '0') + carry;
if (val > 1) {
carry = 1;
val = val % 2;
} else {
carry = 0;
}
res = val + res;
}
if (carry == 1) {
res = 1 + res;
}
return res;
}
365. Count 1 in Binary
输入一个32位整数,求它里面有多少个二进制的1存在。
每次“&”都会去掉num最右边的1.
如果一个整数不为0,那么这个整数至少有一位是1。如果我们把这个整数减1,那么原来处在整数最右边的1就会变为0,原来在1后面的所有的0都会变成1(如果最右边的1后面还有0的话)。其余所有位将不会受到影响。
举个例子:一个二进制数1100,从右边数起第三位是处于最右边的一个1。减去1后,第三位变成0,它后面的两位0变成了1,而前面的1保持不变,因此得到的结果是1011.
我们发现减1的结果是把最右边的一个1开始的所有位都取反了。这个时候如果我们再把原来的整数和减去1之后的结果做与运算,从原来整数最右边一个1那一位开始所有位都会变成0。
如1100&1011=1000.也就是说,把一个整数减去1,再和原整数做与运算,会把该整数最右边一个1变成0.那么一个整数的二进制有多少个1,就可以进行多少次这样的操作。
public int countOnes(int num) {
int count = 0;
while (num != 0) {
num = num & (num - 1);
count++;
}
return count;
}
格雷码(Gray Code)是一个数列集合,每个数使用二进位来表示,假设使用n位元来表示每个数字,任两个数之间只有一个位元值不同。
例如以下为3位元的格雷码: 000 001 011 010 110 111 101 100 。
如果要产生n位元的格雷码,那么格雷码的个数为2^n.
假设原始的值从0开始,格雷码产生的规律是:
第一步,改变最右边的位元值;
第二步,改变右起第一个为1的位元的左边位元;
第三步,第四步重复第一步和第二步,直到所有的格雷码产生完毕(换句话说,已经走了(2^n) - 1 步)。
用一个例子来说明: 假设产生3位元的格雷码,原始值位 000
第一步:改变最右边的位元值: 001
第二步:改变右起第一个为1的位元的左边位元: 011
第三步:改变最右边的位元值: 010
第四步:改变右起第一个为1的位元的左边位元: 110
第五步:改变最右边的位元值: 111
第六步:改变右起第一个为1的位元的左边位元: 101
第七步:改变最右边的位元值: 100
如果按照这个规则来生成格雷码,是没有问题的,但是这样做太复杂了。如果仔细观察格雷码的结构,我们会有以下发现除了最高位(左边第一位),格雷码的位元完全上下对称(看下面列表)。比如第一个格雷码与最后一个格雷码对称(除了第一位),第二个格雷码与倒数第二个对称,以此类推。
000
001
011
010
110
111
101
100
所以,在实现的时候,我们完全可以利用递归,在每一层前面加上0或者1,然后就可以列出所有的格雷码。 比如:
第一步:产生 0, 1 两个字符串。
第二步:在第一步的基础上,每一个字符串都加上0和1,但是每次只能加一个,所以得做两次。这样就变成了 00,01,11,10 (注意对称)。
第三步:在第二步的基础上,再给每个字符串都加上0和1,同样,每次只能加一个,这样就变成了 000,001,011,010,110,111,101,100。
好了,这样就把3位元格雷码生成好了。
如果要生成4位元格雷码,我们只需要在3位元格雷码上再加一层0,1就可以了: 0000,0001,0011,0010,0110,0111,0101,0100,1100,1101,1110,1010,0111,1001,1000.
也就是说,n位元格雷码是基于n-1位元格雷码产生的。 如果能够理解上面的部分,下面部分的代码实现就易理解了。
public ArrayList<Integer> grayCode(int n) {
ArrayList<Integer> res = new ArrayList<Integer>();
if (n <= 0) {
res.add(0);
return res;
}
res = grayCode(n - 1);
for (int i = res.size() - 1; i >= 0; i--) {
int num = res.get(i);
num += (1 << (n - 1));
res.add(num);
}
return res;
}
判断2个数的二进制位上有多少位是不同的。就先对两个数进行一下异或操作,得到的结果就是一个包含2个数不同位信息的数,比如100和011异或的结果是111,因为100和011三个位置都不同,所以有3个位置不同,所以结果是111.然后我们用位操作统计这个111里面有多少个1,就行了
public static int bitSwapRequired(int a, int b) {
// 异或的结果就是两数不同的位置的值为1
int xor = a ^ b;
// 然后统计这个数里面有多少个1
int count = 0;
while (xor != 0) {
xor = xor & (xor - 1);
count++;
}
return count;
}
用O(1)的时间检查一个数是否是2的平方。这里可以用位运算。我们可以先列举一下,2的倍数的二进制数分别是10、100、1000、10000、100000等等。找到规律后,可以发现
符合这种规律的数都是最左边是一个1,然后右边都是0.那我们把它减去1之后再看看,分别是01、011、0111、01111、011111等等。然后再把他两做&操作,发现得到的都是0了。这个规律是肯定成立的,利用这个规律就可以在O(1)时间内判断了:
public boolean checkPowerOf2(int n) {
if (n <= 0) {
return false;
}
return (n & (n - 1)) == 0;
}
这道题如果转换为用二进制串俩做是比较简单的,如果一定要用位运算来做,其实这道题并不难,至少不涉及太多的数据结构和算法,需要的只是一些数学知识和位运算的知识,和一些细心与耐心。 首先,因为涉及到位运算,当然要将所有数字表示成32位二进制数的形式(题目中给出的不满32位的形式不利于解题)。拿题目中的例子来说:
N=(10000000000)2 M=(10101)2 即N=1024,将其表示成32位的形式,即在前面补0得到
N=(00000000 00000000 00000100 00000000)2 M=(00000000 00000000 00000000 00010101)2。Update之后N=(10001010100)2,
即N=(00000000 00000000 00000000 00000100 01010100)2。其中加粗的部分即M。与Update之前的N对比,可以发现,除了i到j这一部分,其他部分与原数据相同。
由位运算的性质可知,要保留一个数不变,需要与1做与运算,而变成M的部分可以先将该部分变成0,然后再与M做或运算,或者直接将M移位后做加法。好了,思路有了之后,我们来看代码。
public int updateBits(int n, int m, int i, int j) {
int mask;
if (j < 31) {
mask = ~((1 << (j + 1)) - (1 << i));
} else {
mask = (1 << i) - 1;
}
return (mask & n) + (m << i);
}
其中的mask即我们需要的掩码,以题目中的例子为例,mask=(11111111 11111111 11111111 10000011)2,这样与n做与运算,就可以做到我们前面说的保留i到j之外的部分,同时i到j之间的部分清0,然后在和左移i位的m相加就得到最后的结果。
首先,这个的输入是小数,这里就涉及到小数的二进制表示法。整数部分表示成二进制相信大家都会,只要不断的对2取余,然后再将余数反转就可以。那么小数部分如何表示成二进制,方法如下:
对十进制小数乘2,得到的整数部分和小数部分,整数部分既是相应的二进制数码,再用2乘小数部分(之前乘后得到新的小数部分),又得到整数和小数部分。如此不断重复,直到小数部分为0或达到精度要求为止。第一次所得到为最高位,最后一次得到为最低位如:0.25的二进制0.25*2=0.5 取整是0。0.5*2=1.0 取整是1即0.25的二进制为 0.01 ( 第一次所得到为最高位,最后一次得到为最低位)。例子如下:
0.8125的二进制
0.8125*2=1.625 取整是1
0.625*2=1.25 取整是1
0.25*2=0.5 取整是0
0.5*2=1.0 取整是1
即0.8125的二进制是0.1101(第一次所得到为最高位,最后一次得到为最低位)
在上述的计算方法中,可能出现无限循环的情况,因此必须控制精度。题目中已经给出32位小数精度,超过则返回ERROR。
private String left(String n) {
int num = Integer.parseInt(n);
String res = "";
while (num != 0) {
res = (num % 2) + res;
num /= 2;
}
return res;
}
private String right(String n) {
double num = Double.parseDouble(n);
if (num == 0.0) {
return "0";
}
String res = "";
while (num < 1) {
if (res.length() >= 32) {
return "ERROR";
}
num = num * 2;
if (num < 1) {
res += "0";
} else if (num == 1) {
res += "1";
return res;
} else {
res += "1";
num -= 1;
}
}
return res;
}
public String binaryRepresentation(String n) {
if (n == null || Double.parseDouble(n) == 0) {
return "0";
}
int pos = n.indexOf('.');
// 只有整数部分
if (pos == -1) {
return left(n);
}
String l = left(n.substring(0, pos));
String r = right("0." + n.substring(pos + 1));
// 右边假如就是0.0000,那就直接返回整数部分
if (r == "0") {
return l;
}
// 右边假如是0.XXXX的形式
if (pos == 1 && n.charAt(0) == '0') {
return "0." + right(n);
}
// 超过32位精度
if (r == "ERROR") {
return r;
}
// 整数和小数部分都有
return l + "." + r;
}
题目不算难,但我硬是提交了5次才完全AC。主要是处理Corner case不到位,这也是为啥这道题也被归类为Hard的原因吧!